A26: сборка кватерниона из каналов дорожки, с юнит-тестом
Some checks are pending
CI / checks (push) Waiting to run
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:
parent
d92b962b36
commit
133bee0aed
1 changed files with 36 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue