Skip to main content

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
122/// Header length of version 5 files, which store the skeleton name as UTF-16.
123const HEADER_LENGTH_V5: u32 = 268;
124
125/// Header length of version 1 and 3 files, which store a longer skeleton name after the LOD headers.
126const HEADER_LENGTH_V3: u32 = 528;
127
128/// Size in bytes of the skeleton name of version 1 and 3 files.
129const SKELETON_ID_SIZE_V3: usize = 512;
130
131pub mod materials;
132mod versions;
133pub mod vertices;
134
135#[cfg(test)] mod test_rigidmodel;
136
137//---------------------------------------------------------------------------//
138//                              Enum & Structs
139//---------------------------------------------------------------------------//
140
141/// Root structure representing a complete RigidModel file.
142///
143/// Contains the file version, associated skeleton, and one or more LOD levels.
144/// The skeleton ID links this model to animation data for skeletal meshes.
145#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
146#[getset(get = "pub", get_mut = "pub", set = "pub")]
147pub struct RigidModel {
148    /// File format version (6, 7, or 8).
149    version: u32,
150
151    /// Unknown field, purpose unclear.
152    uk_1: u16,
153
154    /// Unknown field after the skeleton identifier of versions 1 and 3.
155    uk_2: u32,
156
157    /// Skeleton identifier for skeletal animation (e.g., "humanoid01").
158    /// Empty string for static models without animation.
159    skeleton_id: String,
160
161    /// Level of Detail structures, ordered from highest to lowest quality.
162    /// Typically contains 1-4 LODs for distance-based rendering optimization.
163    lods: Vec<Lod>,
164}
165
166/// Level of Detail structure containing meshes at a specific quality level.
167///
168/// LODs allow the game engine to render lower-poly versions of models at greater
169/// distances, improving performance. Each LOD contains one or more mesh blocks.
170#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
171#[getset(get = "pub", get_mut = "pub", set = "pub")]
172pub struct Lod {
173    /// Distance in game units at which this LOD becomes visible.
174    /// Lower distances = higher detail. Typically: LOD0 = 0.0, LOD1 = 75.0, LOD2 = 150.0, etc.
175    visibility_distance: f32,
176
177    /// Authored LOD index (0 = highest quality, 1 = medium, 2 = low, etc.).
178    authored_lod_number: u32,
179
180    /// Quality level indicator (purpose not fully documented).
181    quality_level: u32,
182
183    /// If the header of this LOD leaves the sizes of its vertex and index data at 0, as some version 6 files do.
184    /// Those sizes are written as 0 then, instead of the real ones.
185    unset_data_sizes: bool,
186
187    /// Individual mesh blocks that make up this LOD.
188    /// Each block has its own geometry and material.
189    mesh_blocks: Vec<MeshBlock>,
190}
191
192/// A single mesh unit with geometry and material data.
193///
194/// Mesh blocks are the fundamental rendering unit. Each block contains vertices,
195/// indices for triangle construction, and a material defining how it's rendered.
196#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
197#[getset(get = "pub", get_mut = "pub", set = "pub")]
198pub struct MeshBlock {
199    /// Mesh geometry data (vertices, indices, bounding box).
200    mesh: Mesh,
201
202    /// Material definition (textures, shaders, rendering parameters).
203    material: Material,
204}
205
206/// Mesh geometry container with vertices, indices, and metadata.
207///
208/// Contains all geometric data needed to render a mesh: vertex positions/normals/UVs,
209/// triangle indices, bounding box for culling, and shader parameters.
210#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
211#[getset(get = "pub", get_mut = "pub", set = "pub")]
212pub struct Mesh {
213    /// Human-readable mesh name (e.g., "head_mesh", "sword_blade").
214    name: String,
215
216    /// Material type determines vertex format and material data structure.
217    material_type: MaterialType,
218
219    /// Raw shader parameter data (format not fully documented).
220    shader_params: ShaderParams,
221
222    /// Axis-aligned bounding box minimum corner (x, y, z).
223    min_bb: Vector3<f32>,
224
225    /// Axis-aligned bounding box maximum corner (x, y, z).
226    max_bb: Vector3<f32>,
227
228    /// Lighting configuration string (format not fully documented).
229    lighting_constants: String,
230
231    /// Vertex data array. Format depends on `material_type` and vertex format.
232    /// See [`Vertex`] for field details.
233    vertices: Vec<Vertex>,
234
235    /// Index buffer defining triangles (groups of 3 indices into `vertices`).
236    indices: Vec<u16>,
237
238    /// Indices stored after the index buffer, not counted in the mesh header. Rope meshes of version 6 use them,
239    /// for triangles of other meshes of the same LOD.
240    extra_indices: Vec<u16>,
241}
242
243/// Raw shader parameter data (not fully decoded).
244///
245/// Contains binary shader configuration data. The structure of this data
246/// is not fully reverse-engineered and is stored as raw bytes.
247#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
248#[getset(get = "pub", get_mut = "pub", set = "pub")]
249pub struct ShaderParams {
250    /// Raw shader parameter bytes.
251    data: Vec<u8>,
252}
253
254//---------------------------------------------------------------------------//
255//                              Implementations
256//---------------------------------------------------------------------------//
257
258impl Decodeable for RigidModel {
259
260    fn decode<R: ReadBytes>(data: &mut R, _extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
261        let signature_bytes = data.read_slice(4, false)?;
262        if signature_bytes.as_slice() != SIGNATURE {
263            return Err(RLibError::DecodingRigidModelUnsupportedSignature(signature_bytes));
264        }
265
266        let mut rigid = Self::default();
267        rigid.version = data.read_u32()?;
268
269        match rigid.version {
270            8 => rigid.read_v8(data)?,
271            7 => rigid.read_v7(data)?,
272            1 | 3 | 5 | 6 => rigid.read_v6(data)?,
273            _ => Err(RLibError::DecodingRigidModelUnsupportedVersion(rigid.version))?,
274        }
275
276        // Trigger an error if there's left data on the source.
277        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
278
279        Ok(rigid)
280    }
281}
282
283impl Encodeable for RigidModel {
284
285    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
286        buffer.write_all(SIGNATURE)?;
287        buffer.write_u32(self.version)?;
288
289        match self.version {
290            8 => self.write_v8(buffer)?,
291            7 => self.write_v7(buffer)?,
292            1 | 3 | 5 | 6 => self.write_v6(buffer)?,
293            _ => Err(RLibError::DecodingRigidModelUnsupportedVersion(self.version))?,
294        }
295
296        Ok(())
297    }
298}