#1010: recover a static Orientation's Y angle without losing precision near gimbal lock
uniform_parent_pose took the Y angle from asin, which loses precision within about 1e-6 degrees of +-90. The Y angle now comes from atan2 on cos(Y), as the X and Z angles already do. The gimbal branch and the principal-range output are unchanged. A new test parents a static Orientation with Y 1e-6 degrees off gimbal lock under a uniform parent. Its largest corner displacement was 3.3e-8 px before this change and 1.5e-14 px after. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
470ea7a3af
commit
be99fecfdc
|
|
@ -743,9 +743,10 @@ fn uniform_parent_pose(m: effectcraft_geom::Mat4, orientation: [f64; 3]) -> Opti
|
|||
return Some((orientation, factor));
|
||||
}
|
||||
let r = (rotation * Mat4::orientation(Vec3::from(orientation))).0;
|
||||
let sy = (-r[2][0]).clamp(-1.0, 1.0);
|
||||
let y = sy.asin();
|
||||
let (x, z) = if (1.0 - sy * sy).sqrt() > 1e-9 { (r[2][1].atan2(r[2][2]), r[1][0].atan2(r[0][0])) } else { ((-r[1][2]).atan2(r[1][1]), 0.0) };
|
||||
// atan2 rather than asin: near ±90 degrees asin loses precision, while cos(y) stays exact.
|
||||
let cy = r[0][0].hypot(r[1][0]);
|
||||
let y = (-r[2][0]).atan2(cy);
|
||||
let (x, z) = if cy > 1e-9 { (r[2][1].atan2(r[2][2]), r[1][0].atan2(r[0][0])) } else { ((-r[1][2]).atan2(r[1][1]), 0.0) };
|
||||
Some(([x.to_degrees(), y.to_degrees(), z.to_degrees()], factor))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -383,6 +383,29 @@ fn parenting_static_orientation_handles_signed_uniform_scale_and_gimbal_order()
|
|||
assert!(max_corners < 1e-8 && max_pixels < 2e-5, "static uniform pose must use the existing Euler order including signed scale and gimbal angles");
|
||||
}
|
||||
|
||||
/// A static Orientation just off gimbal lock (Y within 1e-6 degrees of ±90) keeps its world pose
|
||||
/// under a uniform parent. Recovering Euler angles from the matrix must not lose precision there.
|
||||
#[test]
|
||||
fn parenting_static_orientation_is_exact_just_off_gimbal_lock() {
|
||||
let mut max_corners = 0.0f64;
|
||||
for factor in [120.0, -120.0] {
|
||||
for y in [90.0 - 1e-6, -90.0 + 1e-6] {
|
||||
let (mut s, p, c) = fixture(true, true);
|
||||
set(&mut s, p, "transform/orientation", json!([0.0, 0.0, 0.0]));
|
||||
set(&mut s, p, "transform/rotationX", json!(0.0));
|
||||
set(&mut s, p, "transform/rotationY", json!(0.0));
|
||||
set(&mut s, p, "transform/rotation", json!(42.0));
|
||||
set(&mut s, p, "transform/scale", json!([factor, factor, factor]));
|
||||
set(&mut s, c, "transform/orientation", json!([23.0, y, -37.0]));
|
||||
set(&mut s, c, "transform/scale", json!([200.0, 160.0, 150.0]));
|
||||
let before = world(&s, c, Tick::ZERO);
|
||||
parent(&mut s, c, Some(p), true);
|
||||
max_corners = max_corners.max(displacement(before, world(&s, c, Tick::ZERO)));
|
||||
}
|
||||
}
|
||||
assert!(max_corners < 1e-9, "a static Orientation near gimbal lock must keep its world pose: corners {max_corners}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parenting_animated_orientation_and_nonuniform_parent_keep_authored_pose_boundary() {
|
||||
for animated in [true, false] {
|
||||
|
|
|
|||
Loading…
Reference in New Issue