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::ByteSpan;
9use crate::common_file_operations::{Half1, Half2, Half3};
10use crate::mtrl::ColorDyeTable::{
11    DawntrailColorDyeTable, LegacyColorDyeTable, OpaqueColorDyeTable,
12};
13use crate::mtrl::ColorTable::{DawntrailColorTable, LegacyColorTable, OpaqueColorTable};
14use binrw::{BinRead, BinResult, binread, binrw};
15
16#[binrw]
17#[derive(Debug)]
18#[allow(dead_code)]
19struct MaterialFileHeader {
20    version: u32,
21    file_size: u16,
22    data_set_size: u16,
23    string_table_size: u16,
24    shader_package_name_offset: u16,
25    texture_count: u8,
26    uv_set_count: u8,
27    color_set_count: u8,
28    additional_data_size: u8,
29}
30
31#[binrw]
32#[derive(Debug)]
33struct MaterialHeader {
34    shader_value_list_size: u16,
35    shader_key_count: u16,
36    constant_count: u16,
37    sampler_count: u16,
38    flags: u32,
39}
40
41#[binrw]
42#[derive(Debug)]
43#[allow(dead_code)]
44struct ColorSet {
45    name_offset: u16,
46    index: u16,
47}
48
49#[binread]
50#[derive(Debug, Clone, Copy)]
51#[repr(C)]
52#[allow(dead_code)]
53pub struct DawntrailColorTableRow {
54    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
55    pub diffuse_color: [f32; 3],
56
57    #[br(map = |x: Half1| { x.value.to_f32() })]
58    pub unknown1: f32,
59
60    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
61    pub specular_color: [f32; 3],
62
63    #[br(map = |x: Half1| { x.value.to_f32() })]
64    pub unknown2: f32,
65
66    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
67    pub emissive_color: [f32; 3],
68
69    #[br(map = |x: Half1| { x.value.to_f32() })]
70    pub unknown3: f32,
71
72    #[br(map = |x: Half1| { x.value.to_f32() })]
73    pub sheen_rate: f32,
74
75    #[br(map = |x: Half1| { x.value.to_f32() })]
76    pub sheen_tint: f32,
77
78    #[br(map = |x: Half1| { x.value.to_f32() })]
79    pub sheen_aperture: f32,
80
81    #[br(map = |x: Half1| { x.value.to_f32() })]
82    pub unknown4: f32,
83
84    #[br(map = |x: Half1| { x.value.to_f32() })]
85    pub roughness: f32,
86
87    #[br(map = |x: Half1| { x.value.to_f32() })]
88    pub unknown5: f32,
89
90    #[br(map = |x: Half1| { x.value.to_f32() })]
91    pub metalness: f32,
92
93    #[br(map = |x: Half1| { x.value.to_f32() })]
94    pub anisotropy: f32,
95
96    #[br(map = |x: Half1| { x.value.to_f32() })]
97    pub unknown6: f32,
98
99    #[br(map = |x: Half1| { x.value.to_f32() })]
100    pub sphere_mask: f32,
101
102    #[br(map = |x: Half1| { x.value.to_f32() })]
103    pub unknown7: f32,
104
105    #[br(map = |x: Half1| { x.value.to_f32() })]
106    pub unknown8: f32,
107
108    pub shader_index: u16,
109
110    pub tile_set: u16,
111
112    #[br(map = |x: Half1| { x.value.to_f32() })]
113    pub tile_alpha: f32,
114
115    pub sphere_index: u16,
116
117    #[br(map = |x: Half2| { [x.x.to_f32(), x.y.to_f32()] })]
118    pub material_repeat: [f32; 2],
119
120    #[br(map = |x: Half2| { [x.x.to_f32(), x.y.to_f32()] })]
121    pub material_skew: [f32; 2],
122}
123
124#[binread]
125#[derive(Debug, Clone, Copy)]
126#[repr(C)]
127#[allow(dead_code)]
128pub struct LegacyColorTableRow {
129    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
130    pub diffuse_color: [f32; 3],
131
132    #[br(map = |x: Half1| { x.value.to_f32() })]
133    pub specular_strength: f32,
134
135    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
136    pub specular_color: [f32; 3],
137
138    #[br(map = |x: Half1| { x.value.to_f32() })]
139    pub gloss_strength: f32,
140
141    #[br(map = |x: Half3| { [x.r.to_f32(), x.g.to_f32(), x.b.to_f32()] })]
142    pub emissive_color: [f32; 3],
143
144    pub tile_set: u16,
145
146    #[br(map = |x: Half2| { [x.x.to_f32(), x.y.to_f32()] })]
147    pub material_repeat: [f32; 2],
148
149    #[br(map = |x: Half2| { [x.x.to_f32(), x.y.to_f32()] })]
150    pub material_skew: [f32; 2],
151}
152
153#[binread]
154#[derive(Debug)]
155#[allow(dead_code)]
156pub struct LegacyColorTableData {
157    #[br(count = 16)]
158    pub rows: Vec<LegacyColorTableRow>,
159}
160
161#[binread]
162#[derive(Debug)]
163#[allow(dead_code)]
164pub struct DawntrailColorTableData {
165    #[br(count = 32)]
166    pub rows: Vec<DawntrailColorTableRow>,
167}
168
169#[binread]
170#[derive(Debug)]
171#[allow(dead_code)]
172pub struct OpaqueColorTableData {
173    // TODO: Support
174}
175
176#[binread]
177#[derive(Debug)]
178#[allow(dead_code)]
179pub enum ColorTable {
180    LegacyColorTable(LegacyColorTableData),
181    DawntrailColorTable(DawntrailColorTableData),
182    OpaqueColorTable(OpaqueColorTableData),
183}
184
185#[binread]
186#[derive(Debug)]
187#[allow(dead_code)]
188pub struct LegacyColorDyeTableRow {
189    #[br(temp)]
190    data: u16,
191
192    #[br(calc = data >> 5)]
193    pub template: u16,
194
195    #[br(calc = (data & 0x01) != 0)]
196    pub diffuse: bool,
197
198    #[br(calc = (data & 0x02) != 0)]
199    pub specular: bool,
200
201    #[br(calc = (data & 0x04) != 0)]
202    pub emissive: bool,
203
204    #[br(calc = (data & 0x08) != 0)]
205    pub gloss: bool,
206
207    #[br(calc = (data & 0x10) != 0)]
208    pub specular_strength: bool,
209}
210
211#[binread]
212#[derive(Debug)]
213#[allow(dead_code)]
214pub struct DawntrailColorDyeTableRow {
215    #[br(temp)]
216    data: u32,
217
218    #[br(calc = ((data >> 16) & 0x7FF) as u16)]
219    pub template: u16,
220
221    #[br(calc = ((data >> 27) & 0x3) as u8)]
222    pub channel: u8,
223
224    #[br(calc = (data & 0x0001) != 0)]
225    pub diffuse: bool,
226
227    #[br(calc = (data & 0x0002) != 0)]
228    pub specular: bool,
229
230    #[br(calc = (data & 0x0004) != 0)]
231    pub emissive: bool,
232
233    #[br(calc = (data & 0x0008) != 0)]
234    pub scalar3: bool,
235
236    #[br(calc = (data & 0x0010) != 0)]
237    pub metalness: bool,
238
239    #[br(calc = (data & 0x0020) != 0)]
240    pub roughness: bool,
241
242    #[br(calc = (data & 0x0040) != 0)]
243    pub sheen_rate: bool,
244
245    #[br(calc = (data & 0x0080) != 0)]
246    pub sheen_tint_rate: bool,
247
248    #[br(calc = (data & 0x0100) != 0)]
249    pub sheen_aperture: bool,
250
251    #[br(calc = (data & 0x0200) != 0)]
252    pub anisotropy: bool,
253
254    #[br(calc = (data & 0x0400) != 0)]
255    pub sphere_map_index: bool,
256
257    #[br(calc = (data & 0x0800) != 0)]
258    pub sphere_map_mask: bool,
259}
260
261#[binread]
262#[derive(Debug)]
263#[allow(dead_code)]
264pub struct LegacyColorDyeTableData {
265    #[br(count = 16)]
266    pub rows: Vec<LegacyColorDyeTableRow>,
267}
268
269#[binread]
270#[derive(Debug)]
271#[allow(dead_code)]
272pub struct DawntrailColorDyeTableData {
273    #[br(count = 32)]
274    pub rows: Vec<DawntrailColorDyeTableRow>,
275}
276
277#[binread]
278#[derive(Debug)]
279#[allow(dead_code)]
280pub struct OpaqueColorDyeTableData {
281    // TODO: implement
282}
283
284#[binread]
285#[derive(Debug)]
286#[allow(dead_code)]
287pub enum ColorDyeTable {
288    LegacyColorDyeTable(LegacyColorDyeTableData),
289    DawntrailColorDyeTable(DawntrailColorDyeTableData),
290    OpaqueColorDyeTable(OpaqueColorDyeTableData),
291}
292
293#[binrw]
294#[derive(Debug, Clone, Copy)]
295#[repr(C)]
296#[allow(dead_code)]
297pub struct ShaderKey {
298    pub category: u32,
299    pub value: u32,
300}
301
302#[binrw]
303#[derive(Debug, Clone, Copy)]
304#[allow(dead_code)]
305pub struct ConstantStruct {
306    constant_id: u32,
307    value_offset: u16,
308    value_size: u16,
309}
310
311#[derive(Debug, Clone)]
312#[repr(C)]
313#[allow(dead_code)]
314pub struct Constant {
315    id: u32,
316    num_values: u32,
317    values: [f32; 4],
318}
319
320// from https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Parsing/MtrlStructs.cs
321#[binrw]
322#[repr(u8)]
323#[derive(Debug, Clone, Copy)]
324pub enum TextureUsage {
325    #[brw(magic = 0x88408C04u32)]
326    Sampler,
327    #[brw(magic = 0x213CB439u32)]
328    Sampler0,
329    #[brw(magic = 0x563B84AFu32)]
330    Sampler1,
331    #[brw(magic = 0xFEA0F3D2u32)]
332    SamplerCatchlight,
333    #[brw(magic = 0x1E6FEF9Cu32)]
334    SamplerColorMap0,
335    #[brw(magic = 0x6968DF0Au32)]
336    SamplerColorMap1,
337    #[brw(magic = 0x115306BEu32)]
338    SamplerDiffuse,
339    #[brw(magic = 0xF8D7957Au32)]
340    SamplerEnvMap,
341    #[brw(magic = 0x8A4E82B6u32)]
342    SamplerMask,
343    #[brw(magic = 0x0C5EC1F1u32)]
344    SamplerNormal,
345    #[brw(magic = 0xAAB4D9E9u32)]
346    SamplerNormalMap0,
347    #[brw(magic = 0xDDB3E97Fu32)]
348    SamplerNormalMap1,
349    #[brw(magic = 0x87F6474Du32)]
350    SamplerReflection,
351    #[brw(magic = 0x2B99E025u32)]
352    SamplerSpecular,
353    #[brw(magic = 0x1BBC2F12u32)]
354    SamplerSpecularMap0,
355    #[brw(magic = 0x6CBB1F84u32)]
356    SamplerSpecularMap1,
357    #[brw(magic = 0xE6321AFCu32)]
358    SamplerWaveMap,
359    #[brw(magic = 0x574E22D6u32)]
360    SamplerWaveletMap0,
361    #[brw(magic = 0x20491240u32)]
362    SamplerWaveletMap1,
363    #[brw(magic = 0x95E1F64Du32)]
364    SamplerWhitecapMap,
365
366    #[brw(magic = 0x565f8fd8u32)]
367    UnknownDawntrail1,
368
369    #[brw(magic = 0xe5338c17u32)]
370    UnknownDawntrail2,
371}
372
373#[binrw]
374#[derive(Debug, Clone, Copy)]
375#[repr(C)]
376#[allow(dead_code)]
377pub struct Sampler {
378    texture_usage: TextureUsage,
379    flags: u32, // TODO: unknown
380    texture_index: u8,
381    unknown1: u8,
382    unknown2: u8,
383    unknown3: u8,
384}
385
386#[binrw::parser(reader, endian)]
387fn parse_color_table(table_dimension_logs: u8) -> BinResult<Option<ColorTable>> {
388    Ok(Some(match table_dimension_logs {
389        0 | 0x42 => LegacyColorTable(LegacyColorTableData::read_options(reader, endian, ())?),
390        0x53 => DawntrailColorTable(DawntrailColorTableData::read_options(reader, endian, ())?),
391        _ => OpaqueColorTable(OpaqueColorTableData::read_options(reader, endian, ())?),
392    }))
393}
394
395#[binrw::parser(reader, endian)]
396fn parse_color_dye_table(table_dimension_logs: u8) -> BinResult<Option<ColorDyeTable>> {
397    Ok(Some(match table_dimension_logs {
398        0 => LegacyColorDyeTable(LegacyColorDyeTableData::read_options(reader, endian, ())?),
399        0x50...0x5F => DawntrailColorDyeTable(DawntrailColorDyeTableData::read_options(
400            reader,
401            endian,
402            (),
403        )?),
404        _ => OpaqueColorDyeTable(OpaqueColorDyeTableData::read_options(reader, endian, ())?),
405    }))
406}
407
408#[binrw]
409#[derive(Debug)]
410#[allow(dead_code)]
411#[br(little)]
412struct MaterialData {
413    file_header: MaterialFileHeader,
414
415    #[br(count = file_header.texture_count)]
416    offsets: Vec<u32>,
417
418    #[br(count = file_header.uv_set_count)]
419    uv_color_sets: Vec<ColorSet>,
420
421    #[br(count = file_header.color_set_count)]
422    color_sets: Vec<ColorSet>,
423
424    #[br(count = file_header.string_table_size)]
425    strings: Vec<u8>,
426
427    #[br(count = file_header.additional_data_size)]
428    #[br(pad_size_to = 4)]
429    #[br(map = |x: Vec<u8>| u32::from_le_bytes(x[0..4].try_into().unwrap()))]
430    table_flags: u32,
431
432    #[br(calc = (table_flags & 0x4) != 0)]
433    #[bw(ignore)]
434    has_table: bool,
435
436    #[br(calc = (table_flags & 0x8) != 0)]
437    #[bw(ignore)]
438    has_dye_table: bool,
439
440    #[br(calc = ((table_flags >> 4) & 0xF) as u8)]
441    #[bw(ignore)]
442    table_width_log: u8,
443
444    #[br(calc = ((table_flags >> 8) & 0xF) as u8)]
445    #[bw(ignore)]
446    table_height_log: u8,
447
448    #[br(calc = (table_flags >> 4) as u8)]
449    #[bw(ignore)]
450    table_dimension_logs: u8,
451
452    #[br(calc = !has_table || table_width_log != 0 && table_height_log != 0)]
453    #[bw(ignore)]
454    is_dawntrail: bool,
455
456    #[br(if(has_table))]
457    #[br(parse_with = parse_color_table)]
458    #[br(args(table_dimension_logs))]
459    #[bw(ignore)]
460    color_table: Option<ColorTable>,
461
462    #[br(if(has_dye_table))]
463    #[br(parse_with = parse_color_dye_table)]
464    #[br(args(table_dimension_logs))]
465    #[bw(ignore)]
466    color_dye_table: Option<ColorDyeTable>,
467
468    header: MaterialHeader,
469
470    #[br(count = header.shader_key_count)]
471    shader_keys: Vec<ShaderKey>,
472    #[br(count = header.constant_count)]
473    constants: Vec<ConstantStruct>,
474    #[br(count = header.sampler_count)]
475    samplers: Vec<Sampler>,
476    #[br(count = header.shader_value_list_size / 4)]
477    shader_values: Vec<f32>,
478}
479
480#[derive(Debug)]
481pub struct Material {
482    pub shader_package_name: String,
483    pub texture_paths: Vec<String>,
484    pub shader_keys: Vec<ShaderKey>,
485    pub constants: Vec<Constant>,
486    pub samplers: Vec<Sampler>,
487    pub color_table: Option<ColorTable>,
488    pub color_dye_table: Option<ColorDyeTable>,
489}
490
491impl Material {
492    pub fn from_existing(buffer: ByteSpan) -> Option<Material> {
493        let mut cursor = Cursor::new(buffer);
494        let mat_data = MaterialData::read(&mut cursor).ok()?;
495
496        let mut texture_paths = vec![];
497
498        let mut offset = 0;
499        for _ in 0..mat_data.file_header.texture_count {
500            let mut string = String::new();
501
502            let mut next_char = mat_data.strings[offset] as char;
503            while next_char != '\0' {
504                string.push(next_char);
505                offset += 1;
506                next_char = mat_data.strings[offset] as char;
507            }
508
509            texture_paths.push(string);
510
511            offset += 1;
512        }
513
514        // TODO: move to reusable function
515        let mut shader_package_name = String::new();
516
517        offset = mat_data.file_header.shader_package_name_offset as usize;
518
519        let mut next_char = mat_data.strings[offset] as char;
520        while next_char != '\0' {
521            shader_package_name.push(next_char);
522            offset += 1;
523            next_char = mat_data.strings[offset] as char;
524        }
525
526        let mut constants = Vec::new();
527        for constant in mat_data.constants {
528            let mut values: [f32; 4] = [0.0; 4];
529
530            // TODO: use mem::size_of
531            let num_floats = constant.value_size / 4;
532            for i in 0..num_floats as usize {
533                values[i] = mat_data.shader_values[(constant.value_offset as usize / 4) + i];
534            }
535
536            constants.push(Constant {
537                id: constant.constant_id,
538                num_values: num_floats as u32,
539                values,
540            });
541        }
542
543        Some(Material {
544            shader_package_name,
545            texture_paths,
546            shader_keys: mat_data.shader_keys,
547            constants,
548            samplers: mat_data.samplers,
549            color_table: mat_data.color_table,
550            color_dye_table: mat_data.color_dye_table,
551        })
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use std::fs::read;
558    use std::path::PathBuf;
559
560    use super::*;
561
562    #[test]
563    fn test_invalid() {
564        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
565        d.push("resources/tests");
566        d.push("random");
567
568        // Feeding it invalid data should not panic
569        Material::from_existing(&read(d).unwrap());
570    }
571}