Skip to main content

physis/
mtrl.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#![allow(clippy::unnecessary_fallible_conversions)] // This wrongly trips on binrw code
5
6use std::io::Cursor;
7
8use crate::common::Platform;
9use crate::common_file_operations::{Half1, Half2, Half3, null_terminated_utf8};
10use crate::mtrl::ColorDyeTable::{
11    DawntrailColorDyeTable, LegacyColorDyeTable, OpaqueColorDyeTable,
12};
13use crate::mtrl::ColorTable::{DawntrailColorTable, LegacyColorTable, OpaqueColorTable};
14use crate::{ByteBuffer, ByteSpan, ReadableFile, WritableFile};
15use binrw::{BinRead, BinResult, BinWrite, binrw};
16
17#[binrw]
18#[derive(Debug)]
19#[allow(dead_code)]
20struct MaterialFileHeader {
21    version: u32,
22    file_size: u16,
23    data_set_size: u16,
24    string_table_size: u16,
25    shader_package_name_offset: u16,
26    texture_count: u8,
27    uv_set_count: u8,
28    color_set_count: u8,
29    additional_data_size: u8,
30}
31
32#[binrw]
33#[derive(Debug)]
34struct MaterialHeader {
35    shader_value_list_size: u16,
36    shader_key_count: u16,
37    constant_count: u16,
38    sampler_count: u16,
39    flags: u32,
40}
41
42#[binrw]
43#[derive(Debug)]
44#[allow(dead_code)]
45struct ColorSet {
46    name_offset: u16,
47    index: u16,
48}
49
50#[binrw]
51#[derive(Debug, Clone, Copy)]
52#[repr(C)]
53#[allow(dead_code)]
54pub struct DawntrailColorTableRow {
55    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
56    pub diffuse_color: [f32; 3],
57
58    #[br(map = |x: Half1| { x.value.to_f32() })]
59    pub unknown1: f32,
60
61    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
62    pub specular_color: [f32; 3],
63
64    #[br(map = |x: Half1| { x.value.to_f32() })]
65    pub unknown2: f32,
66
67    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
68    pub emissive_color: [f32; 3],
69
70    #[br(map = |x: Half1| { x.value.to_f32() })]
71    pub unknown3: f32,
72
73    #[br(map = |x: Half1| { x.value.to_f32() })]
74    pub sheen_rate: f32,
75
76    #[br(map = |x: Half1| { x.value.to_f32() })]
77    pub sheen_tint: f32,
78
79    #[br(map = |x: Half1| { x.value.to_f32() })]
80    pub sheen_aperture: f32,
81
82    #[br(map = |x: Half1| { x.value.to_f32() })]
83    pub unknown4: f32,
84
85    #[br(map = |x: Half1| { x.value.to_f32() })]
86    pub roughness: f32,
87
88    #[br(map = |x: Half1| { x.value.to_f32() })]
89    pub unknown5: f32,
90
91    #[br(map = |x: Half1| { x.value.to_f32() })]
92    pub metalness: f32,
93
94    #[br(map = |x: Half1| { x.value.to_f32() })]
95    pub anisotropy: f32,
96
97    #[br(map = |x: Half1| { x.value.to_f32() })]
98    pub unknown6: f32,
99
100    #[br(map = |x: Half1| { x.value.to_f32() })]
101    pub sphere_mask: f32,
102
103    #[br(map = |x: Half1| { x.value.to_f32() })]
104    pub unknown7: f32,
105
106    #[br(map = |x: Half1| { x.value.to_f32() })]
107    pub unknown8: f32,
108
109    pub shader_index: u16,
110
111    pub tile_set: u16,
112
113    #[br(map = |x: Half1| { x.value.to_f32() })]
114    pub tile_alpha: f32,
115
116    pub sphere_index: u16,
117
118    #[br(map = |x: Half2| { [x.x.to_f32(), x.y.to_f32()] })]
119    pub material_repeat: [f32; 2],
120
121    #[br(map = |x: Half2| { [x.x.to_f32(), x.y.to_f32()] })]
122    pub material_skew: [f32; 2],
123}
124
125#[binrw]
126#[derive(Debug, Clone, Copy)]
127#[repr(C)]
128#[allow(dead_code)]
129pub struct LegacyColorTableRow {
130    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
131    pub diffuse_color: [f32; 3],
132
133    #[br(map = |x: Half1| { x.value.to_f32() })]
134    pub specular_strength: f32,
135
136    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
137    pub specular_color: [f32; 3],
138
139    #[br(map = |x: Half1| { x.value.to_f32() })]
140    pub gloss_strength: f32,
141
142    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
143    pub emissive_color: [f32; 3],
144
145    pub tile_set: u16,
146
147    #[br(map = |x: Half1| { x.value.to_f32() })]
148    pub material_repeat_x: f32,
149
150    #[br(map = |x: Half2| { [x.x.to_f32(), x.y.to_f32()] })]
151    pub material_skew: [f32; 2],
152
153    #[br(map = |x: Half1| { x.value.to_f32() })]
154    pub material_repeat_y: f32,
155}
156
157#[binrw]
158#[derive(Debug, Clone, Default)]
159#[allow(dead_code)]
160pub struct LegacyColorTableData {
161    #[br(count = 16)]
162    pub rows: Vec<LegacyColorTableRow>,
163}
164
165#[binrw]
166#[derive(Debug, Clone, Default)]
167#[allow(dead_code)]
168pub struct DawntrailColorTableData {
169    #[br(count = 32)]
170    pub rows: Vec<DawntrailColorTableRow>,
171}
172
173#[binrw]
174#[derive(Debug, Clone, Default)]
175#[allow(dead_code)]
176pub struct OpaqueColorTableData {
177    // TODO: Support
178}
179
180#[binrw]
181#[derive(Debug, Clone)]
182#[allow(dead_code)]
183pub enum ColorTable {
184    LegacyColorTable(LegacyColorTableData),
185    DawntrailColorTable(DawntrailColorTableData),
186    OpaqueColorTable(OpaqueColorTableData),
187}
188
189#[binrw]
190#[derive(Debug, Clone)]
191#[allow(dead_code)]
192pub struct LegacyColorDyeTableRow {
193    #[br(temp)]
194    #[bw(calc = 0)] // TODO: lol no
195    data: u16,
196
197    #[br(calc = data >> 5)]
198    #[bw(ignore)]
199    pub template: u16,
200
201    #[br(calc = (data & 0x01) != 0)]
202    #[bw(ignore)]
203    pub diffuse: bool,
204
205    #[br(calc = (data & 0x02) != 0)]
206    #[bw(ignore)]
207    pub specular: bool,
208
209    #[br(calc = (data & 0x04) != 0)]
210    #[bw(ignore)]
211    pub emissive: bool,
212
213    #[br(calc = (data & 0x08) != 0)]
214    #[bw(ignore)]
215    pub gloss: bool,
216
217    #[br(calc = (data & 0x10) != 0)]
218    #[bw(ignore)]
219    pub specular_strength: bool,
220}
221
222#[binrw]
223#[derive(Debug, Clone)]
224#[allow(dead_code)]
225pub struct DawntrailColorDyeTableRow {
226    #[br(temp)]
227    #[bw(calc = 0)] // TODO: lol no
228    data: u32,
229
230    #[br(calc = ((data >> 16) & 0x7FF) as u16)]
231    #[bw(ignore)]
232    pub template: u16,
233
234    #[br(calc = ((data >> 27) & 0x3) as u8)]
235    #[bw(ignore)]
236    pub channel: u8,
237
238    #[br(calc = (data & 0x0001) != 0)]
239    #[bw(ignore)]
240    pub diffuse: bool,
241
242    #[br(calc = (data & 0x0002) != 0)]
243    #[bw(ignore)]
244    pub specular: bool,
245
246    #[br(calc = (data & 0x0004) != 0)]
247    #[bw(ignore)]
248    pub emissive: bool,
249
250    #[br(calc = (data & 0x0008) != 0)]
251    #[bw(ignore)]
252    pub scalar3: bool,
253
254    #[br(calc = (data & 0x0010) != 0)]
255    #[bw(ignore)]
256    pub metalness: bool,
257
258    #[br(calc = (data & 0x0020) != 0)]
259    #[bw(ignore)]
260    pub roughness: bool,
261
262    #[br(calc = (data & 0x0040) != 0)]
263    #[bw(ignore)]
264    pub sheen_rate: bool,
265
266    #[br(calc = (data & 0x0080) != 0)]
267    #[bw(ignore)]
268    pub sheen_tint_rate: bool,
269
270    #[br(calc = (data & 0x0100) != 0)]
271    #[bw(ignore)]
272    pub sheen_aperture: bool,
273
274    #[br(calc = (data & 0x0200) != 0)]
275    #[bw(ignore)]
276    pub anisotropy: bool,
277
278    #[br(calc = (data & 0x0400) != 0)]
279    #[bw(ignore)]
280    pub sphere_map_index: bool,
281
282    #[br(calc = (data & 0x0800) != 0)]
283    #[bw(ignore)]
284    pub sphere_map_mask: bool,
285}
286
287#[binrw]
288#[derive(Debug, Clone)]
289#[allow(dead_code)]
290pub struct LegacyColorDyeTableData {
291    #[br(count = 16)]
292    pub rows: Vec<LegacyColorDyeTableRow>,
293}
294
295#[binrw]
296#[derive(Debug, Clone)]
297#[allow(dead_code)]
298pub struct DawntrailColorDyeTableData {
299    #[br(count = 32)]
300    pub rows: Vec<DawntrailColorDyeTableRow>,
301}
302
303#[binrw]
304#[derive(Debug, Clone)]
305#[allow(dead_code)]
306pub struct OpaqueColorDyeTableData {
307    // TODO: implement
308}
309
310#[binrw]
311#[derive(Debug, Clone)]
312#[allow(dead_code)]
313pub enum ColorDyeTable {
314    LegacyColorDyeTable(LegacyColorDyeTableData),
315    DawntrailColorDyeTable(DawntrailColorDyeTableData),
316    OpaqueColorDyeTable(OpaqueColorDyeTableData),
317}
318
319#[binrw]
320#[derive(Debug, Clone, Copy)]
321#[repr(C)]
322#[allow(dead_code)]
323pub struct ShaderKey {
324    pub category: u32,
325    pub value: u32,
326}
327
328#[binrw]
329#[derive(Debug, Clone, Copy)]
330#[allow(dead_code)]
331pub struct ConstantStruct {
332    constant_id: u32,
333    value_offset: u16,
334    value_size: u16,
335}
336
337#[derive(Debug, Clone)]
338#[repr(C)]
339#[allow(dead_code)]
340pub struct Constant {
341    id: u32,
342    num_values: u32,
343    values: [f32; 4],
344}
345
346#[binrw]
347#[derive(Debug, Clone, Copy)]
348#[repr(C)]
349#[allow(dead_code)]
350pub struct Sampler {
351    /// This is a CRC hash, it can be calculated via ShaderPackage::crc
352    pub texture_usage: u32,
353    flags: u32, // TODO: unknown
354    #[brw(pad_after = 3)] // empty bytes/padding
355    texture_index: u8,
356}
357
358#[binrw::parser(reader, endian)]
359fn parse_color_table(table_dimension_logs: u8) -> BinResult<Option<ColorTable>> {
360    // NOTE: the unwrap_or_default is intentional, as some materials (like `bg/ffxiv/fst_f1/fld/common/material/f1f0_treesdw04a.mtrl`) have a color table but its not actually written.
361    Ok(Some(match table_dimension_logs {
362        0 | 0x42 => LegacyColorTable(
363            LegacyColorTableData::read_options(reader, endian, ()).unwrap_or_default(),
364        ),
365        0x53 => DawntrailColorTable(
366            DawntrailColorTableData::read_options(reader, endian, ()).unwrap_or_default(),
367        ),
368        _ => OpaqueColorTable(
369            OpaqueColorTableData::read_options(reader, endian, ()).unwrap_or_default(),
370        ),
371    }))
372}
373
374#[binrw::parser(reader, endian)]
375fn parse_color_dye_table(table_dimension_logs: u8) -> BinResult<Option<ColorDyeTable>> {
376    Ok(Some(match table_dimension_logs {
377        0 => LegacyColorDyeTable(LegacyColorDyeTableData::read_options(reader, endian, ())?),
378        0x50...0x5F => DawntrailColorDyeTable(DawntrailColorDyeTableData::read_options(
379            reader,
380            endian,
381            (),
382        )?),
383        _ => OpaqueColorDyeTable(OpaqueColorDyeTableData::read_options(reader, endian, ())?),
384    }))
385}
386
387#[binrw]
388#[derive(Debug)]
389#[allow(dead_code)]
390struct MaterialData {
391    file_header: MaterialFileHeader,
392
393    #[br(count = file_header.texture_count)]
394    offsets: Vec<u32>,
395
396    #[br(count = file_header.uv_set_count)]
397    uv_color_sets: Vec<ColorSet>,
398
399    #[br(count = file_header.color_set_count)]
400    color_sets: Vec<ColorSet>,
401
402    #[br(count = file_header.string_table_size)]
403    strings: Vec<u8>,
404
405    #[br(count = file_header.additional_data_size)]
406    additional_data: Vec<u8>,
407
408    #[bw(ignore)]
409    #[br(calc = if additional_data.len() >= 4 { u32::from_le_bytes(additional_data[0..4].try_into().unwrap()) } else { 0 })]
410    table_flags: u32,
411
412    #[br(calc = (table_flags & 0x4) != 0)]
413    #[bw(ignore)]
414    has_table: bool,
415
416    #[br(calc = (table_flags & 0x8) != 0)]
417    #[bw(ignore)]
418    has_dye_table: bool,
419
420    #[br(calc = ((table_flags >> 4) & 0xF) as u8)]
421    #[bw(ignore)]
422    table_width_log: u8,
423
424    #[br(calc = ((table_flags >> 8) & 0xF) as u8)]
425    #[bw(ignore)]
426    table_height_log: u8,
427
428    #[br(calc = (table_flags >> 4) as u8)]
429    #[bw(ignore)]
430    table_dimension_logs: u8,
431
432    #[br(calc = !has_table || table_width_log != 0 && table_height_log != 0)]
433    #[bw(ignore)]
434    is_dawntrail: bool,
435
436    #[br(if(has_table))]
437    #[br(parse_with = parse_color_table)]
438    #[br(args(table_dimension_logs))]
439    #[bw(if(*has_table))]
440    color_table: Option<ColorTable>,
441
442    #[br(if(has_dye_table))]
443    #[br(parse_with = parse_color_dye_table)]
444    #[br(args(table_dimension_logs))]
445    #[bw(if(*has_dye_table))]
446    color_dye_table: Option<ColorDyeTable>,
447
448    header: MaterialHeader,
449
450    #[br(count = header.shader_key_count)]
451    shader_keys: Vec<ShaderKey>,
452    #[br(count = header.constant_count)]
453    constants: Vec<ConstantStruct>,
454    #[br(count = header.sampler_count)]
455    samplers: Vec<Sampler>,
456    #[br(count = header.shader_value_list_size as usize / std::mem::size_of::<f32>())]
457    shader_values: Vec<f32>,
458}
459
460/// Material file, usually with the `.mtrl` file extension.
461///
462/// Contains general information about a material, such as which textures it uses.
463#[derive(Debug)]
464pub struct Material {
465    mat_data: MaterialData,
466    pub shader_package_name: String,
467    pub texture_paths: Vec<String>,
468    pub shader_keys: Vec<ShaderKey>,
469    pub constants: Vec<Constant>,
470    pub samplers: Vec<Sampler>,
471    pub color_table: Option<ColorTable>,
472    pub color_dye_table: Option<ColorDyeTable>,
473}
474
475impl ReadableFile for Material {
476    fn from_existing(platform: Platform, buffer: ByteSpan) -> Option<Material> {
477        let mut cursor = Cursor::new(buffer);
478        let mat_data = MaterialData::read_options(&mut cursor, platform.endianness(), ()).ok()?;
479
480        let mut texture_paths = vec![];
481
482        // Right now lets consider an empty string buffer to be an indicator for an invalid MTRL file.
483        if mat_data.strings.is_empty() {
484            return None;
485        }
486
487        let mut offset = 0;
488        for _ in 0..mat_data.file_header.texture_count {
489            let (s, next) = null_terminated_utf8(&mat_data.strings, offset);
490            texture_paths.push(s);
491            offset = next;
492        }
493
494        let (shader_package_name, _) = null_terminated_utf8(
495            &mat_data.strings,
496            mat_data.file_header.shader_package_name_offset as usize,
497        );
498
499        // bg/ffxiv/wil_w1/evt/w1eb/material/w1eb_f1_vfog1a.mtrl has a shader value list of 9, which doesn't make sense in this system
500        // eventually we need to un-hardcode it from vec4 or whatever
501        let mut constants = Vec::new();
502        const VALUE_SIZE: u16 = std::mem::size_of::<f32>() as u16;
503        if mat_data.header.shader_value_list_size % VALUE_SIZE == 0 {
504            for constant in &mat_data.constants {
505                let mut values: [f32; 4] = [0.0; 4];
506
507                // TODO: use mem::size_of
508                let num_floats = constant.value_size / VALUE_SIZE;
509                for (i, value) in values.iter_mut().enumerate().take(num_floats as usize) {
510                    *value = mat_data.shader_values[(constant.value_offset as usize / 4) + i];
511                }
512
513                constants.push(Constant {
514                    id: constant.constant_id,
515                    num_values: num_floats as u32,
516                    values,
517                });
518            }
519        }
520
521        let shader_keys = mat_data.shader_keys.clone();
522        let samplers = mat_data.samplers.clone();
523        let color_table = mat_data.color_table.clone();
524        let color_dye_table = mat_data.color_dye_table.clone();
525
526        Some(Material {
527            mat_data,
528            shader_package_name,
529            texture_paths,
530            shader_keys,
531            constants,
532            samplers,
533            color_table,
534            color_dye_table,
535        })
536    }
537}
538
539impl WritableFile for Material {
540    fn write_to_buffer(&self, platform: Platform) -> Option<ByteBuffer> {
541        let mut buffer = ByteBuffer::new();
542
543        {
544            let mut cursor = Cursor::new(&mut buffer);
545            self.mat_data
546                .write_options(&mut cursor, platform.endianness(), ())
547                .ok()?;
548        }
549
550        Some(buffer)
551    }
552}
553
554impl Material {
555    /// Builds a material path for a specific gear
556    pub fn gear_material_path(gear_id: i32, gear_version: i32, material_name: &str) -> String {
557        format!("chara/equipment/e{gear_id:04}/material/v{gear_version:04}{material_name}")
558    }
559
560    /// Builds a skin material path for a character
561    pub fn skin_material_path(race_code: i32, body_code: i32, material_name: &str) -> String {
562        format!(
563            "chara/human/c{race_code:04}/obj/body/b{body_code:04}/material/v0001{material_name}"
564        )
565    }
566
567    /// Builds a face material path for a character
568    pub fn face_material_path(race_code: i32, face_code: i32, material_name: &str) -> String {
569        format!("chara/human/c{race_code:04}/obj/face/f{face_code:04}/material{material_name}")
570    }
571
572    /// Builds a hair material path for a character
573    pub fn hair_material_path(race_code: i32, hair_code: i32, material_name: &str) -> String {
574        format!(
575            "chara/human/c{race_code:04}/obj/hair/h{hair_code:04}/material/v0001{material_name}"
576        )
577    }
578
579    /// Builds a ear material path for a character
580    pub fn ear_material_path(race_code: i32, ear_code: i32, material_name: &str) -> String {
581        format!("chara/human/c{race_code:04}/obj/ear/e{ear_code:04}/material/v0001{material_name}")
582    }
583
584    /// Builds a tail material path for a character
585    pub fn tail_material_path(race_code: i32, tail_code: i32, material_name: &str) -> String {
586        format!(
587            "chara/human/c{race_code:04}/obj/tail/t{tail_code:04}/material/v0001{material_name}"
588        )
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use crate::pass_random_invalid;
595
596    use super::*;
597
598    #[test]
599    fn test_invalid() {
600        pass_random_invalid::<Material>();
601    }
602}