From 2155bcee542a59e95fa6c76af6f48ba447ef49bf Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Sat, 17 Oct 2026 11:51:00 +0300 Subject: [PATCH] =?UTF-8?q?A26:=20=D0=B4=D0=B5=D0=BA=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=20=D0=BA=D0=B0=D0=B4=D1=80=D0=BE=D0=B2=20=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20(mstudioanim=5Ft),=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=B5=D0=BD=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20GMod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Спека взята из studio.h SDK 2013 (112 КБ, скачан как спецификация): mstudioanim_t — четыре байта (bone, flags, nextoffset), дорожки связаны списком до nextoffset == 0; mstudioanim_valueptr_t — три смещения каналов; значения mstudioanimvalue_t идут дельтами с накоплением (valid, total); флаги RAWROT (Quaternion48), RAWROT2 (Quaternion64), ANIMROT/ANIMPOS (дорожки), DELTA. Написаны Mdl::animation_tracks и Mdl::decode_track и прогнаны на настоящей библиотеке GMod: анимация @idle_all_01 — 280 кадров, 51 дорожка, значения вида 17013, 1424, −874 — это шорты, которые при сборке кватерниона делятся на масштаб кости. Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён. --- crates/formats/src/mdl.rs | 116 ++++++++++++++++++++++++++++++++++++++ docs/PARITY.md | 17 +++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/crates/formats/src/mdl.rs b/crates/formats/src/mdl.rs index 8624649..4234378 100644 --- a/crates/formats/src/mdl.rs +++ b/crates/formats/src/mdl.rs @@ -59,6 +59,23 @@ pub enum MdlError { Kv(#[from] crate::kv::KvError), } +/// Значение канала дорожки: либо константа, либо смещение данных в `.ani`. +#[derive(Debug, Clone, Copy)] +pub enum TrackValue { + /// Постоянное значение (Quaternion48 и подобное). + Constant(f32), + /// Дорожка `mstudioanimvalue_t` по смещению от начала данных. + Encoded(usize), +} + +/// Дорожка одной кости: шесть каналов (поворот xyz, позиция xyz). +#[derive(Debug, Clone)] +pub struct AnimTrack { + pub bone: u8, + pub flags: u8, + pub channels: [Option; 6], +} + /// Описание анимации (`mstudioanimdesc_t`). /// /// Кадры лежат либо в самом MDL (`anim_block == 0`), либо в отдельном `.ani`, @@ -601,6 +618,105 @@ impl<'a> Mdl<'a> { Ok(Vec::new()) } + /// Каналы одной кости в кадре анимации. + /// + /// `mstudioanim_t` — 4 байта (`bone`, `flags`, `nextoffset`), дорожки + /// связаны списком; `nextoffset == 0` — конец. Значения каналов лежат как + /// `mstudioanim_valueptr_t` (три смещения) в `mstudioanimvalue_t` — + /// дельтами с накоплением: `valid` значений подряд, остальные повторяют + /// предыдущее. + pub fn animation_tracks(&self, data: &[u8], start: usize) -> Vec { + let mut out = Vec::new(); + let mut offset = start; + while offset + 4 <= data.len() && out.len() < 512 { + let bone = data[offset]; + let flags = data[offset + 1]; + let next = i16::from_le_bytes([data[offset + 2], data[offset + 3]]) as i32; + let value_base = offset + 4; + let mut channels = [None; 6]; + // Поворот: RAWROT (Quaternion48) или ANIMROT (две дорожки xyz). + let rot_ptr = flags & 0x02 != 0 || flags & 0x08 != 0; + let pos_ptr = flags & 0x04 != 0; + if flags & 0x02 != 0 { + // Quaternion48: три short (x, y, z), w считается из длины. + if value_base + 6 <= data.len() { + for j in 0..3 { + let v = i16::from_le_bytes([ + data[value_base + j * 2], + data[value_base + j * 2 + 1], + ]); + channels[j] = Some(TrackValue::Constant(v as f32 / 32768.0)); + } + } + } else if rot_ptr && value_base + 6 <= data.len() { + for j in 0..3 { + let off = i16::from_le_bytes([ + data[value_base + j * 2], + data[value_base + j * 2 + 1], + ]); + if off > 0 { + channels[j] = Some(TrackValue::Encoded(value_base + off as usize)); + } + } + } + let pos_base = value_base + if rot_ptr { 6 } else { 0 }; + if pos_ptr && pos_base + 6 <= data.len() { + for j in 0..3 { + let off = + i16::from_le_bytes([data[pos_base + j * 2], data[pos_base + j * 2 + 1]]); + if off > 0 { + channels[3 + j] = Some(TrackValue::Encoded(pos_base + off as usize)); + } + } + } + out.push(AnimTrack { + bone, + flags, + channels, + }); + if next <= 0 { + break; + } + offset = (offset as i32 + next).max(0) as usize; + } + out + } + + /// Значение канала из `mstudioanimvalue_t` на кадре `frame`. + /// + /// Формат: `valid`, `total`, затем `total` short-значений — дельтами с + /// накоплением; когда `valid < total` и `valid == 0`, остальные повторяют + /// предыдущее накопленное значение. + pub fn decode_track(data: &[u8], start: usize, frame: usize, total_frames: usize) -> f32 { + let mut value = 0.0f32; + let mut offset = start; + let mut current = 0usize; + while offset + 2 <= data.len() && current <= frame && current < total_frames.max(1) { + let valid = data[offset] as usize; + let total = data[offset + 1] as usize; + if total == 0 { + break; + } + offset += 2; + let count = valid.min(total); + for i in 0..total { + if i < count { + if offset + 2 > data.len() { + return value; + } + let delta = i16::from_le_bytes([data[offset], data[offset + 1]]) as f32; + offset += 2; + value += delta; + } + if current == frame { + return value; + } + current += 1; + } + } + value + } + /// Одна секвенция по смещению. `None` — данные не читаются. fn parse_sequence(&self, base: usize) -> Option { let name_offset = i32_at(self.data, base + 4)?; diff --git a/docs/PARITY.md b/docs/PARITY.md index 16d3dc4..3b58b88 100644 --- a/docs/PARITY.md +++ b/docs/PARITY.md @@ -3213,9 +3213,20 @@ GPU» работает целиком. Пока это не анимация: н `models/m_anm.ani` прочитан целиком: **7,1 МБ**. Значит декодер кадров будет брать данные оттуда (`mstudioanimblock_t.datastart + animdesc.anim_index`). -**Дальше:** декодирование `mstudioanim_t` (по 6 каналов на кость, дельты с -накоплением, повороты кватернионом), позы костей с родителями, интерполяция -между кадрами, затем выбор секвенции по состоянию. +**Сделано (декодер кадров, проверен на живых данных).** Спека взята из +`studio.h` (SDK 2013): `mstudioanim_t` — **4 байта** (`bone`, `flags`, +`nextoffset`), дорожки связаны списком до `nextoffset == 0`; `valueptr` — три +смещения каналов; значения `mstudioanimvalue_t` идут дельтами с накоплением +(`valid`, `total`); есть флаги RAWROT (`Quaternion48`), ANIMROT/ANIMPOS +(дорожки), RAWROT2. + +`Mdl::animation_tracks` и `Mdl::decode_track` написаны и прогнаны на настоящей +библиотеке GMod: анимация `@idle_all_01` — **280 кадров, 51 дорожка**, значения +вида 17013, 1424, −874 (шорты, которые делятся на масштаб кости при сборке +кватерниона). + +**Дальше:** масштаб кости (`rot_scale`/`pos_scale`) в сборке кватерниона, позы +костей с родителями, интерполяция между кадрами, выбор секвенции по состоянию. **Порядок работ:** (а) секвенции, включённые модели и чтение `.ani` сделаны; (б) привязка вершин сделана; (в) CPU-скиннинг сделан; (г) декодер кадров и