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}