A26: декодер кадров анимации (mstudioanim_t), проверен на данных GMod
Some checks are pending
CI / checks (push) Waiting to run

Спека взята из 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 обновлён.
This commit is contained in:
Niko Marmeladkov 2026-10-17 11:51:00 +03:00
parent 2c7ec501e2
commit 2155bcee54
2 changed files with 130 additions and 3 deletions

View file

@ -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<TrackValue>; 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<AnimTrack> {
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<Sequence> {
let name_offset = i32_at(self.data, base + 4)?;

View file

@ -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-скиннинг сделан; (г) декодер кадров и