rpfm_lib/files/rigidmodel/mod.rs
1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! RigidModel file format support for Total War games.
12//!
13//! # Overview
14//!
15//! RigidModel files (`.rigid_model_v2`) are the primary 3D model format used by Total War games
16//! to store mesh geometry, materials, skeletal animation data, and visual effects. These files
17//! contain everything needed to render game assets including characters, buildings, terrain,
18//! vegetation, and props.
19//!
20//! # File Structure
21//!
22//! A RigidModel file contains:
23//!
24//! 1. **Header**: File signature (`RMV2`), version number, skeleton ID
25//! 2. **LOD (Level of Detail) structures**: Multiple quality levels for distance-based rendering
26//! 3. **Mesh blocks**: Individual mesh units, each with geometry and material data
27//! 4. **Vertex data**: 3D positions, normals, UVs, bone weights (often compressed)
28//! 5. **Material definitions**: Textures, shaders, attachment points, rendering parameters
29//!
30//! # Supported Versions
31//!
32//! | Version | Support | Notes |
33//! |---------|---------|-----------------------|
34//! | 6 | ✅ Full | Older format |
35//! | 7 | ✅ Full | Intermediate format |
36//! | 8 | ✅ Full | Current/newest format |
37//!
38//! # Material System
39//!
40//! RigidModels support 40+ material types for different rendering needs:
41//! - **Standard rendering**: DefaultMaterial, Decal, Tree, Grass, Water
42//! - **Skeletal animation**: WeightedSkin, WeightedCloth
43//! - **Terrain**: RsTerrain, WeightedTextureBlend, TiledDirtmap
44//! - **Special effects**: Collision, DebugGeometry, PointLight
45//!
46//! Each material type determines:
47//! - Which vertex format is used (affects vertex compression and available data)
48//! - What textures and parameters are stored
49//! - How the material is rendered in-game
50//!
51//! # Vertex Formats
52//!
53//! Different vertex formats optimize storage for specific use cases:
54//! - **Static (0)**: Standard geometry without animation
55//! - **Weighted (3)**: Skeletal animation with bone indices and weights
56//! - **Cinematic (4)**: High-quality vertices for cutscenes (supports 4 bones)
57//! - **Grass (5)**: Vegetation-specific format
58//! - **ClothSim (25)**: Cloth physics simulation vertices
59//! - **Collision (1)**: Simplified collision mesh vertices
60//!
61//! Vertices use various compression techniques:
62//! - Half-precision floats (f16) for positions and UVs
63//! - Normalized u8 vectors for normals, tangents, bitangents
64//! - Percentage encoding for bone weights
65//!
66//! # Usage
67//!
68//! ```ignore
69//! use rpfm_lib::files::rigidmodel::RigidModel;
70//! use rpfm_lib::files::{Decodeable, Encodeable};
71//!
72//! // Decode a RigidModel file
73//! let model = RigidModel::decode(&mut reader, &None)?;
74//!
75//! // Access LODs and meshes
76//! for lod in model.lods() {
77//! println!("LOD distance: {}", lod.visibility_distance());
78//! for mesh_block in lod.mesh_blocks() {
79//! println!("Mesh: {}", mesh_block.mesh().name());
80//! println!("Vertices: {}", mesh_block.mesh().vertices().len());
81//! }
82//! }
83//!
84//! // Encode back to bytes
85//! model.encode(&mut writer, &None)?;
86//! ```
87//!
88//! # Credits
89//!
90//! Most of the reverse-engineering work for this module was done by Victimized, Phazer, and Ole.
91//! Their research enabled the implementation of this format decoder/encoder.
92
93use getset::*;
94use nalgebra::Vector3;
95use serde_derive::{Serialize, Deserialize};
96
97use std::io::Write;
98
99use crate::binary::{ReadBytes, WriteBytes};
100use crate::error::{Result, RLibError};
101use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable};
102use crate::utils::check_size_mismatch;
103
104use self::materials::{Material, MaterialType};
105use self::vertices::Vertex;
106
107/// Magic bytes identifying a RigidModel file (`RMV2`).
108const SIGNATURE: &[u8; 4] = b"RMV2";
109
110/// File extension for RigidModel files.
111pub const EXTENSION: &str = ".rigid_model_v2";
112
113// String field sizes (null-padded to fixed lengths in binary format)
114const PADDED_SIZE_32: usize = 32; // Small strings (mesh names, etc.)
115const PADDED_SIZE_64: usize = 64; // Medium strings
116const PADDED_SIZE_128: usize = 128; // Large strings (skeleton IDs, texture paths)
117const PADDED_SIZE_256: usize = 256; // Extra-large strings
118
119/// Base header size in bytes (signature + version + skeleton_id + lod count fields).
120const HEADER_LENGTH: u32 = 140;
121
122pub mod materials;
123mod versions;
124pub mod vertices;
125
126#[cfg(test)] mod test_rigidmodel;
127
128//---------------------------------------------------------------------------//
129// Enum & Structs
130//---------------------------------------------------------------------------//
131
132/// Root structure representing a complete RigidModel file.
133///
134/// Contains the file version, associated skeleton, and one or more LOD levels.
135/// The skeleton ID links this model to animation data for skeletal meshes.
136#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
137#[getset(get = "pub", get_mut = "pub", set = "pub")]
138pub struct RigidModel {
139 /// File format version (6, 7, or 8).
140 version: u32,
141
142 /// Unknown field, purpose unclear.
143 uk_1: u16,
144
145 /// Skeleton identifier for skeletal animation (e.g., "humanoid01").
146 /// Empty string for static models without animation.
147 skeleton_id: String,
148
149 /// Level of Detail structures, ordered from highest to lowest quality.
150 /// Typically contains 1-4 LODs for distance-based rendering optimization.
151 lods: Vec<Lod>,
152}
153
154/// Level of Detail structure containing meshes at a specific quality level.
155///
156/// LODs allow the game engine to render lower-poly versions of models at greater
157/// distances, improving performance. Each LOD contains one or more mesh blocks.
158#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
159#[getset(get = "pub", get_mut = "pub", set = "pub")]
160pub struct Lod {
161 /// Distance in game units at which this LOD becomes visible.
162 /// Lower distances = higher detail. Typically: LOD0 = 0.0, LOD1 = 75.0, LOD2 = 150.0, etc.
163 visibility_distance: f32,
164
165 /// Authored LOD index (0 = highest quality, 1 = medium, 2 = low, etc.).
166 authored_lod_number: u32,
167
168 /// Quality level indicator (purpose not fully documented).
169 quality_level: u32,
170
171 /// Individual mesh blocks that make up this LOD.
172 /// Each block has its own geometry and material.
173 mesh_blocks: Vec<MeshBlock>,
174}
175
176/// A single mesh unit with geometry and material data.
177///
178/// Mesh blocks are the fundamental rendering unit. Each block contains vertices,
179/// indices for triangle construction, and a material defining how it's rendered.
180#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
181#[getset(get = "pub", get_mut = "pub", set = "pub")]
182pub struct MeshBlock {
183 /// Mesh geometry data (vertices, indices, bounding box).
184 mesh: Mesh,
185
186 /// Material definition (textures, shaders, rendering parameters).
187 material: Material,
188}
189
190/// Mesh geometry container with vertices, indices, and metadata.
191///
192/// Contains all geometric data needed to render a mesh: vertex positions/normals/UVs,
193/// triangle indices, bounding box for culling, and shader parameters.
194#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
195#[getset(get = "pub", get_mut = "pub", set = "pub")]
196pub struct Mesh {
197 /// Human-readable mesh name (e.g., "head_mesh", "sword_blade").
198 name: String,
199
200 /// Material type determines vertex format and material data structure.
201 material_type: MaterialType,
202
203 /// Raw shader parameter data (format not fully documented).
204 shader_params: ShaderParams,
205
206 /// Axis-aligned bounding box minimum corner (x, y, z).
207 min_bb: Vector3<f32>,
208
209 /// Axis-aligned bounding box maximum corner (x, y, z).
210 max_bb: Vector3<f32>,
211
212 /// Lighting configuration string (format not fully documented).
213 lighting_constants: String,
214
215 /// Vertex data array. Format depends on `material_type` and vertex format.
216 /// See [`Vertex`] for field details.
217 vertices: Vec<Vertex>,
218
219 /// Index buffer defining triangles (groups of 3 indices into `vertices`).
220 indices: Vec<u16>,
221}
222
223/// Raw shader parameter data (not fully decoded).
224///
225/// Contains binary shader configuration data. The structure of this data
226/// is not fully reverse-engineered and is stored as raw bytes.
227#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
228#[getset(get = "pub", get_mut = "pub", set = "pub")]
229pub struct ShaderParams {
230 /// Raw shader parameter bytes.
231 data: Vec<u8>,
232}
233
234//---------------------------------------------------------------------------//
235// Implementations
236//---------------------------------------------------------------------------//
237
238impl Decodeable for RigidModel {
239
240 fn decode<R: ReadBytes>(data: &mut R, _extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
241 let signature_bytes = data.read_slice(4, false)?;
242 if signature_bytes.as_slice() != SIGNATURE {
243 return Err(RLibError::DecodingRigidModelUnsupportedSignature(signature_bytes));
244 }
245
246 let mut rigid = Self::default();
247 rigid.version = data.read_u32()?;
248
249 match rigid.version {
250 8 => rigid.read_v8(data)?,
251 7 => rigid.read_v7(data)?,
252 6 => rigid.read_v6(data)?,
253 _ => Err(RLibError::DecodingRigidModelUnsupportedVersion(rigid.version))?,
254 }
255
256 // Trigger an error if there's left data on the source.
257 check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
258
259 Ok(rigid)
260 }
261}
262
263impl Encodeable for RigidModel {
264
265 fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
266 buffer.write_all(SIGNATURE)?;
267 buffer.write_u32(self.version)?;
268
269 match self.version {
270 8 => self.write_v8(buffer)?,
271 7 => self.write_v7(buffer)?,
272 6 => self.write_v6(buffer)?,
273 _ => Err(RLibError::DecodingRigidModelUnsupportedVersion(self.version))?,
274 }
275
276 Ok(())
277 }
278}