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#[binrw]
321#[derive(Debug, Clone, Copy)]
322#[repr(C)]
323#[allow(dead_code)]
324pub struct Sampler {
325    /// This is a CRC hash, it can be calculated via ShaderPackage::crc
326    pub texture_usage: u32,
327    flags: u32, // TODO: unknown
328    texture_index: u8,
329    unknown1: u8,
330    unknown2: u8,
331    unknown3: u8,
332}
333
334#[binrw::parser(reader, endian)]
335fn parse_color_table(table_dimension_logs: u8) -> BinResult<Option<ColorTable>> {
336    Ok(Some(match table_dimension_logs {
337        0 | 0x42 => LegacyColorTable(LegacyColorTableData::read_options(reader, endian, ())?),
338        0x53 => DawntrailColorTable(DawntrailColorTableData::read_options(reader, endian, ())?),
339        _ => OpaqueColorTable(OpaqueColorTableData::read_options(reader, endian, ())?),
340    }))
341}
342
343#[binrw::parser(reader, endian)]
344fn parse_color_dye_table(table_dimension_logs: u8) -> BinResult<Option<ColorDyeTable>> {
345    Ok(Some(match table_dimension_logs {
346        0 => LegacyColorDyeTable(LegacyColorDyeTableData::read_options(reader, endian, ())?),
347        0x50...0x5F => DawntrailColorDyeTable(DawntrailColorDyeTableData::read_options(
348            reader,
349            endian,
350            (),
351        )?),
352        _ => OpaqueColorDyeTable(OpaqueColorDyeTableData::read_options(reader, endian, ())?),
353    }))
354}
355
356#[binrw]
357#[derive(Debug)]
358#[allow(dead_code)]
359#[br(little)]
360struct MaterialData {
361    file_header: MaterialFileHeader,
362
363    #[br(count = file_header.texture_count)]
364    offsets: Vec<u32>,
365
366    #[br(count = file_header.uv_set_count)]
367    uv_color_sets: Vec<ColorSet>,
368
369    #[br(count = file_header.color_set_count)]
370    color_sets: Vec<ColorSet>,
371
372    #[br(count = file_header.string_table_size)]
373    strings: Vec<u8>,
374
375    #[br(count = file_header.additional_data_size)]
376    #[br(pad_size_to = 4)]
377    #[br(map = |x: Vec<u8>| u32::from_le_bytes(x[0..4].try_into().unwrap()))]
378    table_flags: u32,
379
380    #[br(calc = (table_flags & 0x4) != 0)]
381    #[bw(ignore)]
382    has_table: bool,
383
384    #[br(calc = (table_flags & 0x8) != 0)]
385    #[bw(ignore)]
386    has_dye_table: bool,
387
388    #[br(calc = ((table_flags >> 4) & 0xF) as u8)]
389    #[bw(ignore)]
390    table_width_log: u8,
391
392    #[br(calc = ((table_flags >> 8) & 0xF) as u8)]
393    #[bw(ignore)]
394    table_height_log: u8,
395
396    #[br(calc = (table_flags >> 4) as u8)]
397    #[bw(ignore)]
398    table_dimension_logs: u8,
399
400    #[br(calc = !has_table || table_width_log != 0 && table_height_log != 0)]
401    #[bw(ignore)]
402    is_dawntrail: bool,
403
404    #[br(if(has_table))]
405    #[br(parse_with = parse_color_table)]
406    #[br(args(table_dimension_logs))]
407    #[bw(ignore)]
408    color_table: Option<ColorTable>,
409
410    #[br(if(has_dye_table))]
411    #[br(parse_with = parse_color_dye_table)]
412    #[br(args(table_dimension_logs))]
413    #[bw(ignore)]
414    color_dye_table: Option<ColorDyeTable>,
415
416    header: MaterialHeader,
417
418    #[br(count = header.shader_key_count)]
419    shader_keys: Vec<ShaderKey>,
420    #[br(count = header.constant_count)]
421    constants: Vec<ConstantStruct>,
422    #[br(count = header.sampler_count)]
423    samplers: Vec<Sampler>,
424    #[br(count = header.shader_value_list_size / 4)]
425    shader_values: Vec<f32>,
426}
427
428#[derive(Debug)]
429pub struct Material {
430    pub shader_package_name: String,
431    pub texture_paths: Vec<String>,
432    pub shader_keys: Vec<ShaderKey>,
433    pub constants: Vec<Constant>,
434    pub samplers: Vec<Sampler>,
435    pub color_table: Option<ColorTable>,
436    pub color_dye_table: Option<ColorDyeTable>,
437}
438
439impl Material {
440    pub fn from_existing(buffer: ByteSpan) -> Option<Material> {
441        let mut cursor = Cursor::new(buffer);
442        let mat_data = MaterialData::read(&mut cursor).ok()?;
443
444        let mut texture_paths = vec![];
445
446        let mut offset = 0;
447        for _ in 0..mat_data.file_header.texture_count {
448            let mut string = String::new();
449
450            let mut next_char = mat_data.strings[offset] as char;
451            while next_char != '\0' {
452                string.push(next_char);
453                offset += 1;
454                next_char = mat_data.strings[offset] as char;
455            }
456
457            texture_paths.push(string);
458
459            offset += 1;
460        }
461
462        // TODO: move to reusable function
463        let mut shader_package_name = String::new();
464
465        offset = mat_data.file_header.shader_package_name_offset as usize;
466
467        let mut next_char = mat_data.strings[offset] as char;
468        while next_char != '\0' {
469            shader_package_name.push(next_char);
470            offset += 1;
471            next_char = mat_data.strings[offset] as char;
472        }
473
474        let mut constants = Vec::new();
475        for constant in mat_data.constants {
476            let mut values: [f32; 4] = [0.0; 4];
477
478            // TODO: use mem::size_of
479            let num_floats = constant.value_size / 4;
480            for i in 0..num_floats as usize {
481                values[i] = mat_data.shader_values[(constant.value_offset as usize / 4) + i];
482            }
483
484            constants.push(Constant {
485                id: constant.constant_id,
486                num_values: num_floats as u32,
487                values,
488            });
489        }
490
491        Some(Material {
492            shader_package_name,
493            texture_paths,
494            shader_keys: mat_data.shader_keys,
495            constants,
496            samplers: mat_data.samplers,
497            color_table: mat_data.color_table,
498            color_dye_table: mat_data.color_dye_table,
499        })
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use std::fs::read;
506    use std::path::PathBuf;
507
508    use super::*;
509
510    #[test]
511    fn test_invalid() {
512        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
513        d.push("resources/tests");
514        d.push("random");
515
516        // Feeding it invalid data should not panic
517        Material::from_existing(&read(d).unwrap());
518    }
519}