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}