Skip to main content

xc3_model/
lib.rs

1//! # xc3_model
2//! xc3_model provides high level data access for the files that make up a model.
3//!
4//! Each type represents fully compressed and decoded data associated with one or more [xc3_lib] types.
5//! This simplifies the processing that needs to be done to access model data
6//! and abstracts away most of the game specific complexities.
7//!
8//! # Getting Started
9//! Loading a normal model returns a single [ModelRoot].
10//! Loading a map returns multiple [ModelRoot].
11//! Each [ModelRoot] has its own set of images.
12//!
13//! The [ShaderDatabase] is optional and improves the accuracy of texture and material assignments.
14//!
15//! ```rust no_run
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! use xc3_model::shader_database::ShaderDatabase;
18//!
19//! let database = ShaderDatabase::from_file("xc3.bin")?;
20//!
21//! let root = xc3_model::load_model("ch01011013.wimdo", Some(&database))?;
22//! println!("{}", root.image_textures.len());
23//!
24//! let roots = xc3_model::load_map("ma59a.wismhd", Some(&database))?;
25//! println!("{}", roots[0].image_textures.len());
26//! # Ok(())
27//! # }
28//! ```
29
30use std::{hash::Hash, io::Cursor, path::Path};
31
32use animation::Animation;
33use binrw::{BinRead, BinReaderExt};
34use error::{LoadModelError, LoadModelLegacyError};
35use glam::{Mat4, Vec3};
36use indexmap::IndexMap;
37use material::{create_materials, create_materials_samplers_legacy};
38use shader_database::ShaderDatabase;
39use skinning::Skinning;
40use vertex::ModelBuffers;
41use xc3_lib::{
42    apmd::Apmd,
43    bc::Bc,
44    error::{DecompressStreamError, ReadFileError},
45    evpa::Evpa,
46    hkt::Hkt,
47    msrd::streaming::chr_folder,
48    mxmd::{Mxmd, legacy::MxmdLegacy},
49    sar1::Sar1,
50    xbc1::MaybeXbc1,
51};
52
53pub use collision::load_collisions;
54pub use map::load_map;
55use material::{Material, Texture};
56pub use sampler::{AddressMode, FilterMode, Sampler};
57pub use skeleton::{Bone, Skeleton};
58pub use texture::{ExtractedTextures, ImageFormat, ImageTexture, ViewDimension};
59pub use transform::Transform;
60pub use xc3_lib::mxmd::{MeshRenderFlags2, MeshRenderPass};
61
62#[cfg(feature = "gltf")]
63pub mod gltf;
64
65pub mod animation;
66pub mod collision;
67pub mod error;
68mod map;
69pub mod material;
70pub mod model;
71pub mod monolib;
72mod sampler;
73pub mod shader_database;
74mod skeleton;
75pub mod skinning;
76mod texture;
77mod transform;
78pub mod vertex;
79
80// TODO: Document why these are different.
81// TODO: Come up with a better name
82#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
83#[derive(Debug, PartialEq, Clone)]
84pub struct ModelRoot {
85    pub models: Models,
86    /// The vertex data for each [Model].
87    pub buffers: ModelBuffers,
88
89    /// The textures selected by each [Material].
90    /// This includes all packed and embedded textures after
91    /// combining all mip levels.
92    pub image_textures: Vec<ImageTexture>,
93
94    // TODO: Do we even need to store the skinning if the weights already have the skinning bone name list?
95    pub skeleton: Option<Skeleton>,
96}
97
98#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
99#[derive(Debug, PartialEq, Clone)]
100pub struct MapRoot {
101    pub groups: Vec<ModelGroup>,
102
103    /// The textures selected by each [Material].
104    /// This includes all packed and embedded textures after
105    /// combining all mip levels.
106    pub image_textures: Vec<ImageTexture>,
107}
108
109#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
110#[derive(Debug, PartialEq, Clone)]
111pub struct ModelGroup {
112    pub models: Vec<Models>,
113    /// The vertex data selected by each [Model].
114    pub buffers: Vec<ModelBuffers>,
115}
116
117// TODO: Should samplers be optional?
118// TODO: Come up with a better name?
119/// See [ModelsV112](xc3_lib::mxmd::ModelsV112).
120#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
121#[derive(Debug, PartialEq, Clone)]
122pub struct Models {
123    pub models: Vec<Model>,
124    pub materials: Vec<Material>,
125    pub samplers: Vec<Sampler>,
126
127    // TODO: should skinning information be combined with the skeleton?
128    pub skinning: Option<Skinning>,
129
130    // TODO: when is this None?
131    pub lod_data: Option<LodData>,
132
133    // TODO: Use none instead of empty?
134    /// The name of the controller for each morph target like "mouth_shout".
135    pub morph_controller_names: Vec<String>,
136
137    /// The the morph controller names used for animations.
138    pub animation_morph_names: Vec<String>,
139
140    // TODO: make this a function instead to avoid dependencies?
141    /// The minimum XYZ coordinates of the bounding volume.
142    pub max_xyz: Vec3,
143
144    /// The maximum XYZ coordinates of the bounding volume.
145    pub min_xyz: Vec3,
146}
147
148/// See [ModelV112](xc3_lib::mxmd::ModelV112).
149#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
150#[derive(Debug, PartialEq, Clone)]
151pub struct Model {
152    pub meshes: Vec<Mesh>,
153    /// Each mesh has an instance for every transform in [instances](#structfield.instances).
154    pub instances: Vec<Mat4>,
155    /// The index of the [ModelBuffers] in [buffers](struct.ModelGroup.html#structfield.buffers).
156    /// This will only be non zero for some map models.
157    pub model_buffers_index: usize,
158
159    pub max_xyz: Vec3,
160    pub min_xyz: Vec3,
161    pub bounding_radius: f32,
162}
163
164/// See [MeshV112](xc3_lib::mxmd::MeshV112).
165#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
166#[derive(Debug, PartialEq, Clone)]
167pub struct Mesh {
168    pub flags1: u32,
169    pub flags2: MeshRenderFlags2,
170    pub vertex_buffer_index: usize,
171    pub index_buffer_index: usize,
172    pub index_buffer_index2: usize,
173    pub material_index: usize,
174    pub ext_mesh_index: Option<usize>,
175    pub lod_item_index: Option<usize>,
176    pub base_mesh_index: Option<usize>,
177}
178
179/// See [LodData](xc3_lib::mxmd::LodData).
180#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
181#[derive(Debug, PartialEq, Clone)]
182pub struct LodData {
183    pub unk1: u32,
184    pub items: Vec<LodItem>,
185    pub groups: Vec<LodGroup>,
186}
187
188/// See [LodItem](xc3_lib::mxmd::LodItem).
189#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
190#[derive(Debug, PartialEq, Clone)]
191pub struct LodItem {
192    pub unk2: f32,
193    pub index: u8,
194}
195
196/// See [LodGroup](xc3_lib::mxmd::LodGroup).
197#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
198#[derive(Debug, PartialEq, Clone)]
199pub struct LodGroup {
200    pub base_lod_index: usize,
201    pub lod_count: usize,
202}
203
204impl LodData {
205    /// Returns `true` if a mesh with `lod_item_index` should be rendered
206    /// as part of the highest detailed or base level of detail (LOD).
207    pub fn is_base_lod(&self, lod_item_index: Option<usize>) -> bool {
208        match lod_item_index {
209            Some(i) => self.groups.iter().any(|g| g.base_lod_index == i),
210            None => true,
211        }
212    }
213}
214
215// TODO: Take an iterator for wimdo paths and merge to support xc1?
216/// Load a model from a `.wimdo` or `.pcmdo` file.
217/// The corresponding `.wismt` or `.pcsmt` and `.chr` or `.arc` should be in the same directory.
218///
219/// # Examples
220/// Most models use a single file and return a single root.
221///
222/// ``` rust no_run
223/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
224/// use xc3_model::{load_model, shader_database::ShaderDatabase};
225///
226/// // Shulk's hair
227/// let database = ShaderDatabase::from_file("xc1.bin")?;
228/// let root = load_model("xeno1/chr/pc/pc010101.wimdo", Some(&database));
229///
230/// // Pyra
231/// let database = ShaderDatabase::from_file("xc2.bin")?;
232/// let root = load_model("xeno2/model/bl/bl000101.wimdo", Some(&database));
233///
234/// // Mio military uniform
235/// let database = ShaderDatabase::from_file("xc3.bin")?;
236/// let root = load_model("xeno3/chr/ch/ch01027000.wimdo", Some(&database));
237///
238/// // Tatsu
239/// let database = ShaderDatabase::from_file("xcxde.bin")?;
240/// let root = load_model("xenox/chr/np/np009001.wimdo", Some(&database));
241/// # Ok(())
242/// # }
243/// ```
244///
245/// For models split into multiple files, simply combine the roots.
246/// ```rust no_run
247/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
248/// # use xc3_model::{load_model, shader_database::ShaderDatabase};
249/// let database = ShaderDatabase::from_file("xc1.bin")?;
250///
251/// // Shulk's main outfit.
252/// let paths = [
253///     "xeno1/chr/pc/pc010201.wimdo",
254///     "xeno1/chr/pc/pc010202.wimdo",
255///     "xeno1/chr/pc/pc010203.wimdo",
256///     "xeno1/chr/pc/pc010204.wimdo",
257///     "xeno1/chr/pc/pc010205.wimdo",
258///     "xeno1/chr/pc/pc010109.wimdo",
259/// ];
260///
261/// let mut roots = Vec::new();
262/// for path in paths {
263///     let root = xc3_model::load_model(path, Some(&database))?;
264///     roots.push(root);
265/// }
266/// # Ok(())
267/// # }
268/// ```
269#[tracing::instrument(skip_all)]
270pub fn load_model<P: AsRef<Path>>(
271    wimdo_path: P,
272    shader_database: Option<&ShaderDatabase>,
273) -> Result<ModelRoot, LoadModelError> {
274    let wimdo_path = wimdo_path.as_ref();
275
276    let mxmd = load_wimdo(wimdo_path)?;
277    let chr = chr_folder(wimdo_path);
278
279    // Desktop PC models aren't used in game but are straightforward to support.
280    let is_pc = wimdo_path.extension().and_then(|e| e.to_str()) == Some("pcmdo");
281    let wismt_path = if is_pc {
282        wimdo_path.with_extension("pcsmt")
283    } else {
284        wimdo_path.with_extension("wismt")
285    };
286
287    let model_name = model_name(wimdo_path);
288    let skel = load_skel(wimdo_path, &model_name);
289
290    ModelRoot::from_mxmd(
291        &mxmd,
292        &wismt_path,
293        chr.as_deref(),
294        skel,
295        shader_database,
296        is_pc,
297    )
298}
299
300pub fn load_skel(wimdo: &Path, model_name: &str) -> Option<xc3_lib::bc::skel::Skel> {
301    load_chr(wimdo, model_name)
302        .and_then(|chr| {
303            // Xenoblade 3 embeds skeletons in chr files.
304            chr.entries
305                .iter()
306                .find_map(|e| match e.read_data::<xc3_lib::bc::Bc>() {
307                    Ok(bc) => match bc.data {
308                        xc3_lib::bc::BcData::Skel(skel) => Some(skel),
309                        _ => None,
310                    },
311                    _ => None,
312                })
313        })
314        .or_else(|| {
315            // TODO: Only try this for xcx de models (v40).
316            // Xenoblade X DE uses a file for just the skeleton.
317            Bc::from_file(wimdo.with_file_name(format!("{model_name}_rig.skl")))
318                .ok()
319                .or_else(|| {
320                    let model_name = model_name.trim_end_matches("_us").trim_end_matches("_eu");
321                    Bc::from_file(wimdo.with_file_name(format!("{model_name}_rig.skl"))).ok()
322                })
323                .and_then(|bc| match bc.data {
324                    xc3_lib::bc::BcData::Skel(skel) => Some(skel),
325                    _ => None,
326                })
327        })
328}
329
330fn load_chr(wimdo: &Path, model_name: &str) -> Option<Sar1> {
331    // TODO: Does every wimdo have a chr file?
332    // TODO: Does something control the chr name used?
333    // Try to find the base skeleton file first if it exists.
334    // This avoids loading incomplete skeletons specific to each model.
335    // XC1: pc010101.wimdo -> pc010000.chr.
336    // XC3: ch01012013.wimdo -> ch01012000.chr.
337    let base_name = base_chr_name(model_name);
338    Sar1::from_file(wimdo.with_file_name(&base_name).with_extension("chr"))
339        .ok()
340        .or_else(|| Sar1::from_file(wimdo.with_file_name(&base_name).with_extension("arc")).ok())
341        .or_else(|| Sar1::from_file(wimdo.with_extension("chr")).ok())
342        .or_else(|| Sar1::from_file(wimdo.with_extension("arc")).ok())
343        .or_else(|| {
344            // Keep trying with more 0's at the end to match in game naming conventions.
345            // This usually only requires one additional 0.
346            // XC3: ch01056013.wimdo -> ch01056010.chr.
347            (0..model_name.len()).find_map(|i| {
348                let mut chr_name = model_name.to_string();
349                chr_name.replace_range(chr_name.len() - i.., &"0".repeat(i));
350                let chr_path = wimdo.with_file_name(chr_name).with_extension("chr");
351                Sar1::from_file(chr_path).ok()
352            })
353        })
354}
355
356fn base_chr_name(model_name: &str) -> String {
357    let mut chr_name = model_name.to_string();
358    chr_name.replace_range(chr_name.len() - 3.., "000");
359    chr_name
360}
361
362/// Load a model from a `.camdo` file.
363/// The corresponding `.casmt`should be in the same directory.
364///
365/// # Examples
366///
367/// ``` rust no_run
368/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
369/// use xc3_model::{load_model_legacy, shader_database::ShaderDatabase};
370///
371/// // Tatsu
372/// let database = ShaderDatabase::from_file("xcx.bin")?;
373/// let root = load_model_legacy("xenox/chr_np/np009001.camdo", Some(&database))?;
374/// # Ok(())
375/// # }
376/// ```
377#[tracing::instrument(skip_all)]
378pub fn load_model_legacy<P: AsRef<Path>>(
379    camdo_path: P,
380    shader_database: Option<&ShaderDatabase>,
381) -> Result<ModelRoot, LoadModelLegacyError> {
382    let camdo_path = camdo_path.as_ref();
383    let mxmd = MxmdLegacy::from_file(camdo_path).map_err(LoadModelLegacyError::Camdo)?;
384
385    let casmt = mxmd
386        .streaming
387        .as_ref()
388        .map(|_| {
389            std::fs::read(camdo_path.with_extension("casmt")).map_err(LoadModelLegacyError::Casmt)
390        })
391        .transpose()?;
392
393    let model_name = model_name(camdo_path);
394    let hkt_path = camdo_path.with_file_name(format!("{model_name}_rig.hkt"));
395    let hkt = Hkt::from_file(hkt_path).ok();
396
397    ModelRoot::from_mxmd_model_legacy(&mxmd, casmt, hkt.as_ref(), shader_database)
398}
399
400// TODO: move this to xc3_lib?
401#[derive(BinRead)]
402enum Wimdo {
403    Mxmd(Box<Mxmd>),
404    Apmd(Apmd),
405}
406
407fn load_wimdo(wimdo_path: &Path) -> Result<Mxmd, LoadModelError> {
408    let mut reader = Cursor::new(std::fs::read(wimdo_path).map_err(|e| {
409        LoadModelError::Wimdo(ReadFileError {
410            path: wimdo_path.to_owned(),
411            source: e.into(),
412        })
413    })?);
414    let wimdo: Wimdo = reader.read_le().map_err(|e| {
415        LoadModelError::Wimdo(ReadFileError {
416            path: wimdo_path.to_owned(),
417            source: e,
418        })
419    })?;
420    match wimdo {
421        Wimdo::Mxmd(mxmd) => Ok(*mxmd),
422        Wimdo::Apmd(apmd) => apmd
423            .entries
424            .iter()
425            .find_map(|e| {
426                if e.entry_type == xc3_lib::apmd::EntryType::Mxmd {
427                    Some(Mxmd::from_bytes(&e.entry_data))
428                } else {
429                    None
430                }
431            })
432            .map_or(Err(LoadModelError::MissingApmdMxmdEntry), |r| {
433                r.map_err(|e| {
434                    LoadModelError::Wimdo(ReadFileError {
435                        path: wimdo_path.to_owned(),
436                        source: e,
437                    })
438                })
439            }),
440    }
441}
442
443/// Load all animations from a `.anm`, `.mot`, `.motstm_data`, or `.evpa` file.
444///
445/// # Examples
446/// ``` rust no_run
447/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
448/// // Fiora
449/// let animations = xc3_model::load_animations("xeno1/chr/pc/mp080000.mot")?;
450/// println!("{}", animations.len());
451///
452/// // Pyra
453/// let animations = xc3_model::load_animations("xeno2/model/bl/bl000101.mot")?;
454/// println!("{}", animations.len());
455///
456/// // Mio military uniform
457/// let animations = xc3_model::load_animations("xeno3/chr/ch/ch01027000_event.mot")?;
458/// println!("{}", animations.len());
459/// # Ok(())
460/// # }
461/// ```
462#[tracing::instrument(skip_all)]
463pub fn load_animations<P: AsRef<Path>>(
464    anim_path: P,
465) -> Result<Vec<Animation>, DecompressStreamError> {
466    // Most animations are in sar1 archives.
467    // Xenoblade 1 DE compresses the sar1 archive.
468    // Some animations are in standalone BC files.
469    // Some Xenoblade X DE animations are in xcb1 archives.
470    let anim_file = <MaybeXbc1<AnimFile>>::from_file(anim_path)?;
471
472    let mut animations = Vec::new();
473    match anim_file {
474        MaybeXbc1::Uncompressed(anim) => add_anim_file(&mut animations, anim),
475        MaybeXbc1::Xbc1(xbc1) => {
476            if let Ok(anim) = xbc1.extract() {
477                add_anim_file(&mut animations, anim);
478            }
479        }
480    }
481
482    Ok(animations)
483}
484
485#[derive(BinRead)]
486enum AnimFile {
487    Sar1(Sar1),
488    Bc(Bc),
489    Evpa(Evpa),
490}
491
492fn add_anim_file(animations: &mut Vec<Animation>, anim: AnimFile) {
493    match anim {
494        AnimFile::Sar1(sar1) => {
495            for entry in &sar1.entries {
496                if let Ok(bc) = entry.read_data() {
497                    add_bc_animations(animations, bc);
498                }
499            }
500        }
501        AnimFile::Bc(bc) => {
502            add_bc_animations(animations, bc);
503        }
504        AnimFile::Evpa(evpa) => {
505            for e in &evpa.entries {
506                if let Ok(bc) = Bc::from_bytes(&e.entry_data) {
507                    add_bc_animations(animations, bc);
508                }
509            }
510        }
511    }
512}
513
514fn add_bc_animations(animations: &mut Vec<Animation>, bc: Bc) {
515    if let xc3_lib::bc::BcData::Anim(anim) = bc.data {
516        let animation = Animation::from_anim(&anim);
517        animations.push(animation);
518    }
519}
520
521// TODO: Move this to xc3_shader?
522fn model_name(model_path: &Path) -> String {
523    model_path
524        .file_stem()
525        .unwrap_or_default()
526        .to_string_lossy()
527        .to_string()
528}
529
530#[cfg(feature = "arbitrary")]
531fn arbitrary_smolstr(u: &mut arbitrary::Unstructured) -> arbitrary::Result<smol_str::SmolStr> {
532    let text: String = u.arbitrary()?;
533    Ok(text.into())
534}
535
536#[cfg(test)]
537#[macro_export]
538macro_rules! assert_hex_eq {
539    ($a:expr, $b:expr) => {
540        pretty_assertions::assert_str_eq!(hex::encode($a), hex::encode($b))
541    };
542}
543
544/// A trait for mapping unique items to an index.
545pub trait IndexMapExt<T> {
546    /// The index value associated with `key`.
547    /// Inserts `key` with an index equal to the current length if not present.
548    fn entry_index(&mut self, key: T) -> usize;
549}
550
551impl<T> IndexMapExt<T> for IndexMap<T, usize>
552where
553    T: Hash + Eq,
554{
555    fn entry_index(&mut self, key: T) -> usize {
556        let new_value = self.len();
557        *self.entry(key).or_insert(new_value)
558    }
559}
560
561fn get_bytes(bytes: &[u8], offset: u32, size: Option<u32>) -> std::io::Result<&[u8]> {
562    let start = offset as usize;
563
564    match size {
565        Some(size) => {
566            let end = start + size as usize;
567            bytes.get(start..end).ok_or_else(|| {
568                std::io::Error::new(
569                    std::io::ErrorKind::UnexpectedEof,
570                    format!(
571                        "byte range {start}..{end} out of range for length {}",
572                        bytes.len()
573                    ),
574                )
575            })
576        }
577        None => bytes.get(start..).ok_or_else(|| {
578            std::io::Error::new(
579                std::io::ErrorKind::UnexpectedEof,
580                format!(
581                    "byte offset {start} out of range for length {}",
582                    bytes.len()
583                ),
584            )
585        }),
586    }
587}