avian3d/dynamics/integrator/mod.rs
1//! Applies forces and velocities to bodies in order to move them according to the equations of motion
2//! using numerical integration.
3//!
4//! See [`IntegratorPlugin`].
5
6use crate::prelude::*;
7use bevy::{
8 ecs::{intern::Interned, query::QueryData, schedule::ScheduleLabel},
9 prelude::*,
10};
11use dynamics::solver::SolverDiagnostics;
12
13use super::solver::solver_body::SolverBody;
14
15/// Integrates Newton's 2nd law of motion, applying forces and moving entities according to their velocities.
16///
17/// This acts as a prediction for the next positions and orientations of the bodies. The [solver](dynamics::solver)
18/// corrects these predicted positions to take constraints like contacts and joints into account.
19///
20/// Currently, only the [semi-implicit (symplectic) Euler](https://en.wikipedia.org/wiki/Semi-implicit_Euler_method)
21/// integration scheme is supported. It is the standard for game physics, being stable, efficient, and sufficiently accurate.
22///
23/// See [`IntegrationSystems`] for the system sets used by this plugin.
24pub struct IntegratorPlugin {
25 schedule: Interned<dyn ScheduleLabel>,
26}
27
28impl IntegratorPlugin {
29 /// Creates an [`IntegratorPlugin`] with the schedule that the integration systems should run in.
30 ///
31 /// The default schedule is [`SubstepSchedule`].
32 pub fn new(schedule: impl ScheduleLabel) -> Self {
33 Self {
34 schedule: schedule.intern(),
35 }
36 }
37}
38
39impl Default for IntegratorPlugin {
40 fn default() -> Self {
41 Self::new(SubstepSchedule)
42 }
43}
44
45impl Plugin for IntegratorPlugin {
46 fn build(&self, app: &mut App) {
47 // Add `VelocityIntegrationData` to all `SolverBody`s.
48 app.register_required_components::<SolverBody, VelocityIntegrationData>();
49
50 app.init_resource::<Gravity>();
51
52 app.configure_sets(
53 PhysicsSchedule,
54 (
55 IntegrationSystems::UpdateVelocityIncrements
56 .in_set(SolverSystems::PreSubstep)
57 .before(IntegrationSystems::Velocity),
58 IntegrationSystems::ClearVelocityIncrements
59 .in_set(SolverSystems::PostSubstep)
60 .after(IntegrationSystems::Velocity),
61 ),
62 );
63
64 app.add_systems(
65 PhysicsSchedule,
66 (
67 pre_process_velocity_increments
68 .in_set(IntegrationSystems::UpdateVelocityIncrements),
69 clear_velocity_increments.in_set(IntegrationSystems::ClearVelocityIncrements),
70 ),
71 );
72
73 app.configure_sets(
74 self.schedule.intern(),
75 (IntegrationSystems::Velocity, IntegrationSystems::Position).chain(),
76 );
77
78 app.add_systems(
79 self.schedule.intern(),
80 (
81 (integrate_velocities, clamp_velocities)
82 .chain()
83 .in_set(IntegrationSystems::Velocity),
84 integrate_positions.in_set(IntegrationSystems::Position),
85 ),
86 );
87 }
88}
89
90/// System sets for position and velocity integration,
91/// applying forces and moving bodies based on velocity.
92#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub enum IntegrationSystems {
94 /// Applies gravity and locked axes to the linear and angular velocity increments of bodies,
95 /// and multiplies them by the substep delta time to get the final per-substep increments.
96 ///
97 /// Runs in the [`PhysicsSchedule`], in [`SolverSystems::PreSubstep`].
98 UpdateVelocityIncrements,
99 /// Applies velocity increments to the linear and angular velocities of bodies.
100 ///
101 /// Typically runs in the [`SubstepSchedule`], in [`IntegrationSystems::Velocity`].
102 Velocity,
103 /// Moves bodies based on their current velocities and the physics time step.
104 ///
105 /// Typically runs in the [`SubstepSchedule`], in [`IntegrationSystems::Position`].
106 Position,
107 /// Clears the velocity increments of bodies after the substepping loop.
108 ///
109 /// Runs in the [`PhysicsSchedule`], in [`SolverSystems::PostSubstep`].
110 ClearVelocityIncrements,
111}
112
113/// A deprecated alias for [`IntegrationSystems`].
114#[deprecated(since = "0.4.0", note = "Renamed to `IntegrationSystems`")]
115pub type IntegrationSet = IntegrationSystems;
116
117/// A resource for the global gravitational acceleration.
118///
119/// The default is an acceleration of 9.81 m/s^2 pointing down, which is approximate to the gravitational
120/// acceleration near Earth's surface. Note that if you are using pixels as length units in 2D,
121/// this gravity will be tiny. You should modify the gravity to fit your application.
122///
123/// You can also control how gravity affects a specific [rigid body](RigidBody) using the [`GravityScale`]
124/// component. The magnitude of the gravity will be multiplied by this scaling factor.
125///
126/// # Example
127///
128/// ```no_run
129#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
130#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
131/// use bevy::prelude::*;
132///
133/// # #[cfg(feature = "f32")]
134/// fn main() {
135/// App::new()
136/// .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
137#[cfg_attr(
138 feature = "2d",
139 doc = " .insert_resource(Gravity(Vec2::NEG_Y * 100.0))"
140)]
141#[cfg_attr(
142 feature = "3d",
143 doc = " .insert_resource(Gravity(Vec3::NEG_Y * 19.6))"
144)]
145/// .run();
146/// }
147/// # #[cfg(not(feature = "f32"))]
148/// # fn main() {} // Doc test needs main
149/// ```
150///
151/// You can also modify gravity while the app is running.
152#[derive(Reflect, Resource, Debug)]
153#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
154#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
155#[reflect(Debug, Resource)]
156pub struct Gravity(pub Vector);
157
158impl Default for Gravity {
159 fn default() -> Self {
160 Self(Vector::Y * -9.81)
161 }
162}
163
164impl Gravity {
165 /// Zero gravity.
166 pub const ZERO: Gravity = Gravity(Vector::ZERO);
167}
168
169/// A marker component for bodies that use custom velocity integration.
170///
171/// This means that gravity, damping, and external forces will not be applied automatically.
172/// You are responsible for applying all forces and updating velocities manually.
173///
174/// Exceptions include:
175///
176/// - Contact impulses and joint impulses for dynamic bodies
177/// - Impulses applied via [`Forces`]
178#[derive(Component, Debug, Default, Reflect)]
179#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
180#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
181#[reflect(Component, Debug, Default)]
182pub struct CustomVelocityIntegration;
183
184/// A marker component for bodies that use custom position integration.
185///
186/// This means that the body's position and rotation will not be updated automatically
187/// based on velocity. You are responsible for updating the position and rotation manually.
188///
189/// This can be useful for implementing kinematic bodies that are moved according to custom logic,
190/// such as with [`MoveAndSlide`].
191#[derive(Component, Debug, Default, Reflect)]
192#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
193#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
194#[reflect(Component, Debug, Default)]
195pub struct CustomPositionIntegration;
196
197/// Pre-computed data for speeding up velocity integration.
198///
199/// This includes:
200///
201/// - Velocity increments for [`Gravity`].
202/// - Velocity increments for [`ConstantForce`], [`ConstantTorque`], [`ConstantLinearAcceleration`], and [`ConstantAngularAcceleration`].
203/// - Velocity increments for forces, torques, and accelerations applied using [`Forces`].
204/// - Cached operands for applying linear and angular velocity damping.
205///
206/// The values are computed once per time step, and applied to the body at each substep
207/// with basic addition and multiplication. This moves the more expensive operations
208/// and branching out of the substepping loop.
209// -----------------------
210// 20 bytes in 2D with f32
211// 32 bytes in 3D with f32
212#[derive(Component, Debug, Default, PartialEq, Reflect)]
213#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
214#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
215#[reflect(Component, Debug, Default, PartialEq)]
216pub struct VelocityIntegrationData {
217 /// The linear velocity increment to be applied to the body at each substep.
218 ///
219 /// **Note:** This is treated as linear acceleration until [`IntegrationSystems::UpdateVelocityIncrements`].
220 /// where it is multiplied by the time step to get the corresponding velocity increment.
221 pub linear_increment: Vector,
222 /// The angular velocity increment to be applied to the body at each substep.
223 ///
224 /// **Note:** This is treated as angular acceleration until [`IntegrationSystems::UpdateVelocityIncrements`].
225 /// where it is multiplied by the time step to get the corresponding velocity increment.
226 pub angular_increment: AngularVector,
227 /// The right-hand side of the linear damping equation,
228 /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
229 pub linear_damping_rhs: Scalar,
230 /// The right-hand side of the angular damping equation,
231 /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
232 pub angular_damping_rhs: Scalar,
233}
234
235impl VelocityIntegrationData {
236 /// Applies a given linear acceleration to the body.
237 pub fn apply_linear_acceleration(&mut self, acceleration: Vector) {
238 self.linear_increment += acceleration;
239 }
240
241 /// Applies a given angular acceleration to the body.
242 pub fn apply_angular_acceleration(&mut self, acceleration: AngularVector) {
243 self.angular_increment += acceleration;
244 }
245
246 /// Updates the cached right-hand side of the linear damping equation,
247 /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
248 pub fn update_linear_damping_rhs(&mut self, damping_coefficient: Scalar, delta_secs: Scalar) {
249 self.linear_damping_rhs = 1.0 / (1.0 + delta_secs * damping_coefficient);
250 }
251
252 /// Updates the cached right-hand side of the angular damping equation,
253 /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
254 pub fn update_angular_damping_rhs(&mut self, damping_coefficient: Scalar, delta_secs: Scalar) {
255 self.angular_damping_rhs = 1.0 / (1.0 + delta_secs * damping_coefficient);
256 }
257}
258
259/// Applies gravity and locked axes to the linear and angular velocity increments of bodies.
260pub fn pre_process_velocity_increments(
261 mut bodies: Query<(
262 &RigidBody,
263 &mut VelocityIntegrationData,
264 Option<&LinearDamping>,
265 Option<&AngularDamping>,
266 Option<&GravityScale>,
267 Option<&LockedAxes>,
268 )>,
269 gravity: Res<Gravity>,
270 time: Res<Time<Substeps>>,
271 mut diagnostics: ResMut<SolverDiagnostics>,
272) {
273 let start = crate::utils::Instant::now();
274
275 let delta_secs = time.delta_secs_f64() as Scalar;
276
277 // TODO: Do we want to skip kinematic bodies here?
278 bodies.par_iter_mut().for_each(
279 |(rb, mut integration, lin_damping, ang_damping, gravity_scale, locked_axes)| {
280 if !rb.is_dynamic() {
281 // Skip non-dynamic bodies.
282 return;
283 }
284
285 let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
286
287 // Update the cached right-hand side of the velocity damping equation,
288 // `1 / (1 + dt * c)`, where `c` is the damping coefficient.
289 let lin_damping = lin_damping.map_or(0.0, |damping| damping.0);
290 let ang_damping = ang_damping.map_or(0.0, |damping| damping.0);
291 integration.update_linear_damping_rhs(lin_damping, delta_secs);
292 integration.update_angular_damping_rhs(ang_damping, delta_secs);
293
294 // NOTE: The `ForcePlugin` handles the application of external forces and torques.
295 // NOTE: The velocity increments are treated as accelerations at this point.
296
297 // Apply gravity.
298 integration.linear_increment += gravity.0 * gravity_scale.map_or(1.0, |scale| scale.0);
299
300 // Apply locked axes.
301 integration.linear_increment = locked_axes.apply_to_vec(integration.linear_increment);
302 integration.angular_increment =
303 locked_axes.apply_to_angular_velocity(integration.angular_increment);
304
305 // The velocity increments are treated as accelerations until this point.
306 // Multiply by the time step to get the final velocity increments.
307 integration.linear_increment *= delta_secs;
308 integration.angular_increment *= delta_secs;
309 },
310 );
311
312 diagnostics.update_velocity_increments += start.elapsed();
313}
314
315/// Clears the velocity increments of bodies after the substepping loop.
316fn clear_velocity_increments(
317 mut bodies: Query<&mut VelocityIntegrationData, With<SolverBody>>,
318 mut diagnostics: ResMut<SolverDiagnostics>,
319) {
320 let start = crate::utils::Instant::now();
321
322 bodies.par_iter_mut().for_each(|mut integration| {
323 integration.linear_increment = Vector::ZERO;
324 integration.angular_increment = AngularVector::ZERO;
325 });
326
327 diagnostics.update_velocity_increments += start.elapsed();
328}
329
330#[derive(QueryData)]
331#[query_data(mutable)]
332#[doc(hidden)]
333pub struct VelocityIntegrationQuery {
334 solver_body: &'static mut SolverBody,
335 integration: &'static mut VelocityIntegrationData,
336 #[cfg(feature = "3d")]
337 angular_inertia: &'static ComputedAngularInertia,
338 #[cfg(feature = "3d")]
339 rotation: &'static Rotation,
340}
341
342/// Integrates the velocities of bodies by applying velocity increments and damping.
343pub fn integrate_velocities(
344 mut bodies: Query<
345 VelocityIntegrationQuery,
346 (RigidBodyActiveFilter, Without<CustomVelocityIntegration>),
347 >,
348 mut diagnostics: ResMut<SolverDiagnostics>,
349 #[cfg(feature = "3d")] time: Res<Time>,
350) {
351 let start = crate::utils::Instant::now();
352
353 #[cfg(feature = "3d")]
354 let delta_secs = time.delta_secs_f64() as Scalar;
355
356 bodies.par_iter_mut().for_each(|mut body| {
357 if body.solver_body.flags.is_kinematic() {
358 // Skip kinematic bodies.
359 return;
360 }
361
362 // Apply velocity damping.
363 body.solver_body.linear_velocity *= body.integration.linear_damping_rhs;
364 body.solver_body.angular_velocity *= body.integration.angular_damping_rhs;
365
366 // Apply velocity increments.
367 body.solver_body.linear_velocity += body.integration.linear_increment;
368 body.solver_body.angular_velocity += body.integration.angular_increment;
369
370 #[cfg(feature = "3d")]
371 {
372 if body.solver_body.is_gyroscopic() {
373 // TODO: Should this be opt-in with a `GyroscopicMotion` component?
374 // TODO: It's a bit unfortunate that this has to run in the substepping loop
375 // rather than pre-computing the velocity increments once per time step.
376 // This needs to be done because the gyroscopic torque relies on up-to-date rotations
377 // and world-space angular inertia tensors. Omitting the change in orientation would
378 // lead to worse accuracy and angular momentum not being conserved.
379 let rotation = body.solver_body.delta_rotation.0 * body.rotation.0;
380 solve_gyroscopic_torque(
381 &mut body.solver_body.angular_velocity,
382 rotation,
383 body.angular_inertia,
384 delta_secs,
385 );
386 }
387 }
388 });
389
390 diagnostics.integrate_velocities += start.elapsed();
391}
392
393/// Applies the effects of gyroscopic motion to the given angular velocity.
394///
395/// Gyroscopic motion is the tendency of a rotating object to maintain its axis of rotation
396/// unless acted upon by an external torque. It manifests as objects with non-uniform angular
397/// inertia tensors seemingly wobbling as they spin in the air or on the ground.
398///
399/// Gyroscopic motion is important for realistic spinning behavior, and for simulating
400/// gyroscopic phenomena such as the Dzhanibekov effect.
401#[cfg(feature = "3d")]
402#[inline]
403pub fn solve_gyroscopic_torque(
404 ang_vel: &mut Vector,
405 rotation: Quaternion,
406 local_inertia: &ComputedAngularInertia,
407 delta_secs: Scalar,
408) {
409 // References:
410 // - The "Gyroscopic Motion" section of Erin Catto's GDC 2015 slides on Numerical Methods.
411 // https://box2d.org/files/ErinCatto_NumericalMethods_GDC2015.pdf
412 // - Jolt Physics - MotionProperties::ApplyGyroscopicForceInternal
413 // https://github.com/jrouwe/JoltPhysics/blob/d497df2b9b0fa9aaf41295e1406079c23148232d/Jolt/Physics/Body/MotionProperties.inl#L102
414 //
415 // Erin Catto's GDC presentation suggests using implicit Euler for gyroscopic torque,
416 // as semi-implicit Euler can easily blow up with larger time steps due to extrapolating velocity.
417 // The extrapolation diverges quickly because gyroscopic torque is quadratic in the angular velocity.
418 //
419 // However, implicit Euler is measurably more expensive than semi-implicit Euler.
420 // We instead take inspiration from Jolt, and use semi-implicit Euler integration,
421 // clamping the magnitude of the angular momentum to remain the same.
422 // This is efficient, prevents energy from being introduced into the system,
423 // and produces reasonably accurate results for game purposes.
424
425 // Convert angular velocity to body space so that we can use the local angular inertia.
426 let local_ang_vel = rotation.inverse() * *ang_vel;
427
428 // Compute body-space angular momentum.
429 let local_momentum = local_inertia.tensor() * local_ang_vel;
430
431 // The gyroscopic torque is given by:
432 //
433 // T = -ω x I ω = -ω x L
434 //
435 // where ω is the angular velocity, I is the angular inertia tensor,
436 // and L is the angular momentum.
437 //
438 // The change in angular momentum is given by:
439 //
440 // ΔL = T Δt = -ω x L Δt
441 //
442 // Thus, we can compute the new angular momentum as:
443 //
444 // L' = L + ΔL = L - Δt (ω x L)
445 let mut new_local_momentum = local_momentum - delta_secs * local_ang_vel.cross(local_momentum);
446
447 // Make sure the magnitude of the angular momentum remains the same to avoid introducing
448 // energy into the system due to the extrapolation done by semi-implicit Euler integration.
449 let new_local_momentum_length_squared = new_local_momentum.length_squared();
450 if new_local_momentum_length_squared == 0.0 {
451 *ang_vel = Vector::ZERO;
452 return;
453 }
454 new_local_momentum *=
455 (local_momentum.length_squared() / new_local_momentum_length_squared).sqrt();
456
457 // Convert back to world-space angular velocity.
458 let local_inverse_inertia = local_inertia.inverse();
459 *ang_vel = rotation * (local_inverse_inertia * new_local_momentum);
460}
461
462// NOTE: If the majority of bodies have clamped velocities, it would be more efficient
463// to do this in `integrate_velocities` rather than in a separate system.
464// By doing this in a separate system, we're optimizing for the assumption
465// that only some bodies have clamped velocities.
466/// Clamps the velocities of bodies to [`MaxLinearSpeed`] and [`MaxAngularSpeed`].
467fn clamp_velocities(
468 mut bodies: ParamSet<(
469 Query<(&mut SolverBody, &MaxLinearSpeed)>,
470 Query<(&mut SolverBody, &MaxAngularSpeed)>,
471 )>,
472 mut diagnostics: ResMut<SolverDiagnostics>,
473) {
474 let start = crate::utils::Instant::now();
475
476 // Clamp linear velocity.
477 bodies.p0().iter_mut().for_each(|(mut body, max_speed)| {
478 let linear_speed_squared = body.linear_velocity.length_squared();
479 if linear_speed_squared > max_speed.0 * max_speed.0 {
480 body.linear_velocity *= max_speed.0 / linear_speed_squared.sqrt();
481 }
482 });
483
484 // Clamp angular velocity.
485 bodies.p1().iter_mut().for_each(|(mut body, max_speed)| {
486 #[cfg(feature = "2d")]
487 if body.angular_velocity.abs() > max_speed.0 {
488 body.angular_velocity = max_speed.copysign(body.angular_velocity);
489 }
490 #[cfg(feature = "3d")]
491 {
492 let angular_speed_squared = body.angular_velocity.length_squared();
493 if angular_speed_squared > max_speed.0 * max_speed.0 {
494 body.angular_velocity *= max_speed.0 / angular_speed_squared.sqrt();
495 }
496 }
497 });
498
499 diagnostics.integrate_velocities += start.elapsed();
500}
501
502/// Integrates the positions of bodies based on their velocities and the time step.
503pub fn integrate_positions(
504 mut solver_bodies: Query<&mut SolverBody, Without<CustomPositionIntegration>>,
505 time: Res<Time>,
506 mut diagnostics: ResMut<SolverDiagnostics>,
507) {
508 let start = crate::utils::Instant::now();
509
510 let delta_secs = time.delta_seconds_adjusted();
511
512 solver_bodies.par_iter_mut().for_each(|body| {
513 let SolverBody {
514 linear_velocity,
515 angular_velocity,
516 delta_position,
517 delta_rotation,
518 ..
519 } = body.into_inner();
520
521 *delta_position += *linear_velocity * delta_secs;
522 #[cfg(feature = "2d")]
523 {
524 // Note: We should probably use `add_angle_fast` here
525 *delta_rotation = Rotation::radians(*angular_velocity * delta_secs) * *delta_rotation;
526 }
527 #[cfg(feature = "3d")]
528 {
529 delta_rotation.0 =
530 Quaternion::from_scaled_axis(*angular_velocity * delta_secs) * delta_rotation.0;
531 }
532 });
533
534 diagnostics.integrate_positions += start.elapsed();
535}
536
537#[cfg(test)]
538mod tests {
539 use core::time::Duration;
540
541 use approx::assert_relative_eq;
542
543 use crate::prelude::*;
544 use bevy::{mesh::MeshPlugin, prelude::*, time::TimeUpdateStrategy};
545
546 fn create_app() -> App {
547 let mut app = App::new();
548 app.add_plugins((
549 MinimalPlugins,
550 PhysicsPlugins::default(),
551 TransformPlugin,
552 #[cfg(feature = "bevy_scene")]
553 AssetPlugin::default(),
554 #[cfg(feature = "bevy_scene")]
555 bevy::scene::ScenePlugin,
556 MeshPlugin,
557 ));
558 app
559 }
560
561 #[test]
562 fn semi_implicit_euler() {
563 let mut app = create_app();
564 app.insert_resource(SubstepCount(1));
565 app.finish();
566
567 let body_entity = app
568 .world_mut()
569 .spawn((
570 RigidBody::Dynamic,
571 #[cfg(feature = "2d")]
572 {
573 (
574 MassPropertiesBundle::from_shape(&Rectangle::from_length(1.0), 1.0),
575 AngularVelocity(2.0),
576 )
577 },
578 #[cfg(feature = "3d")]
579 {
580 (
581 MassPropertiesBundle::from_shape(&Cuboid::from_length(1.0), 1.0),
582 AngularVelocity(Vector::Z * 2.0),
583 )
584 },
585 ))
586 .id();
587
588 // Step by 100 steps of 0.1 seconds.
589 app.insert_resource(Time::from_hz(10.0));
590 app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f64(
591 1.0 / 10.0,
592 )));
593
594 // Initialize the app.
595 app.update();
596
597 for _ in 0..100 {
598 app.update();
599 }
600
601 // Get the body after the simulation.
602 let entity_ref = app.world_mut().entity(body_entity);
603 let position = entity_ref.get::<Position>().unwrap().0;
604 let rotation = *entity_ref.get::<Rotation>().unwrap();
605 let linear_velocity = entity_ref.get::<LinearVelocity>().unwrap().0;
606 let angular_velocity = entity_ref.get::<AngularVelocity>().unwrap().0;
607
608 // Euler methods have some precision issues, but this seems weirdly inaccurate.
609 assert_relative_eq!(position, Vector::NEG_Y * 490.5, epsilon = 10.0);
610
611 #[cfg(feature = "2d")]
612 assert_relative_eq!(
613 rotation.as_radians(),
614 Rotation::radians(20.0).as_radians(),
615 epsilon = 0.00001
616 );
617 #[cfg(feature = "3d")]
618 assert_relative_eq!(
619 rotation.0,
620 Quaternion::from_rotation_z(20.0),
621 epsilon = 0.01
622 );
623
624 assert_relative_eq!(linear_velocity, Vector::NEG_Y * 98.1, epsilon = 0.0001);
625 #[cfg(feature = "2d")]
626 assert_relative_eq!(angular_velocity, 2.0, epsilon = 0.00001);
627 #[cfg(feature = "3d")]
628 assert_relative_eq!(angular_velocity, Vector::Z * 2.0, epsilon = 0.00001);
629 }
630}