use std::collections::HashSet;
use glam::{uvec4, vec3, vec4, Vec3, Vec4};
use indexmap::IndexMap;
use log::{error, warn};
use xc3_model::{
material::assignments::{Assignment, AssignmentValue, OutputAssignments},
ImageTexture, IndexMapExt,
};
use crate::{
pipeline::{Output5Type, PipelineKey},
shadergen::{
generate_alpha_test_wgsl, generate_assignments_wgsl, generate_layering_wgsl,
generate_normal_intensity_wgsl,
},
texture::create_default_black_texture,
DeviceBufferExt, MonolibShaderTextures,
};
#[derive(Debug)]
pub(crate) struct Material {
pub name: String,
pub bind_group2: crate::shader::model::bind_groups::BindGroup2,
pub pipeline_key: PipelineKey,
pub fur_shell_instance_count: Option<u32>,
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip_all)]
pub fn create_material(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipelines: &mut HashSet<PipelineKey>,
material: &xc3_model::material::Material,
textures: &[wgpu::Texture],
samplers: &[wgpu::Sampler],
image_textures: &[ImageTexture],
monolib_shader: &MonolibShaderTextures,
is_instanced_static: bool,
) -> Material {
let default_black = create_default_black_texture(device, queue)
.create_view(&wgpu::TextureViewDescriptor::default());
let default_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
min_filter: wgpu::FilterMode::Linear,
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let mut name_to_index: IndexMap<_, _> = (0..material.textures.len())
.map(|i| (format!("s{i}").into(), i))
.collect();
let material_assignments = material.output_assignments(image_textures);
let assignments = output_assignments(&material_assignments);
if let Some(a) = &material.alpha_test {
name_to_index.entry_index(format!("s{}", a.texture_index).into());
}
let assignments_wgsl = generate_assignments_wgsl(&material_assignments, &mut name_to_index);
let output_layers_wgsl = generate_layering_wgsl(&material_assignments);
let alpha_test_wgsl = material
.alpha_test
.as_ref()
.map(|a| generate_alpha_test_wgsl(a, &mut name_to_index))
.unwrap_or_default();
let normal_intensity_wgsl = material_assignments
.normal_intensity
.as_ref()
.map(|i| generate_normal_intensity_wgsl(*i))
.unwrap_or_default();
let mut texture_views: [Option<_>; 16] = std::array::from_fn(|_| None);
for (name, i) in &name_to_index {
if let Some(texture) = assign_texture(material, textures, monolib_shader, name) {
if *i < texture_views.len() {
texture_views[*i] = Some(texture.create_view(&Default::default()));
}
} else {
error!("Unable to assign texture {name:?} for {:?}", &material.name);
}
}
let fur_params = material
.fur_params
.as_ref()
.map(|p| crate::shader::model::FurShellParams {
xyz_offset: vec3(0.0, p.y_offset * p.shell_width, 0.0),
instance_count: p.instance_count as f32,
shell_width: 1.0 / (p.instance_count as f32) * p.shell_width,
alpha: (1.0 - p.alpha) / p.instance_count as f32,
})
.unwrap_or(crate::shader::model::FurShellParams {
xyz_offset: Vec3::ZERO,
instance_count: 0.0,
shell_width: 0.0,
alpha: 0.0,
});
let outline_width =
value_channel_assignment(material_assignments.outline_width.as_ref()).unwrap_or(0.005);
let per_material = device.create_uniform_buffer(
&format!(
"PerMaterial {:?} shd{:04}",
&material.name, material.technique_index
),
&[crate::shader::model::PerMaterial {
assignments,
outline_width,
fur_params,
alpha_test_ref: material.alpha_test_ref,
}],
);
let bind_group2 = crate::shader::model::bind_groups::BindGroup2::from_bindings(
device,
crate::shader::model::bind_groups::BindGroupLayout2 {
s0: texture_views[0].as_ref().unwrap_or(&default_black),
s1: texture_views[1].as_ref().unwrap_or(&default_black),
s2: texture_views[2].as_ref().unwrap_or(&default_black),
s3: texture_views[3].as_ref().unwrap_or(&default_black),
s4: texture_views[4].as_ref().unwrap_or(&default_black),
s5: texture_views[5].as_ref().unwrap_or(&default_black),
s6: texture_views[6].as_ref().unwrap_or(&default_black),
s7: texture_views[7].as_ref().unwrap_or(&default_black),
s8: texture_views[8].as_ref().unwrap_or(&default_black),
s9: texture_views[9].as_ref().unwrap_or(&default_black),
s10: texture_views[10].as_ref().unwrap_or(&default_black),
s11: texture_views[11].as_ref().unwrap_or(&default_black),
s12: texture_views[12].as_ref().unwrap_or(&default_black),
s13: texture_views[13].as_ref().unwrap_or(&default_black),
s14: texture_views[14].as_ref().unwrap_or(&default_black),
s15: texture_views[15].as_ref().unwrap_or(&default_black),
s0_sampler: material_sampler(material, samplers, 0).unwrap_or(&default_sampler),
s1_sampler: material_sampler(material, samplers, 1).unwrap_or(&default_sampler),
s2_sampler: material_sampler(material, samplers, 2).unwrap_or(&default_sampler),
s3_sampler: material_sampler(material, samplers, 3).unwrap_or(&default_sampler),
s4_sampler: material_sampler(material, samplers, 4).unwrap_or(&default_sampler),
s5_sampler: material_sampler(material, samplers, 5).unwrap_or(&default_sampler),
s6_sampler: material_sampler(material, samplers, 6).unwrap_or(&default_sampler),
s7_sampler: material_sampler(material, samplers, 7).unwrap_or(&default_sampler),
s8_sampler: material_sampler(material, samplers, 8).unwrap_or(&default_sampler),
s9_sampler: material_sampler(material, samplers, 9).unwrap_or(&default_sampler),
s10_sampler: material_sampler(material, samplers, 10).unwrap_or(&default_sampler),
s11_sampler: material_sampler(material, samplers, 11).unwrap_or(&default_sampler),
s12_sampler: material_sampler(material, samplers, 12).unwrap_or(&default_sampler),
s13_sampler: material_sampler(material, samplers, 13).unwrap_or(&default_sampler),
s14_sampler: material_sampler(material, samplers, 14).unwrap_or(&default_sampler),
alpha_test_sampler: material
.alpha_test
.as_ref()
.map(|a| a.sampler_index)
.and_then(|i| samplers.get(i))
.unwrap_or(&default_sampler),
per_material: per_material.as_entire_buffer_binding(),
},
);
let output5_type = if material_assignments.mat_id().is_some() {
if material.render_flags.specular() {
Output5Type::Specular
} else {
Output5Type::Emission
}
} else {
Output5Type::Specular
};
let pipeline_key = PipelineKey {
pass_type: material.pass_type,
flags: material.state_flags,
is_outline: material.name.ends_with("_outline"),
output5_type,
is_instanced_static,
assignments_wgsl,
output_layers_wgsl,
alpha_test_wgsl,
normal_intensity_wgsl,
};
pipelines.insert(pipeline_key.clone());
Material {
name: material.name.clone(),
bind_group2,
pipeline_key,
fur_shell_instance_count: material.fur_params.as_ref().map(|p| p.instance_count),
}
}
fn output_assignments(
assignments: &OutputAssignments,
) -> [crate::shader::model::OutputAssignment; 6] {
[0, 1, 2, 3, 4, 5].map(|i| {
let assignment = &assignments.output_assignments[i];
crate::shader::model::OutputAssignment {
has_channels: uvec4(
has_value(&assignments.assignments, assignment.x) as u32,
has_value(&assignments.assignments, assignment.y) as u32,
has_value(&assignments.assignments, assignment.z) as u32,
has_value(&assignments.assignments, assignment.w) as u32,
),
default_value: output_default(i),
}
})
}
fn has_value(assignments: &[Assignment], i: Option<usize>) -> bool {
if let Some(i) = i {
match &assignments[i] {
Assignment::Value(c) => c.is_some(),
Assignment::Func { args, .. } => args.iter().any(|a| has_value(assignments, Some(*a))),
}
} else {
false
}
}
fn value_channel_assignment(assignment: Option<&AssignmentValue>) -> Option<f32> {
if let Some(AssignmentValue::Float(f)) = assignment {
Some(f.0)
} else {
None
}
}
fn create_bit_info(
mat_id: u32,
mat_flag: bool,
hatching_flag: bool,
specular_col: bool,
ssr: bool,
) -> f32 {
let n_val = mat_id
| ((ssr as u32) << 3)
| ((specular_col as u32) << 4)
| ((mat_flag as u32) << 5)
| ((hatching_flag as u32) << 6);
(n_val as f32 + 0.1) / 255.0
}
fn output_default(i: usize) -> Vec4 {
let etc_flags = create_bit_info(2, false, false, true, false);
let output_defaults: [Vec4; 6] = [
Vec4::ONE,
Vec4::new(0.0, 0.0, 0.0, etc_flags),
Vec4::new(0.5, 0.5, 1.0, 0.0),
Vec4::ZERO,
Vec4::new(1.0, 1.0, 1.0, 0.0),
Vec4::ZERO,
];
vec4(
output_defaults[i][0],
output_defaults[i][1],
output_defaults[i][2],
output_defaults[i][3],
)
}
fn assign_texture<'a>(
material: &xc3_model::material::Material,
textures: &'a [wgpu::Texture],
monolib_shader: &'a MonolibShaderTextures,
name: &str,
) -> Option<&'a wgpu::Texture> {
let texture = match material_texture_index(name) {
Some(texture_index) => {
let image_texture_index = material.textures.get(texture_index)?.image_texture_index;
textures.get(image_texture_index)
}
None => {
monolib_shader.global_texture(name)
}
}?;
if texture.dimension() == wgpu::TextureDimension::D2 && texture.depth_or_array_layers() == 1 {
Some(texture)
} else {
error!(
"Expected 2D texture but found dimension {:?} and {} layers.",
texture.dimension(),
texture.depth_or_array_layers()
);
None
}
}
fn material_texture_index(sampler_name: &str) -> Option<usize> {
sampler_name.strip_prefix('s')?.parse().ok()
}
fn material_sampler<'a>(
material: &xc3_model::material::Material,
samplers: &'a [wgpu::Sampler],
index: usize,
) -> Option<&'a wgpu::Sampler> {
material
.textures
.get(index)
.and_then(|texture| samplers.get(texture.sampler_index))
}