A26: сборка кватерниона из каналов дорожки, с юнит-тестом
Some checks are pending
CI / checks (push) Waiting to run

Нормировка выбрана замером: у кватерниона модуль вектора поворота не больше
единицы. Для кости 0 библиотеки GMod сырое 17013 даёт 0.52 при делении на 32768,
а умножение на rot_scale кости давало 1.63 — для кватерниона невозможно. Поэтому
канал делится на 32768, w считается как √(1 − x² − y² − z²), результат
нормируется.

Тест проверяет три вещи: нулевые каналы дают единичный кватернион, полуоборот
вокруг Z даёт z = ±1 и w = 0, и модуль всегда равен единице на разных сырых
значениях (1, 17013, −5000, 40000) — это и есть критерий, по которому выбран
масштаб.

Все 23 сьюта зелёные.
This commit is contained in:
Niko Marmeladkov 2026-10-17 12:14:10 +03:00
parent d92b962b36
commit 133bee0aed

View file

@ -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<Sequence> {
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;