Skip to main content

rpfm_extensions/gltf/
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//! glTF export support for RigidModel 3D models.
12//!
13//! This module provides functionality to convert Total War's proprietary RigidModel
14//! format (`.rigid_model_v2`) to the standard glTF 2.0 format. glTF is widely supported
15//! by 3D modeling software like Blender, Maya, and 3ds Max.
16//!
17//! # Features
18//!
19//! - **Mesh Export**: Vertex positions, normals, and texture coordinates
20//! - **Material Support**: Basic material properties and texture references
21//! - **LOD Levels**: Each LOD (Level of Detail) is exported as a separate scene
22//! - **Embedded Data**: Binary data is base64-encoded inline (no external files)
23//!
24//! # Limitations
25//!
26//! - Animations are not exported (RigidModel doesn't contain animation data)
27//! - Some advanced material properties may not translate directly to glTF
28//! - Skeleton/rigging data export is limited
29//!
30//! # Usage Example
31//!
32//! ```ignore
33//! use rpfm_extensions::gltf::gltf_from_rigid;
34//!
35//! // Convert RigidModel to glTF
36//! let gltf = gltf_from_rigid(&rigid_model, &mut dependencies)?;
37//!
38//! // Write to file
39//! let file = File::create("output.gltf")?;
40//! gltf.to_writer(file)?;
41//! ```
42//!
43//! # Output Format
44//!
45//! The exported glTF uses the JSON format (`.gltf`) with embedded binary data
46//! rather than the binary format (`.glb`). This makes the files larger but
47//! easier to inspect and debug.
48//!
49//! # Dependencies Integration
50//!
51//! The export process can optionally use the [`Dependencies`] cache to resolve
52//! texture references and include texture data in the export.
53//!
54//! [`Dependencies`]: crate::dependencies::Dependencies
55
56use base64::{Engine, engine::general_purpose::STANDARD};
57pub use gltf::{Document, Gltf, json};
58use gltf_json::{image::MimeType, validation::{Checked::Valid, USize64}};
59
60use std::fs::File;
61use std::io::{BufWriter, Write};
62use std::mem;
63use std::path::Path;
64
65use rpfm_lib::{error::Result, files::RFileDecoded};
66use rpfm_lib::files::rigidmodel::{*, materials::TextureType, vertices::Vertex};
67
68use crate::dependencies::Dependencies;
69
70#[cfg(test)] mod test;
71
72//---------------------------------------------------------------------------//
73//                              Implementations
74//---------------------------------------------------------------------------//
75
76/// Converts a RigidModel to glTF format.
77///
78/// This function takes a Total War RigidModel and produces a glTF document
79/// that can be saved to disk or further processed.
80///
81/// # Arguments
82///
83/// * `value` - The RigidModel to convert
84/// * `dependencies` - Dependencies cache for resolving texture references
85///
86/// # Returns
87///
88/// A `Gltf` document containing the converted model data.
89///
90/// # LOD Handling
91///
92/// Since glTF doesn't natively support LOD levels, each LOD from the RigidModel
93/// is exported as a separate scene within the glTF document. The first scene
94/// (index 0) contains LOD 0 (highest detail).
95///
96/// # Mesh Data
97///
98/// For each mesh, the following data is exported:
99/// - Vertex positions (vec3)
100/// - Texture coordinates (vec2, up to 2 sets)
101/// - Vertex normals (vec3) - reconstructed from tangent/bitangent if needed
102///
103/// # Materials
104///
105/// Materials are created for each mesh with:
106/// - Base color texture (diffuse map)
107/// - Normal map
108/// - Metallic-roughness properties
109pub fn gltf_from_rigid(value: &RigidModel, dependencies: &mut Dependencies) -> Result<Gltf> {
110    let mut root = gltf_json::Root::default();
111
112    // All the data that is total war-exclusive goes here.
113    //root.extras = Some(Box::new(RawValue:: HashMap::new()));
114
115    for lod in value.lods() {
116
117        // As Gltf doesn't support lod levels natively, we do one scene per lod.
118        let mut scene = json::Scene {
119            extensions: Default::default(),
120            extras: Default::default(),
121            name: None,
122            nodes: vec![],
123        };
124
125        for mesh_block in lod.mesh_blocks() {
126
127            let vertices = mesh_block.mesh().vertices();
128            let indices = mesh_block.mesh().indices();
129
130            // Calculate mins and max for the values of the vertex.
131            let (min_pos, max_pos) = bounding_coords_positions(vertices);
132
133            // Encode the vertex and index data to binary.
134            let vertex_bin = to_padded_byte_vector(vertices.clone());
135            let index_bin = to_padded_byte_vector(indices.clone());
136
137            // Buffers
138            let vertex_buffer_length = vertices.len() * mem::size_of::<Vertex>();
139            let vertex_buffer = root.push(json::Buffer {
140                byte_length: USize64::from(vertex_buffer_length),
141                extensions: Default::default(),
142                extras: Default::default(),
143                name: None,
144                uri: Some(format!("data:application/octet-stream;base64,{}", STANDARD.encode(&vertex_bin))),
145            });
146
147            let index_buffer_length = indices.len() * mem::size_of::<u16>();
148            let index_buffer = root.push(json::Buffer {
149                byte_length: USize64::from(index_buffer_length),
150                extensions: Default::default(),
151                extras: Default::default(),
152                name: None,
153                uri: Some(format!("data:application/octet-stream;base64,{}", STANDARD.encode(&index_bin))),
154            });
155
156            // Buffer views.
157            let vertex_buffer_view = root.push(json::buffer::View {
158                buffer: vertex_buffer,
159                byte_length: USize64::from(vertex_buffer_length),
160                byte_offset: None,
161                byte_stride: Some(json::buffer::Stride(mem::size_of::<Vertex>())),
162                extensions: Default::default(),
163                extras: Default::default(),
164                name: None,
165                target: Some(Valid(json::buffer::Target::ArrayBuffer)),
166            });
167
168            let index_buffer_view = root.push(json::buffer::View {
169                buffer: index_buffer,
170                byte_length: USize64::from(index_buffer_length),
171                byte_offset: None,
172                byte_stride: None,
173                extensions: Default::default(),
174                extras: Default::default(),
175                name: None,
176                target: Some(Valid(json::buffer::Target::ElementArrayBuffer)),
177            });
178
179            // Accessors
180            let indices = root.push(json::Accessor {
181                buffer_view: Some(index_buffer_view),
182                byte_offset: Some(USize64(0)),
183                count: USize64::from(indices.len()),
184                component_type: Valid(json::accessor::GenericComponentType(
185                    json::accessor::ComponentType::U16,
186                )),
187                extensions: Default::default(),
188                extras: Default::default(),
189                type_: Valid(json::accessor::Type::Scalar),
190                min: None,
191                max: None,
192                name: None,
193                normalized: false,
194                sparse: None,
195            });
196
197            let positions = root.push(json::Accessor {
198                buffer_view: Some(vertex_buffer_view),
199                byte_offset: Some(USize64(0)),
200                count: USize64::from(vertices.len()),
201                component_type: Valid(json::accessor::GenericComponentType(
202                    json::accessor::ComponentType::F32,
203                )),
204                extensions: Default::default(),
205                extras: Default::default(),
206                type_: Valid(json::accessor::Type::Vec3),
207                min: Some(json::Value::from(Vec::from(min_pos))),
208                max: Some(json::Value::from(Vec::from(max_pos))),
209                name: None,
210                normalized: false,
211                sparse: None,
212            });
213
214            let text_coords_1 = root.push(json::Accessor {
215                buffer_view: Some(vertex_buffer_view),
216                byte_offset: Some(USize64(16)),
217                count: USize64::from(vertices.len()),
218                component_type: Valid(json::accessor::GenericComponentType(
219                    json::accessor::ComponentType::F32,
220                )),
221                extensions: Default::default(),
222                extras: Default::default(),
223                type_: Valid(json::accessor::Type::Vec2),
224                min: None,
225                max: None,
226                name: None,
227                normalized: false,
228                sparse: None,
229            });
230
231            let text_coords_2 = root.push(json::Accessor {
232                buffer_view: Some(vertex_buffer_view),
233                byte_offset: Some(USize64(24)),
234                count: USize64::from(vertices.len()),
235                component_type: Valid(json::accessor::GenericComponentType(
236                    json::accessor::ComponentType::F32,
237                )),
238                extensions: Default::default(),
239                extras: Default::default(),
240                type_: Valid(json::accessor::Type::Vec2),
241                min: None,
242                max: None,
243                name: None,
244                normalized: false,
245                sparse: None,
246            });
247
248            let normals = root.push(json::Accessor {
249                buffer_view: Some(vertex_buffer_view),
250                byte_offset: Some(USize64(32)),
251                count: USize64::from(vertices.len()),
252                component_type: Valid(json::accessor::GenericComponentType(
253                    json::accessor::ComponentType::F32,
254                )),
255                extensions: Default::default(),
256                extras: Default::default(),
257                type_: Valid(json::accessor::Type::Vec3),
258                min: None,
259                max: None,
260                name: None,
261                normalized: false,
262                sparse: None,
263            });
264
265            let tangents = root.push(json::Accessor {
266                buffer_view: Some(vertex_buffer_view),
267                byte_offset: Some(USize64(48)),
268                count: USize64::from(vertices.len()),
269                component_type: Valid(json::accessor::GenericComponentType(
270                    json::accessor::ComponentType::F32,
271                )),
272                extensions: Default::default(),
273                extras: Default::default(),
274                type_: Valid(json::accessor::Type::Vec4),
275                min: None,
276                max: None,
277                name: None,
278                normalized: false,
279                sparse: None,
280            });
281
282            // These are calculated by the client from tangents, not specified in the file.
283            /*let _bitangents = root.push(json::Accessor {
284                buffer_view: Some(vertex_buffer_view),
285                byte_offset: Some(USize64(64)),
286                count: USize64::from(vertices.len()),
287                component_type: Valid(json::accessor::GenericComponentType(
288                    json::accessor::ComponentType::F32,
289                )),
290                extensions: Default::default(),
291                extras: Default::default(),
292                type_: Valid(json::accessor::Type::Vec4),
293                min: Some(json::Value::from(Vec::from(min_bitan))),
294                max: Some(json::Value::from(Vec::from(max_bitan))),
295                name: None,
296                normalized: false,
297                sparse: None,
298            });*/
299
300            let _colors = root.push(json::Accessor {
301                buffer_view: Some(vertex_buffer_view),
302                byte_offset: Some(USize64(80)),
303                count: USize64::from(vertices.len()),
304                component_type: Valid(json::accessor::GenericComponentType(
305                    json::accessor::ComponentType::F32,
306                )),
307                extensions: Default::default(),
308                extras: Default::default(),
309                type_: Valid(json::accessor::Type::Vec4),
310                min: None,
311                max: None,
312                name: None,
313                normalized: false,
314                sparse: None,
315            });
316
317            let joints = root.push(json::Accessor {
318                buffer_view: Some(vertex_buffer_view),
319                byte_offset: Some(USize64(96)),
320                count: USize64::from(vertices.len()),
321                component_type: Valid(json::accessor::GenericComponentType(
322                    json::accessor::ComponentType::U8,
323                )),
324                extensions: Default::default(),
325                extras: Default::default(),
326                type_: Valid(json::accessor::Type::Vec4),
327                min: None,
328                max: None,
329                name: None,
330                normalized: false,
331                sparse: None,
332            });
333
334            let weights = root.push(json::Accessor {
335                buffer_view: Some(vertex_buffer_view),
336                byte_offset: Some(USize64(100)),
337                count: USize64::from(vertices.len()),
338                component_type: Valid(json::accessor::GenericComponentType(
339                    json::accessor::ComponentType::F32,
340                )),
341                extensions: Default::default(),
342                extras: Default::default(),
343                type_: Valid(json::accessor::Type::Vec4),
344                min: None,
345                max: None,
346                name: None,
347                normalized: false,
348                sparse: None,
349            });
350
351            // After the mesh, add the material data.
352            let mut material = json::Material {
353                alpha_cutoff: Default::default(),
354                alpha_mode: Default::default(),
355                double_sided: Default::default(),
356                name: Default::default(),
357                pbr_metallic_roughness: Default::default(),
358                normal_texture: Default::default(),
359                occlusion_texture: Default::default(),
360                emissive_texture: Default::default(),
361                emissive_factor: Default::default(),
362                extensions: Default::default(),
363                extras: Default::default(),
364            };
365
366            // Add the textures used by the material block.
367            for text in mesh_block.material().textures() {
368                if let Ok(ref mut image) = dependencies.file_mut(text.path(), true, true) {
369                    let image_data = image.decode(&None, false, true)?.unwrap();
370
371                    if let RFileDecoded::Image(image) = image_data {
372                        let image_buffer_length = image.data().len();
373
374                        let image_buffer = root.push(json::Buffer {
375                            byte_length: USize64::from(image_buffer_length),
376                            extensions: Default::default(),
377                            extras: Default::default(),
378                            name: None,
379                            uri: Some(format!("data:application/octet-stream;base64,{}", STANDARD.encode(image.data()))),
380                        });
381
382                        let image_buffer_view = root.push(json::buffer::View {
383                            buffer: image_buffer,
384                            byte_length: USize64::from(image_buffer_length),
385                            byte_offset: None,
386                            byte_stride: None,
387                            extensions: Default::default(),
388                            extras: Default::default(),
389                            name: None,
390                            target: None,
391                        });
392
393                        let image = root.push(json::Image {
394                            buffer_view: Some(image_buffer_view),
395                            mime_type: Some(MimeType(String::from("image/png"))),
396                            name: Default::default(),
397                            uri: Default::default(),
398                            extensions: Default::default(),
399                            extras: Default::default(),
400                        });
401
402                        let texture = root.push(json::Texture {
403                            name: None,
404                            sampler: None,
405                            source: image,
406                            extensions: None,
407                            extras: None,
408                        });
409
410                        match text.tex_type() {
411                            TextureType::Diffuse => {
412                                material.pbr_metallic_roughness.base_color_texture = Some(json::texture::Info {
413                                    index: texture,
414                                    tex_coord: 1,
415                                    extensions: None,
416                                    extras: Default::default(),
417                                });
418                            },
419                            TextureType::Normal => {
420                                material.normal_texture = Some(json::material::NormalTexture {
421                                    index: texture,
422                                    scale: 1.0,
423                                    tex_coord: 0,
424                                    extensions: None,
425                                    extras: None,
426                                });
427                            },
428                            _ => {}
429                        }
430                    }
431                }
432            }
433
434            let material = root.push(material);
435
436            // Build the primitive for the mesh.
437            let primitive = json::mesh::Primitive {
438                attributes: {
439                    let mut map = std::collections::BTreeMap::new();
440                    map.insert(Valid(json::mesh::Semantic::Positions), positions);
441                    map.insert(Valid(json::mesh::Semantic::Normals), normals);
442                    map.insert(Valid(json::mesh::Semantic::Tangents), tangents);
443                    map.insert(Valid(json::mesh::Semantic::TexCoords(0)), text_coords_1);
444                    map.insert(Valid(json::mesh::Semantic::TexCoords(1)), text_coords_2);
445                    //map.insert(Valid(json::mesh::Semantic::Colors(0)), colors);
446                    map.insert(Valid(json::mesh::Semantic::Joints(0)), joints);
447                    map.insert(Valid(json::mesh::Semantic::Weights(0)), weights);
448                    map
449                },
450                extensions: Default::default(),
451                extras: Default::default(),
452                indices: Some(indices),
453                material: Some(material),
454                mode: Valid(json::mesh::Mode::Triangles),
455                targets: None,
456            };
457
458            let mesh = root.push(json::Mesh {
459                extensions: Default::default(),
460                extras: Default::default(),
461                name: Some(mesh_block.mesh().name().to_owned()),
462                primitives: vec![primitive],
463                weights: None,
464            });
465
466            let node = root.push(json::Node {
467                mesh: Some(mesh),
468                ..Default::default()
469            });
470
471            scene.nodes.push(node);
472        }
473
474        root.push(scene);
475    }
476
477    // Build the gltf itself.
478    let gltf = Gltf {
479        document: Document::from_json(root).unwrap(),
480        blob: None,
481    };
482
483    Ok(gltf)
484}
485
486/// NOT YET IMPLEMENTED.
487pub fn rigid_from_gltf(_value: &Gltf) -> Result<RigidModel> {
488    let rigid = RigidModel::default();
489
490    Ok(rigid)
491}
492
493pub fn save_gltf_to_disk(value: &Gltf, path: &Path) -> Result<()> {
494    let mut writer_gltf = BufWriter::new(File::create(path)?);
495    writer_gltf.write_all(value.as_json().to_string_pretty()?.as_bytes())?;
496    //let mut writer_bin = BufWriter::new(File::create(path).unwrap());
497    //writer_bin.write_all(&value.blob.clone().unwrap()).unwrap();
498    //
499    Ok(())
500}
501
502/// Calculate bounding coordinates of a list of vertices, used for the clipping distance of the model
503fn bounding_coords_positions(points: &[Vertex]) -> ([f32; 3], [f32; 3]) {
504    let mut min = [f32::MAX, f32::MAX, f32::MAX];
505    let mut max = [f32::MIN, f32::MIN, f32::MIN];
506
507    for point in points {
508        let p = point.position();
509        for i in 0..3 {
510            min[i] = f32::min(min[i], p[i]);
511            max[i] = f32::max(max[i], p[i]);
512        }
513    }
514    (min, max)
515}
516/*
517fn bounding_coords_normals(points: &[Vertex]) -> ([f32; 3], [f32; 3]) {
518    let mut min = [f32::MAX, f32::MAX, f32::MAX];
519    let mut max = [f32::MIN, f32::MIN, f32::MIN];
520
521    for point in points {
522        let p = point.normal();
523        for i in 0..3 {
524            min[i] = f32::min(min[i], p[i]);
525            max[i] = f32::max(max[i], p[i]);
526        }
527    }
528    (min, max)
529}
530
531fn bounding_coords_tangents(points: &[Vertex]) -> ([f32; 4], [f32; 4]) {
532    let mut min = [f32::MAX, f32::MAX, f32::MAX, f32::MAX];
533    let mut max = [f32::MIN, f32::MIN, f32::MIN, f32::MIN];
534
535    for point in points {
536        let p = point.tangent();
537        for i in 0..4 {
538            min[i] = f32::min(min[i], p[i]);
539            max[i] = f32::max(max[i], p[i]);
540        }
541    }
542    (min, max)
543}
544
545fn bounding_coords_bitangents(points: &[Vertex]) -> ([f32; 4], [f32; 4]) {
546    let mut min = [f32::MAX, f32::MAX, f32::MAX, f32::MAX];
547    let mut max = [f32::MIN, f32::MIN, f32::MIN, f32::MIN];
548
549    for point in points {
550        let p = point.bitangent();
551        for i in 0..4 {
552            min[i] = f32::min(min[i], p[i]);
553            max[i] = f32::max(max[i], p[i]);
554        }
555    }
556    (min, max)
557}
558
559fn bounding_coords_colours(points: &[Vertex]) -> ([f32; 4], [f32; 4]) {
560    let mut min = [f32::MAX, f32::MAX, f32::MAX, f32::MAX];
561    let mut max = [f32::MIN, f32::MIN, f32::MIN, f32::MIN];
562
563    for point in points {
564        let p = point.color();
565        for i in 0..4 {
566            min[i] = f32::min(min[i], p[i]);
567            max[i] = f32::max(max[i], p[i]);
568        }
569    }
570    (min, max)
571}
572
573fn bounding_coords_joints(points: &[Vertex]) -> ([u8; 4], [u8; 4]) {
574    let mut min = [u8::MAX, u8::MAX, u8::MAX, u8::MAX];
575    let mut max = [u8::MIN, u8::MIN, u8::MIN, u8::MIN];
576
577    for point in points {
578        let p = point.bone_indices();
579        for i in 0..4 {
580            min[i] = u8::min(min[i], p[i]);
581            max[i] = u8::max(max[i], p[i]);
582        }
583    }
584    (min, max)
585}
586
587fn bounding_coords_weight(points: &[Vertex]) -> ([f32; 4], [f32; 4]) {
588    let mut min = [f32::MAX, f32::MAX, f32::MAX, f32::MAX];
589    let mut max = [f32::MIN, f32::MIN, f32::MIN, f32::MIN];
590
591    for point in points {
592        let p = point.weights();
593        for i in 0..4 {
594            min[i] = f32::min(min[i], p[i]);
595            max[i] = f32::max(max[i], p[i]);
596        }
597    }
598    (min, max)
599}
600
601fn align_to_multiple_of_four(n: &mut usize) {
602    *n = (*n + 3) & !3;
603}*/
604
605fn to_padded_byte_vector<T>(vec: Vec<T>) -> Vec<u8> {
606    let byte_length = vec.len() * mem::size_of::<T>();
607    let byte_capacity = vec.capacity() * mem::size_of::<T>();
608    let alloc = vec.into_boxed_slice();
609    let ptr = Box::<[T]>::into_raw(alloc) as *mut u8;
610    let mut new_vec = unsafe { Vec::from_raw_parts(ptr, byte_length, byte_capacity) };
611    while new_vec.len() % 4 != 0 {
612        new_vec.push(0); // pad to multiple of four bytes
613    }
614    new_vec
615}