A26: чтение анимационного файла .ani (кадры)
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
formats: mdl.animations() — описания анимаций (mstudioanimdesc_t) с определением размера записи по данным (как у секвенций); anim_block_names() — имена анимационных файлов; anim_blocks() — границы блоков внутри .ani. Замеры: у m_anm.mdl 1563 анимации (@reference 101 кадр, @idle_all_01 280, @idle_all_02 151, все 30 fps), 247 блоков, и ВСЕ имеют anim_block > 0 — то есть кадры не в MDL, а в отдельном файле. Файл models/m_anm.ani прочитан целиком: 7,1 МБ. В конце таблицы имён блоков есть пустая запись — чтение по ней падает и пишет предупреждение, это безвредно. Дальше: декодирование mstudioanim_t (6 каналов на кость, дельты с накоплением, повороты кватернионом), позы костей с учётом родителей, интерполяция между кадрами и выбор секвенции по состоянию. Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён.
This commit is contained in:
parent
3c0bef5169
commit
2c7ec501e2
3 changed files with 156 additions and 3 deletions
|
|
@ -140,6 +140,44 @@ pub fn load_model(vfs: &VirtualFs, name: &str, budget: &mut usize) -> Result<Mod
|
|||
continue;
|
||||
};
|
||||
let sequences = included.sequences().unwrap_or_default();
|
||||
if let Ok(anims) = included.animations() {
|
||||
let block_names = included.anim_block_names();
|
||||
let blocks = included.anim_blocks();
|
||||
// Кадры GMod-моделей лежат в отдельном `.ani` — читаем его
|
||||
// целиком: он нужен для поз костей (A26, следующий шаг).
|
||||
let mut ani_bytes = 0usize;
|
||||
for block_name in &block_names {
|
||||
let ani_path = if block_name.starts_with("models/") {
|
||||
block_name.clone()
|
||||
} else {
|
||||
format!("models/{block_name}")
|
||||
};
|
||||
match vfs.read(&ani_path) {
|
||||
Ok(data) => {
|
||||
ani_bytes += data.len();
|
||||
tracing::info!(
|
||||
include = %path,
|
||||
ani = %ani_path,
|
||||
bytes = data.len(),
|
||||
"анимационный файл прочитан"
|
||||
);
|
||||
}
|
||||
Err(err) => tracing::warn!(
|
||||
include = %path,
|
||||
ani = %ani_path,
|
||||
"анимационный файл не прочитан: {err}"
|
||||
),
|
||||
}
|
||||
}
|
||||
tracing::info!(
|
||||
include = %path,
|
||||
animations = anims.len(),
|
||||
block_names = ?block_names,
|
||||
blocks = blocks.len(),
|
||||
ani_bytes,
|
||||
"анимации включённой модели"
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
include = %path,
|
||||
numlocalseq = included.header.numlocalseq,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,22 @@ pub enum MdlError {
|
|||
Kv(#[from] crate::kv::KvError),
|
||||
}
|
||||
|
||||
/// Описание анимации (`mstudioanimdesc_t`).
|
||||
///
|
||||
/// Кадры лежат либо в самом MDL (`anim_block == 0`), либо в отдельном `.ani`,
|
||||
/// имя которого лежит в анимационных блоках модели (у GMod-моделей второе).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AnimDesc {
|
||||
pub name: String,
|
||||
pub fps: f32,
|
||||
pub flags: i32,
|
||||
pub num_frames: i32,
|
||||
pub anim_block: i32,
|
||||
pub anim_index: i32,
|
||||
pub num_movements: i32,
|
||||
pub movement_index: i32,
|
||||
}
|
||||
|
||||
/// Секвенция анимации (`mstudioseqdesc_t`).
|
||||
///
|
||||
/// Читаются поля, нужные для первого среза: имя (для выбора по состоянию),
|
||||
|
|
@ -501,6 +517,90 @@ impl<'a> Mdl<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Имена анимационных блоков — таблица строк через NUL.
|
||||
///
|
||||
/// У GMod-моделей кадры лежат не в MDL, а в отдельном `.ani`; какой именно —
|
||||
/// написано здесь (`szanimblocknameindex`).
|
||||
pub fn anim_block_names(&self) -> Vec<String> {
|
||||
let h = &self.header;
|
||||
let mut out = Vec::new();
|
||||
let mut offset = h.szanimblocknameindex;
|
||||
if offset <= 0 || offset as usize >= self.data.len() {
|
||||
return out;
|
||||
}
|
||||
while (offset as usize) < self.data.len() && out.len() < 16 {
|
||||
let start = offset as usize;
|
||||
let Some(rest) = self.data.get(start..) else {
|
||||
break;
|
||||
};
|
||||
let end = rest.iter().position(|b| *b == 0).unwrap_or(rest.len());
|
||||
let name = String::from_utf8_lossy(&rest[..end]).to_string();
|
||||
if name.is_empty() {
|
||||
break;
|
||||
}
|
||||
out.push(name);
|
||||
offset = (start + end + 1) as i32;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Блоки анимационного файла: границы данных для `anim_block` анимации.
|
||||
pub fn anim_blocks(&self) -> Vec<(i32, i32)> {
|
||||
let h = &self.header;
|
||||
let mut out = Vec::with_capacity(h.numanimblocks.max(0) as usize);
|
||||
for i in 0..h.numanimblocks.max(0) as usize {
|
||||
let base = h.animblockindex.max(0) as usize + i * 8;
|
||||
out.push((
|
||||
i32_at(self.data, base).unwrap_or(0),
|
||||
i32_at(self.data, base + 4).unwrap_or(0),
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Описания анимаций (`mstudioanimdesc_t`).
|
||||
///
|
||||
/// Как и у секвенций, размер записи версионно-зависим (96 у v48, 100 у
|
||||
/// v49), поэтому шаг **определяется по данным**: принимается тот, при
|
||||
/// котором имя читается у всех анимаций.
|
||||
pub fn animations(&self) -> Result<Vec<AnimDesc>, MdlError> {
|
||||
let h = &self.header;
|
||||
let count = h.numlocalanim.max(0) as usize;
|
||||
if count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let start = h.localanimindex.max(0) as usize;
|
||||
for stride in (88..=120).step_by(4) {
|
||||
let mut out = Vec::with_capacity(count);
|
||||
let mut ok = true;
|
||||
for i in 0..count {
|
||||
let base = start + i * stride;
|
||||
let Some(name_offset) = i32_at(self.data, base + 4) else {
|
||||
ok = false;
|
||||
break;
|
||||
};
|
||||
let Some(name) = self.string_at(base, name_offset, "anim.name") else {
|
||||
ok = false;
|
||||
break;
|
||||
};
|
||||
out.push(AnimDesc {
|
||||
name,
|
||||
fps: f32_at(self.data, base + 8),
|
||||
flags: i32_at(self.data, base + 12).unwrap_or(0),
|
||||
num_frames: i32_at(self.data, base + 16).unwrap_or(0),
|
||||
num_movements: i32_at(self.data, base + 20).unwrap_or(0),
|
||||
movement_index: i32_at(self.data, base + 24).unwrap_or(0),
|
||||
anim_block: i32_at(self.data, base + 52).unwrap_or(0),
|
||||
anim_index: i32_at(self.data, base + 56).unwrap_or(0),
|
||||
});
|
||||
}
|
||||
if ok && out.len() == count {
|
||||
return Ok(out);
|
||||
}
|
||||
}
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Одна секвенция по смещению. `None` — данные не читаются.
|
||||
fn parse_sequence(&self, base: usize) -> Option<Sequence> {
|
||||
let name_offset = i32_at(self.data, base + 4)?;
|
||||
|
|
|
|||
|
|
@ -3202,9 +3202,24 @@ GPU» работает целиком. Пока это не анимация: н
|
|||
176…208 байт, поэтому перебор шага расширен до 176…320 с обязательной проверкой
|
||||
имён ВСЕХ секвенций — иначе разбор молча возвращал ноль.
|
||||
|
||||
**Порядок работ:** (а) секвенции и включённые модели сделаны; дальше `.ani`
|
||||
(кадры); (б) привязка вершин сделана; (в) CPU-скиннинг сделан; (г) проигрывание
|
||||
одной секвенции с циклом; (д) выбор по состоянию и блендинг.
|
||||
**Сделано (кадры из `.ani`, подготовка).** `mdl.animations()` — описания
|
||||
анимаций (`mstudioanimdesc_t`), как и секвенции, с определением размера записи по
|
||||
данным; `anim_block_names()` — имена анимационных файлов; `anim_blocks()` —
|
||||
границы блоков внутри `.ani`.
|
||||
|
||||
**Замеры:** у `m_anm.mdl` — **1563 анимации** (`@reference` 101 кадр,
|
||||
`@idle_all_01` 280, `@idle_all_02` 151, все 30 fps), 247 блоков, и **все** с
|
||||
`anim_block > 0`, то есть кадры не в MDL, а в отдельном файле. Файл
|
||||
`models/m_anm.ani` прочитан целиком: **7,1 МБ**. Значит декодер кадров будет
|
||||
брать данные оттуда (`mstudioanimblock_t.datastart + animdesc.anim_index`).
|
||||
|
||||
**Дальше:** декодирование `mstudioanim_t` (по 6 каналов на кость, дельты с
|
||||
накоплением, повороты кватернионом), позы костей с родителями, интерполяция
|
||||
между кадрами, затем выбор секвенции по состоянию.
|
||||
|
||||
**Порядок работ:** (а) секвенции, включённые модели и чтение `.ani` сделаны;
|
||||
(б) привязка вершин сделана; (в) CPU-скиннинг сделан; (г) декодер кадров и
|
||||
проигрывание секвенции с циклом; (д) выбор по состоянию и блендинг.
|
||||
|
||||
**Приёмка:** модель игрока/оружия двигается в кадре, ходьба переходит в бег и
|
||||
присед по скорости, прыжок показывается отдельной секвенцией.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue