A26 шаг б: кости и веса вершин в модели
Some checks are pending
CI / checks (push) Waiting to run

Model теперь несёт skins: Vec<VertexSkin> — привязку вершин к костям из VVD
(до трёх костей с весами и их число), по одной записи на вершину. Индексы
глобальные (после fixup VTX), считаться по ним будет матрица скиннинга вместе
со скелетом Model::bones.

Замер на живых моделях (лог «модель загружена»): companion_doll 1471 вершин /
1471 привязок, стол 136/136, диван 556/556 — привязка есть у всех вершин; у
пропов при этом один корневой узел (bones=1). Модель игрока police.mdl — 57
костей.

Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён.
This commit is contained in:
Niko Marmeladkov 2026-10-17 11:19:21 +03:00
parent d7796974b1
commit f8db4cf73c
2 changed files with 48 additions and 3 deletions

View file

@ -26,6 +26,17 @@ pub enum ModelError {
Geometry(String),
}
/// Привязка вершины к костям (до трёх, как `mstudiovertex_t`).
///
/// Индексы костей — **глобальные** (после fixup VTX), по ним считается
/// матрица скиннинга вместе со скелетом из `Model::bones` (A26).
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct VertexSkin {
pub bones: [u8; 3],
pub weights: [f32; 3],
pub num_bones: u8,
}
/// Вершина модели.
#[derive(Debug, Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
@ -58,6 +69,8 @@ pub struct Model {
pub sequences: Vec<formats::mdl::Sequence>,
/// Кости скелета.
pub bones: Vec<formats::mdl::Bone>,
/// Привязка вершин к костям, по одной на `vertices`.
pub skins: Vec<VertexSkin>,
}
/// Загружает модель по пути `models/...mdl`.
@ -98,12 +111,26 @@ pub fn load_model(vfs: &VirtualFs, name: &str, budget: &mut usize) -> Result<Mod
name: name.clone(),
sequences: Vec::new(),
bones: Vec::new(),
skins: Vec::new(),
};
// Скелет и секвенции: их читает `formats`, движку они нужны для
// скиннинга (A26). Отсутствие данных не ошибка: часть моделей их не имеет.
model.bones = mdl.bones().unwrap_or_default();
model.sequences = mdl.sequences().unwrap_or_default();
if !model.skins.is_empty() {
let skinned = model
.skins
.iter()
.filter(|s| s.num_bones > 0 && s.weights.iter().any(|w| *w > 0.0))
.count();
tracing::info!(
name = %name,
vertices = model.skins.len(),
skinned,
"модель: привязка вершин к костям"
);
}
if !model.sequences.is_empty() {
let sample: Vec<&str> = model
.sequences
@ -205,6 +232,11 @@ pub fn load_model(vfs: &VirtualFs, name: &str, budget: &mut usize) -> Result<Mod
normal: source.normal,
uv: source.texcoord,
});
model.skins.push(VertexSkin {
bones: source.bones,
weights: source.weights,
num_bones: source.num_bones,
});
let pos = Vec3::new(source.position[0], source.position[1], source.position[2]);
model.bounds_min = model.bounds_min.min(pos);
model.bounds_max = model.bounds_max.max(pos);
@ -238,6 +270,14 @@ pub fn load_model(vfs: &VirtualFs, name: &str, budget: &mut usize) -> Result<Mod
model.bounds_min = Vec3::splat(-16.0);
model.bounds_max = Vec3::splat(16.0);
}
tracing::info!(
name = %name,
vertices = model.vertices.len(),
skins = model.skins.len(),
bones = model.bones.len(),
sequences = model.sequences.len(),
"модель загружена"
);
Ok(model)
}

View file

@ -3177,9 +3177,14 @@ Sandbox.
моделях** (`police.mdl` включает общую модель игрока), и их надо сливать по
`mdl.include_models()`.
**Порядок работ:** (а) секвенции и `.ani` — секвенции сделаны, `.ani` и
включённые модели дальше; (б) `ModelVertex` с костями/весами; (в) CPU-скиннинг в
движке и заливка на GPU; (г) проигрывание одной секвенции с циклом; (д) выбор по
**Сделано (шаг б).** `Model` несёт `skins: Vec<VertexSkin>` — привязку вершин
к костям из VVD (`bones[3]`, `weights[3]`, `num_bones`), по одной на вершину.
Замер на живых моделях: `companion_doll` 1471/1471, стол 136/136, диван 556/556 —
привязка есть у всех вершин; у пропов при этом один «корень» (bones=1).
**Порядок работ:** (а) секвенции сделаны; дальше `.ani` (кадры) и слияние
включённых моделей; (б) привязка вершин сделана; (в) CPU-скиннинг в движке и
заливка на GPU; (г) проигрывание одной секвенции с циклом; (д) выбор по
состоянию и блендинг.
**Приёмка:** модель игрока/оружия двигается в кадре, ходьба переходит в бег и