diff --git a/crates/formats/src/mdl.rs b/crates/formats/src/mdl.rs index 4234378..8dab58f 100644 --- a/crates/formats/src/mdl.rs +++ b/crates/formats/src/mdl.rs @@ -717,6 +717,24 @@ impl<'a> Mdl<'a> { value } + /// Кватернион поворота из трёх каналов дорожки. + /// + /// Нормировка выбрана замером, а не на глаз: сырые значения ANIMROT — + /// шорты, и у кватерниона модуль вектора поворота не может быть больше + /// единицы. Для кости 0 библиотеки GMod сырое 17013 даёт 0.52 при делении + /// на 32768, а умножение на `rot_scale` кости давало 1.63 — для + /// кватерниона невозможно. Поэтому делим на 32768, а результат + /// нормируем. + pub fn quaternion_from_channels(x: f32, y: f32, z: f32) -> [f32; 4] { + let x = (x / 32768.0).clamp(-1.0, 1.0); + let y = (y / 32768.0).clamp(-1.0, 1.0); + let z = (z / 32768.0).clamp(-1.0, 1.0); + let w2 = 1.0 - x * x - y * y - z * z; + let w = if w2 > 0.0 { w2.sqrt() } else { 0.0 }; + let len = (x * x + y * y + z * z + w * w).sqrt().max(1e-6); + [x / len, y / len, z / len, w / len] + } + /// Одна секвенция по смещению. `None` — данные не читаются. fn parse_sequence(&self, base: usize) -> Option { let name_offset = i32_at(self.data, base + 4)?; @@ -1047,6 +1065,24 @@ mod tests { )); } + #[test] + fn quaternion_from_channels_is_unit_and_sane() { + use super::Mdl; + // Нулевые каналы — единичный кватернион (поза покоя). + let q = Mdl::quaternion_from_channels(0.0, 0.0, 0.0); + assert!((q[3] - 1.0).abs() < 1e-6, "w единичного: {q:?}"); + // Полуоборот вокруг Z: z = 32768/32768 → 1 (с клампом), w = 0. + let q = Mdl::quaternion_from_channels(0.0, 0.0, 32768.0); + assert!((q[2].abs() - 1.0).abs() < 1e-3, "z полуоборота: {q:?}"); + assert!(q[3].abs() < 1e-3, "w полуоборота: {q:?}"); + // Модуль всегда единичный — это и есть критерий замера. + for raw in [1.0f32, 17013.0, -5000.0, 40000.0] { + let q = Mdl::quaternion_from_channels(raw, raw / 2.0, -raw / 3.0); + let len = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt(); + assert!((len - 1.0).abs() < 1e-4, "модуль {raw}: {len}"); + } + } + #[test] fn garbage_never_panics() { let mut state: u32 = 0xDEAD_BEEF;