Warnings and docs for exponential denormalization in rotate functions (alternative to #17604) (#17646)
# Objective - When obtaining an axis from the transform and putting that into `Transform::rotate_axis` or `Transform::rotate_axis_local`, floating point errors could accumulate exponentially, resulting in denormalized rotation. - This is an alternative to and closes #17604, due to lack of consent around this in the [discord discussion](https://discord.com/channels/691052431525675048/1203087353850364004/1334232710658392227) - Closes #16480 ## Solution - Add a warning of this issue and a recommendation to normalize to the API docs. - Add a runtime warning that checks for denormalized axis in debug mode, with a reference to the API docs.
This commit is contained in:
parent
2d66099f3d
commit
bdf60d6933
@ -8,6 +8,29 @@ use bevy_ecs::{component::Component, prelude::require};
|
|||||||
#[cfg(feature = "bevy_reflect")]
|
#[cfg(feature = "bevy_reflect")]
|
||||||
use {bevy_ecs::reflect::ReflectComponent, bevy_reflect::prelude::*};
|
use {bevy_ecs::reflect::ReflectComponent, bevy_reflect::prelude::*};
|
||||||
|
|
||||||
|
/// Checks that a vector with the given squared length is normalized.
|
||||||
|
///
|
||||||
|
/// Warns for small error with a length threshold of approximately `1e-4`,
|
||||||
|
/// and panics for large error with a length threshold of approximately `1e-2`.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
fn assert_is_normalized(message: &str, length_squared: f32) {
|
||||||
|
use bevy_math::ops;
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::eprintln;
|
||||||
|
|
||||||
|
let length_error_squared = ops::abs(length_squared - 1.0);
|
||||||
|
|
||||||
|
// Panic for large error and warn for slight error.
|
||||||
|
if length_error_squared > 2e-2 || length_error_squared.is_nan() {
|
||||||
|
// Length error is approximately 1e-2 or more.
|
||||||
|
panic!("Error: {message}",);
|
||||||
|
} else if length_error_squared > 2e-4 {
|
||||||
|
// Length error is approximately 1e-4 or more.
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
eprintln!("Warning: {message}",);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Describe the position of an entity. If the entity has a parent, the position is relative
|
/// Describe the position of an entity. If the entity has a parent, the position is relative
|
||||||
/// to its parent position.
|
/// to its parent position.
|
||||||
///
|
///
|
||||||
@ -312,8 +335,21 @@ impl Transform {
|
|||||||
/// Rotates this [`Transform`] around the given `axis` by `angle` (in radians).
|
/// Rotates this [`Transform`] around the given `axis` by `angle` (in radians).
|
||||||
///
|
///
|
||||||
/// If this [`Transform`] has a parent, the `axis` is relative to the rotation of the parent.
|
/// If this [`Transform`] has a parent, the `axis` is relative to the rotation of the parent.
|
||||||
|
///
|
||||||
|
/// # Warning
|
||||||
|
///
|
||||||
|
/// If you pass in an `axis` based on the current rotation (e.g. obtained via [`Transform::local_x`]),
|
||||||
|
/// floating point errors can accumulate exponentially when applying rotations repeatedly this way. This will
|
||||||
|
/// result in a denormalized rotation. In this case, it is recommended to normalize the [`Transform::rotation`] after
|
||||||
|
/// each call to this method.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn rotate_axis(&mut self, axis: Dir3, angle: f32) {
|
pub fn rotate_axis(&mut self, axis: Dir3, angle: f32) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
assert_is_normalized(
|
||||||
|
"The axis given to `Transform::rotate_axis` is not normalized. This may be a result of obtaining \
|
||||||
|
the axis from the transform. See the documentation of `Transform::rotate_axis` for more details.",
|
||||||
|
axis.length_squared(),
|
||||||
|
);
|
||||||
self.rotate(Quat::from_axis_angle(axis.into(), angle));
|
self.rotate(Quat::from_axis_angle(axis.into(), angle));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,8 +386,21 @@ impl Transform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rotates this [`Transform`] around its local `axis` by `angle` (in radians).
|
/// Rotates this [`Transform`] around its local `axis` by `angle` (in radians).
|
||||||
|
///
|
||||||
|
/// # Warning
|
||||||
|
///
|
||||||
|
/// If you pass in an `axis` based on the current rotation (e.g. obtained via [`Transform::local_x`]),
|
||||||
|
/// floating point errors can accumulate exponentially when applying rotations repeatedly this way. This will
|
||||||
|
/// result in a denormalized rotation. In this case, it is recommended to normalize the [`Transform::rotation`] after
|
||||||
|
/// each call to this method.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn rotate_local_axis(&mut self, axis: Dir3, angle: f32) {
|
pub fn rotate_local_axis(&mut self, axis: Dir3, angle: f32) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
assert_is_normalized(
|
||||||
|
"The axis given to `Transform::rotate_axis_local` is not normalized. This may be a result of obtaining \
|
||||||
|
the axis from the transform. See the documentation of `Transform::rotate_axis_local` for more details.",
|
||||||
|
axis.length_squared(),
|
||||||
|
);
|
||||||
self.rotate_local(Quat::from_axis_angle(axis.into(), angle));
|
self.rotate_local(Quat::from_axis_angle(axis.into(), angle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user