xc3_wgpu/
material.rs

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