A26: декодер кадров анимации (mstudioanim_t), проверен на данных GMod
Some checks are pending
CI / checks (push) Waiting to run
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:
parent
2c7ec501e2
commit
2155bcee54
2 changed files with 130 additions and 3 deletions
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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-скиннинг сделан; (г) декодер кадров и
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue