- VVD (IDSV v4): заголовок 64 байта, mstudiovertex_t 48 байт (веса/кости/позиция/нормаль/UV), тангенты, таблица fixup - VTX (packed): BodyPart→Model→LOD→Mesh→StripGroup→Strip, валидация индексов и смещений; смещения вложенных структур относительны своей структуры - fs inspect: для .mdl показывает VVD/VTX-статистику и сверку checksum - тесты: oildrum (270 вершин/756 индексов), 51 пара VVD/VTX с совпадающим checksum - docs: прогресс M1 (остался ANIM, PCF/PHY/SND, GMA/mount.cfg)
344 lines
12 KiB
Rust
344 lines
12 KiB
Rust
//! VVD — вершинные данные модели (`IDSV`, версия 4).
|
||
//!
|
||
//! Заголовок 56 байт, затем вершинный блок (mstudiovertex_t = 48 байт),
|
||
//! блок тангентов (Vector4D = 16 байт) и таблица fixup'ов (12 байт на запись).
|
||
|
||
use std::fmt;
|
||
|
||
/// Сигнатура `IDSV`.
|
||
pub const VVD_MAGIC: u32 = u32::from_le_bytes(*b"IDSV");
|
||
/// Ожидаемая версия.
|
||
pub const VVD_VERSION: i32 = 4;
|
||
/// Размер заголовка.
|
||
pub const HEADER_SIZE: usize = 64;
|
||
/// Размер mstudiovertex_t.
|
||
pub const VERTEX_SIZE: usize = 48;
|
||
/// Размер Vector4D (тангент).
|
||
pub const TANGENT_SIZE: usize = 16;
|
||
/// Размер vertexFileFixup_t.
|
||
pub const FIXUP_SIZE: usize = 12;
|
||
/// Максимум LOD-ов в заголовке.
|
||
pub const MAX_LODS: usize = 8;
|
||
/// Максимальное число вершин/фиксапов (защита от мусора).
|
||
const MAX_VERTICES: usize = 1 << 24;
|
||
const MAX_FIXUPS: usize = 1 << 20;
|
||
|
||
/// Ошибка разбора VVD.
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum VvdError {
|
||
#[error("файл короче заголовка VVD")]
|
||
Truncated,
|
||
#[error("не VVD: магия 0x{found:08x}")]
|
||
BadMagic { found: u32 },
|
||
#[error("неподдерживаемая версия VVD: {0}")]
|
||
UnsupportedVersion(i32),
|
||
#[error("диапазон {field} выходит за пределы файла: {offset}+{size} > {file_len}")]
|
||
OutOfBounds {
|
||
field: &'static str,
|
||
offset: usize,
|
||
size: usize,
|
||
file_len: usize,
|
||
},
|
||
#[error("слишком много элементов: {field} = {count}")]
|
||
TooMany { field: &'static str, count: usize },
|
||
#[error("LOD вне диапазона: {0}")]
|
||
BadLod(usize),
|
||
}
|
||
|
||
/// Заголовок VVD.
|
||
#[derive(Debug, Clone)]
|
||
pub struct VvdHeader {
|
||
pub version: i32,
|
||
pub checksum: i32,
|
||
pub num_lods: i32,
|
||
pub num_lod_vertexes: [i32; MAX_LODS],
|
||
pub num_fixups: i32,
|
||
pub fixup_table_start: i32,
|
||
pub vertex_data_start: i32,
|
||
pub tangent_data_start: i32,
|
||
}
|
||
|
||
/// Вершина (mstudiovertex_t).
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub struct Vertex {
|
||
pub weights: [f32; 3],
|
||
pub bones: [u8; 3],
|
||
pub num_bones: u8,
|
||
pub position: [f32; 3],
|
||
pub normal: [f32; 3],
|
||
pub texcoord: [f32; 2],
|
||
}
|
||
|
||
/// Fixup: диапазон вершин из LOD-а в общем вершинном буфере.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct Fixup {
|
||
pub lod: i32,
|
||
pub source_vertex_id: i32,
|
||
pub num_vertexes: i32,
|
||
}
|
||
|
||
/// Разобранный VVD.
|
||
pub struct Vvd<'a> {
|
||
data: &'a [u8],
|
||
pub header: VvdHeader,
|
||
}
|
||
|
||
impl<'a> Vvd<'a> {
|
||
/// Разбирает VVD из памяти.
|
||
pub fn parse(data: &'a [u8]) -> Result<Vvd<'a>, VvdError> {
|
||
if data.len() < HEADER_SIZE {
|
||
return Err(VvdError::Truncated);
|
||
}
|
||
let magic = u32_at(data, 0).unwrap_or(0);
|
||
if magic != VVD_MAGIC {
|
||
return Err(VvdError::BadMagic { found: magic });
|
||
}
|
||
let version = i32_at(data, 4).unwrap_or(0);
|
||
if version != VVD_VERSION {
|
||
return Err(VvdError::UnsupportedVersion(version));
|
||
}
|
||
|
||
let mut num_lod_vertexes = [0i32; MAX_LODS];
|
||
for (i, slot) in num_lod_vertexes.iter_mut().enumerate() {
|
||
*slot = i32_at(data, 16 + i * 4).unwrap_or(0);
|
||
}
|
||
|
||
let header = VvdHeader {
|
||
version,
|
||
checksum: i32_at(data, 8).unwrap_or(0),
|
||
num_lods: i32_at(data, 12).unwrap_or(0),
|
||
num_lod_vertexes,
|
||
num_fixups: i32_at(data, 48).unwrap_or(0),
|
||
fixup_table_start: i32_at(data, 52).unwrap_or(0),
|
||
vertex_data_start: i32_at(data, 56).unwrap_or(0),
|
||
tangent_data_start: i32_at(data, 60).unwrap_or(0),
|
||
};
|
||
|
||
let vvd = Vvd { data, header };
|
||
vvd.check_ranges()?;
|
||
Ok(vvd)
|
||
}
|
||
|
||
fn check_ranges(&self) -> Result<(), VvdError> {
|
||
let file_len = self.data.len();
|
||
let h = &self.header;
|
||
|
||
let num_lods = h.num_lods.clamp(0, MAX_LODS as i32) as usize;
|
||
for (lod, &count) in h.num_lod_vertexes.iter().take(num_lods).enumerate() {
|
||
if count < 0 {
|
||
return Err(VvdError::BadLod(lod));
|
||
}
|
||
let count = count as usize;
|
||
if count > MAX_VERTICES {
|
||
return Err(VvdError::TooMany {
|
||
field: "vertices",
|
||
count,
|
||
});
|
||
}
|
||
}
|
||
|
||
// Вершинный блок покрывает сумму вершин по LOD-ам (или максимальный LOD).
|
||
let total_vertexes = h.num_lod_vertexes[0].max(0) as usize;
|
||
let vertex_offset = h.vertex_data_start.max(0) as usize;
|
||
check(
|
||
file_len,
|
||
"vertex_data",
|
||
vertex_offset,
|
||
total_vertexes * VERTEX_SIZE,
|
||
)?;
|
||
|
||
if h.tangent_data_start > 0 {
|
||
let tangent_offset = h.tangent_data_start as usize;
|
||
check(
|
||
file_len,
|
||
"tangent_data",
|
||
tangent_offset,
|
||
total_vertexes * TANGENT_SIZE,
|
||
)?;
|
||
}
|
||
|
||
if h.num_fixups > 0 {
|
||
let count = h.num_fixups as usize;
|
||
if count > MAX_FIXUPS {
|
||
return Err(VvdError::TooMany {
|
||
field: "fixups",
|
||
count,
|
||
});
|
||
}
|
||
check(
|
||
file_len,
|
||
"fixup_table",
|
||
h.fixup_table_start.max(0) as usize,
|
||
count * FIXUP_SIZE,
|
||
)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Вершины LOD 0.
|
||
pub fn vertices(&self, lod: usize) -> Result<Vec<Vertex>, VvdError> {
|
||
if lod >= MAX_LODS || lod >= self.header.num_lods.max(0) as usize {
|
||
return Err(VvdError::BadLod(lod));
|
||
}
|
||
let count = self.header.num_lod_vertexes[lod].max(0) as usize;
|
||
let base = self.header.vertex_data_start.max(0) as usize;
|
||
let mut out = Vec::with_capacity(count);
|
||
for i in 0..count {
|
||
let o = base + i * VERTEX_SIZE;
|
||
let Some(chunk) = self.data.get(o..o + VERTEX_SIZE) else {
|
||
return Err(VvdError::OutOfBounds {
|
||
field: "vertex",
|
||
offset: o,
|
||
size: VERTEX_SIZE,
|
||
file_len: self.data.len(),
|
||
});
|
||
};
|
||
out.push(Vertex {
|
||
weights: [f32_at(chunk, 0), f32_at(chunk, 4), f32_at(chunk, 8)],
|
||
bones: [chunk[12], chunk[13], chunk[14]],
|
||
num_bones: chunk[15],
|
||
position: [f32_at(chunk, 16), f32_at(chunk, 20), f32_at(chunk, 24)],
|
||
normal: [f32_at(chunk, 28), f32_at(chunk, 32), f32_at(chunk, 36)],
|
||
texcoord: [f32_at(chunk, 40), f32_at(chunk, 44)],
|
||
});
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
/// Тангенсы LOD 0 (Vector4D), если блок есть.
|
||
pub fn tangents(&self, lod: usize) -> Result<Option<Vec<[f32; 4]>>, VvdError> {
|
||
if self.header.tangent_data_start <= 0 {
|
||
return Ok(None);
|
||
}
|
||
if lod >= MAX_LODS || lod >= self.header.num_lods.max(0) as usize {
|
||
return Err(VvdError::BadLod(lod));
|
||
}
|
||
let count = self.header.num_lod_vertexes[lod].max(0) as usize;
|
||
let base = self.header.tangent_data_start as usize;
|
||
let mut out = Vec::with_capacity(count);
|
||
for i in 0..count {
|
||
let o = base + i * TANGENT_SIZE;
|
||
let Some(chunk) = self.data.get(o..o + TANGENT_SIZE) else {
|
||
return Err(VvdError::OutOfBounds {
|
||
field: "tangent",
|
||
offset: o,
|
||
size: TANGENT_SIZE,
|
||
file_len: self.data.len(),
|
||
});
|
||
};
|
||
out.push([
|
||
f32_at(chunk, 0),
|
||
f32_at(chunk, 4),
|
||
f32_at(chunk, 8),
|
||
f32_at(chunk, 12),
|
||
]);
|
||
}
|
||
Ok(Some(out))
|
||
}
|
||
|
||
/// Таблица fixup'ов.
|
||
pub fn fixups(&self) -> Result<Vec<Fixup>, VvdError> {
|
||
let count = self.header.num_fixups.max(0) as usize;
|
||
let base = self.header.fixup_table_start.max(0) as usize;
|
||
let mut out = Vec::with_capacity(count.min(MAX_FIXUPS));
|
||
for i in 0..count {
|
||
let o = base + i * FIXUP_SIZE;
|
||
let Some(chunk) = self.data.get(o..o + FIXUP_SIZE) else {
|
||
return Err(VvdError::OutOfBounds {
|
||
field: "fixup",
|
||
offset: o,
|
||
size: FIXUP_SIZE,
|
||
file_len: self.data.len(),
|
||
});
|
||
};
|
||
out.push(Fixup {
|
||
lod: i32_at(chunk, 0).unwrap_or(0),
|
||
source_vertex_id: i32_at(chunk, 4).unwrap_or(0),
|
||
num_vertexes: i32_at(chunk, 8).unwrap_or(0),
|
||
});
|
||
}
|
||
Ok(out)
|
||
}
|
||
}
|
||
|
||
impl fmt::Debug for Vvd<'_> {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
f.debug_struct("Vvd").field("header", &self.header).finish()
|
||
}
|
||
}
|
||
|
||
fn check(file_len: usize, field: &'static str, offset: usize, size: usize) -> Result<(), VvdError> {
|
||
if offset.saturating_add(size) > file_len {
|
||
return Err(VvdError::OutOfBounds {
|
||
field,
|
||
offset,
|
||
size,
|
||
file_len,
|
||
});
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn i32_at(data: &[u8], offset: usize) -> Option<i32> {
|
||
let b = data.get(offset..offset + 4)?;
|
||
Some(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
||
}
|
||
|
||
fn u32_at(data: &[u8], offset: usize) -> Option<u32> {
|
||
let b = data.get(offset..offset + 4)?;
|
||
Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
||
}
|
||
|
||
fn f32_at(data: &[u8], offset: usize) -> f32 {
|
||
let Some(b) = data.get(offset..offset + 4) else {
|
||
return 0.0;
|
||
};
|
||
f32::from_le_bytes([b[0], b[1], b[2], b[3]])
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn build_vvd(vertex_count: usize) -> Vec<u8> {
|
||
let mut data = vec![0u8; HEADER_SIZE];
|
||
data[0..4].copy_from_slice(b"IDSV");
|
||
data[4..8].copy_from_slice(&4i32.to_le_bytes());
|
||
data[8..12].copy_from_slice(&0x1234i32.to_le_bytes());
|
||
data[12..16].copy_from_slice(&1i32.to_le_bytes());
|
||
data[16..20].copy_from_slice(&(vertex_count as i32).to_le_bytes());
|
||
data[56..60].copy_from_slice(&(HEADER_SIZE as i32).to_le_bytes());
|
||
for i in 0..vertex_count {
|
||
let mut v = vec![0u8; VERTEX_SIZE];
|
||
v[0..4].copy_from_slice(&1.0f32.to_le_bytes());
|
||
v[15] = 1; // num_bones
|
||
v[16..20].copy_from_slice(&(i as f32).to_le_bytes());
|
||
data.extend_from_slice(&v);
|
||
}
|
||
data
|
||
}
|
||
|
||
#[test]
|
||
fn parse_and_vertices() {
|
||
let data = build_vvd(3);
|
||
let vvd = Vvd::parse(&data).expect("разбор");
|
||
assert_eq!(vvd.header.checksum, 0x1234);
|
||
let verts = vvd.vertices(0).expect("вершины");
|
||
assert_eq!(verts.len(), 3);
|
||
assert_eq!(verts[2].position[0], 2.0);
|
||
assert_eq!(verts[0].num_bones, 1);
|
||
assert!(vvd.tangents(0).expect("тангенты").is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn garbage_never_panics() {
|
||
for len in [0usize, 4, 55, 56, 200, 4096] {
|
||
let mut data = vec![0u8; len];
|
||
if len >= 8 {
|
||
data[0..4].copy_from_slice(b"IDSV");
|
||
data[4..8].copy_from_slice(&4i32.to_le_bytes());
|
||
}
|
||
let _ = Vvd::parse(&data);
|
||
}
|
||
}
|
||
}
|