xc3_wgpu/
material.rs

1use std::collections::HashSet;
2
3use glam::{uvec4, vec3, vec4, Vec3, Vec4};
4use indexmap::IndexMap;
5use log::{error, warn};
6use xc3_model::{
7    material::assignments::{Assignment, AssignmentValue, OutputAssignments},
8    ImageTexture, IndexMapExt,
9};
10
11use crate::{
12    pipeline::{Output5Type, PipelineKey},
13    shadergen::{
14        generate_alpha_test_wgsl, generate_assignments_wgsl, generate_layering_wgsl,
15        generate_normal_intensity_wgsl,
16    },
17    texture::create_default_black_texture,
18    DeviceBufferExt, MonolibShaderTextures,
19};
20
21#[derive(Debug)]
22pub(crate) struct Material {
23    pub name: String,
24    pub bind_group2: crate::shader::model::bind_groups::BindGroup2,
25
26    // The material flags may require a separate pipeline per material.
27    // We only store a key here to allow caching.
28    pub pipeline_key: PipelineKey,
29
30    pub fur_shell_instance_count: Option<u32>,
31}
32
33#[allow(clippy::too_many_arguments)]
34#[tracing::instrument(skip_all)]
35pub fn create_material(
36    device: &wgpu::Device,
37    queue: &wgpu::Queue,
38    pipelines: &mut HashSet<PipelineKey>,
39    material: &xc3_model::material::Material,
40    textures: &[wgpu::Texture],
41    samplers: &[wgpu::Sampler],
42    image_textures: &[ImageTexture],
43    monolib_shader: &MonolibShaderTextures,
44    is_instanced_static: bool,
45) -> Material {
46    // TODO: Is there a better way to handle missing textures?
47    let default_black = create_default_black_texture(device, queue)
48        .create_view(&wgpu::TextureViewDescriptor::default());
49
50    let default_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
51        address_mode_u: wgpu::AddressMode::Repeat,
52        address_mode_v: wgpu::AddressMode::Repeat,
53        min_filter: wgpu::FilterMode::Linear,
54        mag_filter: wgpu::FilterMode::Linear,
55        ..Default::default()
56    });
57
58    // Assign material textures by index to make GPU debugging easier.
59    // TODO: Match the ordering in the actual in game shader using technique?
60    let mut name_to_index: IndexMap<_, _> = (0..material.textures.len())
61        .map(|i| (format!("s{i}").into(), i))
62        .collect();
63
64    let material_assignments = material.output_assignments(image_textures);
65    let assignments = output_assignments(&material_assignments);
66
67    // Alpha textures might not be used in normal shaders.
68    if let Some(a) = &material.alpha_test {
69        name_to_index.entry_index(format!("s{}", a.texture_index).into());
70    }
71
72    let assignments_wgsl = generate_assignments_wgsl(&material_assignments, &mut name_to_index);
73    let output_layers_wgsl = generate_layering_wgsl(&material_assignments);
74
75    // Generate empty code if alpha testing is disabled.
76    let alpha_test_wgsl = material
77        .alpha_test
78        .as_ref()
79        .map(|a| generate_alpha_test_wgsl(a, &mut name_to_index))
80        .unwrap_or_default();
81
82    let normal_intensity_wgsl = material_assignments
83        .normal_intensity
84        .as_ref()
85        .map(|i| generate_normal_intensity_wgsl(*i))
86        .unwrap_or_default();
87
88    let mut texture_views: [Option<_>; 16] = std::array::from_fn(|_| None);
89
90    for (name, i) in &name_to_index {
91        if let Some(texture) = assign_texture(material, textures, monolib_shader, name) {
92            if *i < texture_views.len() {
93                texture_views[*i] = Some(texture.create_view(&Default::default()));
94            }
95        } else {
96            error!("Unable to assign texture {name:?} for {:?}", &material.name);
97        }
98    }
99
100    // Use similar calculated parameter values as in game vertex shaders.
101    let fur_params = material
102        .fur_params
103        .as_ref()
104        .map(|p| crate::shader::model::FurShellParams {
105            xyz_offset: vec3(0.0, p.y_offset * p.shell_width, 0.0),
106            instance_count: p.instance_count as f32,
107            shell_width: 1.0 / (p.instance_count as f32) * p.shell_width,
108            alpha: (1.0 - p.alpha) / p.instance_count as f32,
109        })
110        .unwrap_or(crate::shader::model::FurShellParams {
111            xyz_offset: Vec3::ZERO,
112            instance_count: 0.0,
113            shell_width: 0.0,
114            alpha: 0.0,
115        });
116
117    // TODO: What is a good default outline width?
118    let outline_width =
119        value_channel_assignment(material_assignments.outline_width.as_ref()).unwrap_or(0.005);
120
121    // TODO: This is normally done using a depth prepass.
122    // TODO: Is it ok to combine the prepass alpha in the main pass like this?
123    let per_material = device.create_uniform_buffer(
124        // TODO: include model name?
125        &format!(
126            "PerMaterial {:?} shd{:04}",
127            &material.name, material.technique_index
128        ),
129        &[crate::shader::model::PerMaterial {
130            assignments,
131            outline_width,
132            fur_params,
133            alpha_test_ref: material.alpha_test_ref,
134        }],
135    );
136
137    // Bind all available textures and samplers.
138    // Texture selection happens within generated shader code.
139    // Any unused shader code will likely be removed during shader compilation.
140    let bind_group2 = crate::shader::model::bind_groups::BindGroup2::from_bindings(
141        device,
142        crate::shader::model::bind_groups::BindGroupLayout2 {
143            s0: texture_views[0].as_ref().unwrap_or(&default_black),
144            s1: texture_views[1].as_ref().unwrap_or(&default_black),
145            s2: texture_views[2].as_ref().unwrap_or(&default_black),
146            s3: texture_views[3].as_ref().unwrap_or(&default_black),
147            s4: texture_views[4].as_ref().unwrap_or(&default_black),
148            s5: texture_views[5].as_ref().unwrap_or(&default_black),
149            s6: texture_views[6].as_ref().unwrap_or(&default_black),
150            s7: texture_views[7].as_ref().unwrap_or(&default_black),
151            s8: texture_views[8].as_ref().unwrap_or(&default_black),
152            s9: texture_views[9].as_ref().unwrap_or(&default_black),
153            s10: texture_views[10].as_ref().unwrap_or(&default_black),
154            s11: texture_views[11].as_ref().unwrap_or(&default_black),
155            s12: texture_views[12].as_ref().unwrap_or(&default_black),
156            s13: texture_views[13].as_ref().unwrap_or(&default_black),
157            s14: texture_views[14].as_ref().unwrap_or(&default_black),
158            s15: texture_views[15].as_ref().unwrap_or(&default_black),
159            s0_sampler: material_sampler(material, samplers, 0).unwrap_or(&default_sampler),
160            s1_sampler: material_sampler(material, samplers, 1).unwrap_or(&default_sampler),
161            s2_sampler: material_sampler(material, samplers, 2).unwrap_or(&default_sampler),
162            s3_sampler: material_sampler(material, samplers, 3).unwrap_or(&default_sampler),
163            s4_sampler: material_sampler(material, samplers, 4).unwrap_or(&default_sampler),
164            s5_sampler: material_sampler(material, samplers, 5).unwrap_or(&default_sampler),
165            s6_sampler: material_sampler(material, samplers, 6).unwrap_or(&default_sampler),
166            s7_sampler: material_sampler(material, samplers, 7).unwrap_or(&default_sampler),
167            s8_sampler: material_sampler(material, samplers, 8).unwrap_or(&default_sampler),
168            s9_sampler: material_sampler(material, samplers, 9).unwrap_or(&default_sampler),
169            s10_sampler: material_sampler(material, samplers, 10).unwrap_or(&default_sampler),
170            s11_sampler: material_sampler(material, samplers, 11).unwrap_or(&default_sampler),
171            s12_sampler: material_sampler(material, samplers, 12).unwrap_or(&default_sampler),
172            s13_sampler: material_sampler(material, samplers, 13).unwrap_or(&default_sampler),
173            s14_sampler: material_sampler(material, samplers, 14).unwrap_or(&default_sampler),
174            // TODO: Move alpha test to a separate pass?
175            // s15_sampler: material_sampler(material, samplers, 15).unwrap_or(&default_sampler),
176            alpha_test_sampler: material
177                .alpha_test
178                .as_ref()
179                .map(|a| a.sampler_index)
180                .and_then(|i| samplers.get(i))
181                .unwrap_or(&default_sampler),
182            per_material: per_material.as_entire_buffer_binding(),
183        },
184    );
185
186    // Toon and hair materials seem to always use specular.
187    // TODO: Is there a more reliable way to check this?
188    // TODO: Is any frag shader with 7 outputs using specular?
189    // TODO: Something in the wimdo matches up with shader outputs?
190    // TODO: unk12-14 in material render flags?
191    let output5_type = if material_assignments.mat_id().is_some() {
192        if material.render_flags.specular() {
193            Output5Type::Specular
194        } else {
195            // TODO: This case isn't always accurate.
196            Output5Type::Emission
197        }
198    } else {
199        // TODO: Set better defaults for xcx models?
200        Output5Type::Specular
201    };
202
203    // TODO: How to make sure the pipeline outputs match the render pass?
204    // Each material only goes in exactly one pass?
205    // TODO: Is it redundant to also store the unk type?
206    // TODO: Find a more accurate way to detect outline shaders.
207    let pipeline_key = PipelineKey {
208        pass_type: material.pass_type,
209        flags: material.state_flags,
210        is_outline: material.name.ends_with("_outline"),
211        output5_type,
212        is_instanced_static,
213        assignments_wgsl,
214        output_layers_wgsl,
215        alpha_test_wgsl,
216        normal_intensity_wgsl,
217    };
218    pipelines.insert(pipeline_key.clone());
219
220    Material {
221        name: material.name.clone(),
222        bind_group2,
223        pipeline_key,
224        fur_shell_instance_count: material.fur_params.as_ref().map(|p| p.instance_count),
225    }
226}
227
228fn output_assignments(
229    assignments: &OutputAssignments,
230) -> [crate::shader::model::OutputAssignment; 6] {
231    [0, 1, 2, 3, 4, 5].map(|i| {
232        let assignment = &assignments.output_assignments[i];
233
234        crate::shader::model::OutputAssignment {
235            has_channels: uvec4(
236                has_value(&assignments.assignments, assignment.x) as u32,
237                has_value(&assignments.assignments, assignment.y) as u32,
238                has_value(&assignments.assignments, assignment.z) as u32,
239                has_value(&assignments.assignments, assignment.w) as u32,
240            ),
241            default_value: output_default(i),
242        }
243    })
244}
245
246fn has_value(assignments: &[Assignment], i: Option<usize>) -> bool {
247    if let Some(i) = i {
248        match &assignments[i] {
249            Assignment::Value(c) => c.is_some(),
250            Assignment::Func { args, .. } => args.iter().any(|a| has_value(assignments, Some(*a))),
251        }
252    } else {
253        false
254    }
255}
256
257fn value_channel_assignment(assignment: Option<&AssignmentValue>) -> Option<f32> {
258    if let Some(AssignmentValue::Float(f)) = assignment {
259        Some(f.0)
260    } else {
261        None
262    }
263}
264
265fn create_bit_info(
266    mat_id: u32,
267    mat_flag: bool,
268    hatching_flag: bool,
269    specular_col: bool,
270    ssr: bool,
271) -> f32 {
272    // Adapted from xeno3/chr/ch/ch11021013.pcsmt, shd00036, createBitInfo,
273    let n_val = mat_id
274        | ((ssr as u32) << 3)
275        | ((specular_col as u32) << 4)
276        | ((mat_flag as u32) << 5)
277        | ((hatching_flag as u32) << 6);
278    (n_val as f32 + 0.1) / 255.0
279}
280
281fn output_default(i: usize) -> Vec4 {
282    // TODO: Create a special ID for unrecognized materials instead of toon?
283    let etc_flags = create_bit_info(2, false, false, true, false);
284
285    // Choose defaults that have as close to no effect as possible.
286    // TODO: Make a struct for this instead?
287    // TODO: Move these defaults to xc3_model?
288    let output_defaults: [Vec4; 6] = [
289        Vec4::ONE,
290        Vec4::new(0.0, 0.0, 0.0, etc_flags),
291        Vec4::new(0.5, 0.5, 1.0, 0.0),
292        Vec4::ZERO,
293        Vec4::new(1.0, 1.0, 1.0, 0.0),
294        Vec4::ZERO,
295    ];
296
297    vec4(
298        output_defaults[i][0],
299        output_defaults[i][1],
300        output_defaults[i][2],
301        output_defaults[i][3],
302    )
303}
304
305fn assign_texture<'a>(
306    material: &xc3_model::material::Material,
307    textures: &'a [wgpu::Texture],
308    monolib_shader: &'a MonolibShaderTextures,
309    name: &str,
310) -> Option<&'a wgpu::Texture> {
311    let texture = match material_texture_index(name) {
312        Some(texture_index) => {
313            // Search the material textures like "s0" or "s3".
314            // TODO: Why is this sometimes out of range for XC2 maps?
315            let image_texture_index = material.textures.get(texture_index)?.image_texture_index;
316            textures.get(image_texture_index)
317        }
318        None => {
319            // Search global textures from monolib/shader like "gTResidentTex44".
320            monolib_shader.global_texture(name)
321        }
322    }?;
323
324    // TODO: How to handle 3D textures and cube maps within the shader?
325    if texture.dimension() == wgpu::TextureDimension::D2 && texture.depth_or_array_layers() == 1 {
326        Some(texture)
327    } else {
328        error!(
329            "Expected 2D texture but found dimension {:?} and {} layers.",
330            texture.dimension(),
331            texture.depth_or_array_layers()
332        );
333        None
334    }
335}
336
337fn material_texture_index(sampler_name: &str) -> Option<usize> {
338    // Convert names like "s3" to index 3.
339    // Materials always use this naming convention in the shader.
340    // Xenoblade 1 DE uses up to 14 material samplers.
341    sampler_name.strip_prefix('s')?.parse().ok()
342}
343
344fn material_sampler<'a>(
345    material: &xc3_model::material::Material,
346    samplers: &'a [wgpu::Sampler],
347    index: usize,
348) -> Option<&'a wgpu::Sampler> {
349    // TODO: Why is this sometimes out of range for XC2 maps?
350    material
351        .textures
352        .get(index)
353        .and_then(|texture| samplers.get(texture.sampler_index))
354}