added non-euler-angles-based rotation track

- fixed gimbal lock in GLTF animations
This commit is contained in:
Dmitry Stepanov
2025-10-04 10:26:06 +03:00
parent 10d0e16acd
commit 43952b7ac7
4 changed files with 63 additions and 73 deletions
+12 -3
View File
@@ -260,9 +260,15 @@ fn type_id_to_supported_type(property_type: TypeId) -> Option<(TrackValueKind, V
} else if property_type == TypeId::of::<Vector4<bool>>() {
Some((TrackValueKind::Vector4, ValueType::Vector4Bool))
} else if property_type == TypeId::of::<UnitQuaternion<f32>>() {
Some((TrackValueKind::UnitQuaternion, ValueType::UnitQuaternionF32))
Some((
TrackValueKind::UnitQuaternionEuler,
ValueType::UnitQuaternionF32,
))
} else if property_type == TypeId::of::<UnitQuaternion<f64>>() {
Some((TrackValueKind::UnitQuaternion, ValueType::UnitQuaternionF64))
Some((
TrackValueKind::UnitQuaternionEuler,
ValueType::UnitQuaternionF64,
))
} else {
None
}
@@ -1436,12 +1442,15 @@ impl TrackList {
| TrackValueKind::Vector4 => {
["X", "Y", "Z", "W"].get(i).unwrap_or(&"_")
}
TrackValueKind::UnitQuaternion => match i {
TrackValueKind::UnitQuaternionEuler => match i {
0 => "Pitch",
1 => "Yaw",
2 => "Roll",
_ => "Unknown",
},
TrackValueKind::UnitQuaternion => {
["W", "X", "Y", "Z"].get(i).unwrap_or(&"_")
}
};
let curve_view = TreeBuilder::new(
+41 -18
View File
@@ -31,6 +31,7 @@ use crate::{
},
value::TrackValue,
};
use fyrox_core::algebra::{Quaternion, UnitQuaternion};
/// The kind of track output value, the animation system works only with numeric properties and the number
/// of variants is small.
@@ -51,6 +52,10 @@ pub enum TrackValueKind {
/// A quaternion that represents some rotation. Requires 3 parametric curves, where `XAngle = 0`, `YAngle = 1`,
/// `ZAngle = 2`. The order of rotations is `XYZ`. This triple of curves forms Euler angles which are interpolated
/// and then converted to a quaternion.
UnitQuaternionEuler,
/// A quaternion that represents some rotation. Requires 4 parametric curves for each component.
/// The order is XYZW.
UnitQuaternion,
}
@@ -63,10 +68,11 @@ impl TrackValueKind {
TrackValueKind::Vector2 => 2,
TrackValueKind::Vector3 => 3,
TrackValueKind::Vector4 => 4,
TrackValueKind::UnitQuaternion => {
TrackValueKind::UnitQuaternionEuler => {
// Euler angles
3
}
TrackValueKind::UnitQuaternion => 4,
}
}
}
@@ -98,9 +104,6 @@ pub enum InterpolationMode {
pub struct TrackDataContainer {
curves: Vec<Curve>,
kind: TrackValueKind,
/// Interpolation mode.
#[visit(optional)] // Backward compatibility.
pub mode: InterpolationMode,
}
impl TrackDataContainer {
@@ -117,7 +120,6 @@ impl TrackDataContainer {
curves: (0..kind.components_count())
.map(|_| Curve::default())
.collect(),
mode: Default::default(),
}
}
@@ -206,7 +208,7 @@ impl TrackDataContainer {
}
#[inline(always)]
fn fetch_quaternion(&self, time: f32) -> Option<TrackValue> {
fn fetch_quaternion_euler(&self, time: f32) -> Option<TrackValue> {
if self.curves.len() < 3 {
return None;
}
@@ -218,18 +220,11 @@ impl TrackDataContainer {
let z_curve = self.curves.get_unchecked(2);
// Convert Euler angles to quaternion
let (x, y, z) = match self.mode {
InterpolationMode::Default => (
x_curve.value_at(time),
y_curve.value_at(time),
z_curve.value_at(time),
),
InterpolationMode::ShortPath => (
x_curve.angle_at(time),
y_curve.angle_at(time),
z_curve.angle_at(time),
),
};
let (x, y, z) = (
x_curve.value_at(time),
y_curve.value_at(time),
z_curve.value_at(time),
);
Some(TrackValue::UnitQuaternion(quat_from_euler(
Vector3::new(x, y, z),
@@ -238,6 +233,33 @@ impl TrackDataContainer {
}
}
#[inline(always)]
fn fetch_quaternion(&self, time: f32) -> Option<TrackValue> {
if self.curves.len() < 4 {
return None;
}
// SAFETY: The indices are guaranteed to be correct by the above check.
unsafe {
let x_curve = self.curves.get_unchecked(0);
let y_curve = self.curves.get_unchecked(1);
let z_curve = self.curves.get_unchecked(2);
let w_curve = self.curves.get_unchecked(3);
// Convert Euler angles to quaternion
let (x, y, z, w) = (
x_curve.value_at(time),
y_curve.value_at(time),
z_curve.value_at(time),
w_curve.value_at(time),
);
Some(TrackValue::UnitQuaternion(UnitQuaternion::from_quaternion(
Quaternion::new(w, x, y, z),
)))
}
}
/// Tries to get a value at a given time. The method could fail if the internal set of curves is malformed
/// and cannot produce a desired value (for example, [`Vector3`] can be fetched only if the amount of curves
/// is 3).
@@ -247,6 +269,7 @@ impl TrackDataContainer {
TrackValueKind::Vector2 => self.fetch_vector2(time),
TrackValueKind::Vector3 => self.fetch_vector3(time),
TrackValueKind::Vector4 => self.fetch_vector4(time),
TrackValueKind::UnitQuaternionEuler => self.fetch_quaternion_euler(time),
TrackValueKind::UnitQuaternion => self.fetch_quaternion(time),
}
}
+1 -1
View File
@@ -140,7 +140,7 @@ impl Track {
/// Creates a new track that is responsible in animating a rotation property of a scene node.
pub fn new_rotation() -> Self {
Self {
frames: TrackDataContainer::new(TrackValueKind::UnitQuaternion),
frames: TrackDataContainer::new(TrackValueKind::UnitQuaternionEuler),
binding: ValueBinding::Rotation,
..Default::default()
}
+9 -51
View File
@@ -20,7 +20,6 @@
use super::iter::*;
use super::simplify::*;
use crate::core::algebra::{Quaternion, Unit, UnitQuaternion, Vector3};
use crate::core::log::Log;
use crate::core::math::curve::{Curve, CurveKey, CurveKeyKind};
use crate::core::pool::Handle;
@@ -118,7 +117,10 @@ impl ImportedTarget {
Some(node.local_transform().position().data.as_slice().into())
}
ImportedBinding::Rotation => Some(
quaternion_to_euler(*node.local_transform().rotation().get_value_ref())
node.local_transform()
.rotation()
.get_value_ref()
.coords
.data
.as_slice()
.into(),
@@ -144,7 +146,7 @@ impl ImportedTrack {
target,
curves: match target.binding {
ImportedBinding::Position => <[Vec<CurveKey>; 3]>::default().into(),
ImportedBinding::Rotation => <[Vec<CurveKey>; 3]>::default().into(),
ImportedBinding::Rotation => <[Vec<CurveKey>; 4]>::default().into(),
ImportedBinding::Scale => <[Vec<CurveKey>; 3]>::default().into(),
ImportedBinding::Weight(_) => <[Vec<CurveKey>; 1]>::default().into(),
},
@@ -428,49 +430,6 @@ fn count_morph_targets(target: &gltf::Node) -> usize {
}
}
fn array_to_euler(quaternion: [f32; 4]) -> Vector3<f32> {
quaternion_to_euler(Unit::new_normalize(Quaternion::from(quaternion)))
}
// X - pitch, Y - yaw, Z - roll
// Or maybe X - roll, Y - yaw, Z - pitch
// Or maybe X - roll, Y - pitch, Z - yaw
fn quaternion_to_euler(q: UnitQuaternion<f32>) -> Vector3<f32> {
let (roll, pitch, yaw) = q.euler_angles();
//let (x, y, z) = (pitch, yaw, roll);
let (x, y, z) = (roll, pitch, yaw);
Vector3::new(x, y, z)
}
fn quaternion_to_euler_tangents(
in_tangent: [f32; 4],
value: [f32; 4],
out_tangent: [f32; 4],
) -> (Vector3<f32>, Vector3<f32>) {
let a: UnitQuaternion<f32> = Unit::new_normalize(quaternion_subtract(value, in_tangent).into());
let v: UnitQuaternion<f32> = Unit::new_normalize(value.into());
let b: UnitQuaternion<f32> = Unit::new_normalize(quaternion_add(value, out_tangent).into());
(
quaternion_to_euler(a.rotation_to(&v)),
quaternion_to_euler(v.rotation_to(&b)),
)
}
fn quaternion_add(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
let mut r = [0f32; 4];
for i in 0..4 {
r[i] = a[i] + b[i];
}
r
}
fn quaternion_subtract(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
let mut r = [0f32; 4];
for i in 0..4 {
r[i] = a[i] - b[i];
}
r
}
fn import_sampler<'a, 's, F>(
channel: &'a Channel,
target_handle: Handle<Node>,
@@ -670,11 +629,11 @@ where
ReadOutputs::MorphTargetWeights(_) => return Err(()),
};
let mut track = ImportedTrack::new(target);
for i in 0..3 {
for i in 0..4 {
let curve_keys: Vec<CurveKey> = inputs
.clone()
.zip(out_iter.clone())
.map(|(time, o)| CurveKey::new(time, array_to_euler(o)[i], kind.clone()))
.map(|(time, o)| CurveKey::new(time, o[i], kind.clone()))
.collect::<Vec<_>>();
track.curves[i] = curve_keys;
}
@@ -701,11 +660,10 @@ where
ReadOutputs::MorphTargetWeights(_) => return Err(()),
};
let mut track = ImportedTrack::new(target);
for i in 0..3 {
for i in 0..4 {
let curve_keys: Vec<CurveKey> = iter_cubic_data(inputs.clone(), out_iter.clone())
.map(|(time, o)| {
let (in_tang, out_tang) = quaternion_to_euler_tangents(o[0], o[1], o[2]);
let value = array_to_euler(o[1]);
let (in_tang, value, out_tang) = (o[0], o[1], o[2]);
CurveKey::new(
time,
value[i],