use super::*;
use super::images::GpuImage;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct SamplerKey {
wrap_u: wgpu::AddressMode,
wrap_v: wgpu::AddressMode,
mag: wgpu::FilterMode,
min: wgpu::FilterMode,
}
impl SamplerKey {
const DEFAULT: SamplerKey = SamplerKey {
wrap_u: wgpu::AddressMode::Repeat,
wrap_v: wgpu::AddressMode::Repeat,
mag: wgpu::FilterMode::Linear,
min: wgpu::FilterMode::Linear,
};
fn from_gltf(s: &gltf::texture::Sampler) -> SamplerKey {
SamplerKey {
wrap_u: wrap_to_wgpu(s.wrap_s()),
wrap_v: wrap_to_wgpu(s.wrap_t()),
mag: mag_to_wgpu(s.mag_filter()),
min: min_to_wgpu(s.min_filter()),
}
}
}
fn wrap_to_wgpu(m: gltf::texture::WrappingMode) -> wgpu::AddressMode {
use gltf::texture::WrappingMode;
match m {
WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
WrappingMode::Repeat => wgpu::AddressMode::Repeat,
}
}
fn mag_to_wgpu(f: Option<gltf::texture::MagFilter>) -> wgpu::FilterMode {
match f {
Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest,
_ => wgpu::FilterMode::Linear,
}
}
fn min_to_wgpu(f: Option<gltf::texture::MinFilter>) -> wgpu::FilterMode {
use gltf::texture::MinFilter;
match f {
Some(MinFilter::Nearest)
| Some(MinFilter::NearestMipmapNearest)
| Some(MinFilter::NearestMipmapLinear) => wgpu::FilterMode::Nearest,
_ => wgpu::FilterMode::Linear,
}
}
fn create_gltf_sampler(device: &wgpu::Device, key: SamplerKey) -> wgpu::Sampler {
crate::texture_quality::material_sampler(
device,
"gltf_material_sampler",
key.wrap_u,
key.wrap_v,
key.mag,
key.min,
true,
)
}
fn material_sampler_key(material: &gltf::Material) -> SamplerKey {
let pbr = material.pbr_metallic_roughness();
let tex = pbr
.base_color_texture()
.map(|t| t.texture())
.or_else(|| material.normal_texture().map(|t| t.texture()))
.or_else(|| pbr.metallic_roughness_texture().map(|t| t.texture()))
.or_else(|| material.emissive_texture().map(|t| t.texture()))
.or_else(|| material.occlusion_texture().map(|t| t.texture()));
match tex {
Some(t) => SamplerKey::from_gltf(&t.sampler()),
None => SamplerKey::DEFAULT,
}
}
fn emissive_with_strength(factor: [f32; 3], strength: Option<f32>) -> [f32; 3] {
let s = strength.unwrap_or(1.0);
[factor[0] * s, factor[1] * s, factor[2] * s]
}
fn material_uv_transform(material: &gltf::Material) -> crate::gpu_types::UvTransform {
match material
.pbr_metallic_roughness()
.base_color_texture()
.and_then(|ti| ti.texture_transform())
{
Some(tt) => crate::gpu_types::UvTransform {
offset: tt.offset(),
rotation: tt.rotation(),
scale: tt.scale(),
},
None => crate::gpu_types::UvTransform::default(),
}
}
pub(super) fn build_gltf_materials(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
document: &gltf::Document,
gpu_images: &[GpuImage],
defaults: &crate::asset::MaterialDefaults,
default_tbind: &Arc<wgpu::BindGroup>,
) -> Vec<Material> {
let mut sampler_cache: std::collections::HashMap<SamplerKey, wgpu::Sampler> =
std::collections::HashMap::new();
for material in document.materials() {
let key = material_sampler_key(&material);
sampler_cache
.entry(key)
.or_insert_with(|| create_gltf_sampler(device, key));
}
sampler_cache
.entry(SamplerKey::DEFAULT)
.or_insert_with(|| create_gltf_sampler(device, SamplerKey::DEFAULT));
document
.materials()
.map(|material| {
let pbr = material.pbr_metallic_roughness();
let base_color = pbr.base_color_factor();
let mat_sampler = &sampler_cache[&material_sampler_key(&material)];
let base_view = pbr
.base_color_texture()
.and_then(|ti| gpu_images.get(ti.texture().source().index()))
.map(|img| &img.view)
.unwrap_or(&defaults.white_view);
let normal_view = material
.normal_texture()
.and_then(|nt| gpu_images.get(nt.texture().source().index()))
.map(|img| &img.view)
.unwrap_or(&defaults.flat_normal_view);
let mr_view = pbr
.metallic_roughness_texture()
.and_then(|ti| gpu_images.get(ti.texture().source().index()))
.map(|img| &img.view)
.unwrap_or(&defaults.white_view);
let emissive_view = material
.emissive_texture()
.and_then(|ti| gpu_images.get(ti.texture().source().index()))
.map(|img| &img.view)
.unwrap_or(&defaults.white_view);
let ao_view = material
.occlusion_texture()
.and_then(|ot| gpu_images.get(ot.texture().source().index()))
.map(|img| &img.view)
.unwrap_or(&defaults.white_view);
let has_base = pbr.base_color_texture().is_some();
let has_any_map = has_base
|| material.normal_texture().is_some()
|| pbr.metallic_roughness_texture().is_some()
|| material.emissive_texture().is_some()
|| material.occlusion_texture().is_some();
let emissive =
emissive_with_strength(material.emissive_factor(), material.emissive_strength());
let normal_scale = material.normal_texture().map(|nt| nt.scale()).unwrap_or(1.0);
let occlusion_strength = material
.occlusion_texture()
.map(|ot| ot.strength())
.unwrap_or(1.0);
let uv_transform = material_uv_transform(&material);
let alpha_cutoff = if material.alpha_mode() == gltf::material::AlphaMode::Mask {
material.alpha_cutoff().unwrap_or(0.5)
} else {
0.0
};
let params = crate::gpu_types::MaterialParams::new(
emissive,
normal_scale,
occlusion_strength,
uv_transform,
alpha_cutoff,
);
let is_default_params = emissive == [0.0, 0.0, 0.0]
&& normal_scale == 1.0
&& occlusion_strength == 1.0
&& uv_transform.is_identity()
&& alpha_cutoff == 0.0;
let bind_group = if !has_any_map && is_default_params {
default_tbind.clone()
} else {
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!(
"gltf_material_params_{}",
material.index().unwrap_or(usize::MAX)
)),
contents: bytemuck::cast_slice(&[params]),
usage: wgpu::BufferUsages::UNIFORM,
});
crate::asset::AssetManager::assemble_material_bind_group(
device,
layout,
base_view,
mat_sampler,
normal_view,
mr_view,
emissive_view,
ao_view,
¶ms_buffer,
&format!("gltf_material_{}", material.index().unwrap_or(usize::MAX)),
)
};
let mut mat = Material::new(bind_group);
if has_base {
mat.texture_source = Some(format!(
"gltf_tex_base_{}",
material.index().unwrap_or(usize::MAX)
));
}
let mat_name = material.name().unwrap_or("").to_lowercase();
let is_glass = mat_name.contains("glass");
let alpha = if is_glass {
0.25 } else if material.alpha_mode() == gltf::material::AlphaMode::Opaque {
1.0
} else {
base_color[3]
};
tracing::debug!("GLTF LOAD MAT: name={:?}, alpha_mode={:?}, alpha_factor={}, base_color={:?}, double_sided={}",
material.name(), material.alpha_mode(), alpha, base_color, material.double_sided());
mat.albedo = gizmo_math::Vec4::new(base_color[0], base_color[1], base_color[2], alpha);
mat.metallic = pbr.metallic_factor();
mat.roughness = pbr.roughness_factor();
mat.is_transparent = material.alpha_mode() == gltf::material::AlphaMode::Blend
|| alpha < 0.99
|| is_glass;
mat.is_double_sided = material.double_sided();
mat
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn emissive_strength_scales_factor() {
assert_eq!(emissive_with_strength([1.0, 0.5, 0.0], None), [1.0, 0.5, 0.0]);
assert_eq!(emissive_with_strength([1.0, 0.5, 0.25], Some(4.0)), [4.0, 2.0, 1.0]);
assert_eq!(emissive_with_strength([1.0, 1.0, 1.0], Some(0.0)), [0.0, 0.0, 0.0]);
}
#[test]
fn sampler_filter_and_wrap_converters() {
use gltf::texture::{MagFilter, MinFilter, WrappingMode};
assert_eq!(wrap_to_wgpu(WrappingMode::ClampToEdge), wgpu::AddressMode::ClampToEdge);
assert_eq!(wrap_to_wgpu(WrappingMode::MirroredRepeat), wgpu::AddressMode::MirrorRepeat);
assert_eq!(wrap_to_wgpu(WrappingMode::Repeat), wgpu::AddressMode::Repeat);
assert_eq!(mag_to_wgpu(Some(MagFilter::Nearest)), wgpu::FilterMode::Nearest);
assert_eq!(mag_to_wgpu(Some(MagFilter::Linear)), wgpu::FilterMode::Linear);
assert_eq!(mag_to_wgpu(None), wgpu::FilterMode::Linear);
assert_eq!(min_to_wgpu(Some(MinFilter::Nearest)), wgpu::FilterMode::Nearest);
assert_eq!(min_to_wgpu(Some(MinFilter::NearestMipmapLinear)), wgpu::FilterMode::Nearest);
assert_eq!(min_to_wgpu(Some(MinFilter::Linear)), wgpu::FilterMode::Linear);
assert_eq!(min_to_wgpu(Some(MinFilter::LinearMipmapLinear)), wgpu::FilterMode::Linear);
assert_eq!(min_to_wgpu(None), wgpu::FilterMode::Linear);
}
#[test]
fn gltf_material_sampler_and_emissive_strength_parsed() {
let json = r#"{
"asset": { "version": "2.0" },
"extensionsUsed": ["KHR_materials_emissive_strength"],
"samplers": [
{ "wrapS": 33071, "wrapT": 10497, "magFilter": 9728, "minFilter": 9729 }
],
"images": [ { "uri": "dummy.png" } ],
"textures": [ { "sampler": 0, "source": 0 } ],
"materials": [
{
"pbrMetallicRoughness": { "baseColorTexture": { "index": 0 } },
"emissiveFactor": [1.0, 0.5, 0.25],
"extensions": { "KHR_materials_emissive_strength": { "emissiveStrength": 4.0 } }
}
]
}"#;
let doc = gltf::Gltf::from_slice(json.as_bytes()).expect("parse minimal glTF");
let material = doc.materials().next().expect("one material");
let key = material_sampler_key(&material);
assert_eq!(key.wrap_u, wgpu::AddressMode::ClampToEdge);
assert_eq!(key.wrap_v, wgpu::AddressMode::Repeat);
assert_eq!(key.mag, wgpu::FilterMode::Nearest);
assert_eq!(key.min, wgpu::FilterMode::Linear);
assert_eq!(material.emissive_strength(), Some(4.0));
let emissive =
emissive_with_strength(material.emissive_factor(), material.emissive_strength());
assert_eq!(emissive, [4.0, 2.0, 1.0]);
}
#[test]
fn material_without_textures_uses_default_sampler_key() {
let json = r#"{
"asset": { "version": "2.0" },
"materials": [ { "emissiveFactor": [0.0, 0.0, 0.0] } ]
}"#;
let doc = gltf::Gltf::from_slice(json.as_bytes()).expect("parse");
let material = doc.materials().next().expect("one material");
assert_eq!(material_sampler_key(&material), SamplerKey::DEFAULT);
assert_eq!(material.emissive_strength(), None);
}
#[test]
fn gltf_texture_transform_parsed_and_packed() {
let json = r#"{
"asset": { "version": "2.0" },
"extensionsUsed": ["KHR_texture_transform"],
"images": [ { "uri": "dummy.png" } ],
"samplers": [ {} ],
"textures": [ { "sampler": 0, "source": 0 } ],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"extensions": {
"KHR_texture_transform": {
"offset": [0.1, 0.2],
"rotation": 1.5,
"scale": [2.0, 3.0]
}
}
}
}
}
]
}"#;
let doc = gltf::Gltf::from_slice(json.as_bytes()).expect("parse");
let material = doc.materials().next().expect("one material");
let uv = material_uv_transform(&material);
assert_eq!(uv.offset, [0.1, 0.2]);
assert!((uv.rotation - 1.5).abs() < 1e-6);
assert_eq!(uv.scale, [2.0, 3.0]);
assert!(!uv.is_identity());
let params = crate::gpu_types::MaterialParams::new([0.0; 3], 1.0, 1.0, uv, 0.0);
assert_eq!(params.occlusion_uv_rot_offset, [1.0, 1.5, 0.1, 0.2]);
assert_eq!(params.uv_scale, [2.0, 3.0, 0.0, 0.0]);
}
#[test]
fn material_without_texture_transform_is_identity() {
let json = r#"{
"asset": { "version": "2.0" },
"images": [ { "uri": "dummy.png" } ],
"samplers": [ {} ],
"textures": [ { "sampler": 0, "source": 0 } ],
"materials": [ { "pbrMetallicRoughness": { "baseColorTexture": { "index": 0 } } } ]
}"#;
let doc = gltf::Gltf::from_slice(json.as_bytes()).expect("parse");
let material = doc.materials().next().expect("one material");
assert!(material_uv_transform(&material).is_identity());
let d = crate::gpu_types::MaterialParams::default();
assert_eq!(d.uv_scale, [1.0, 1.0, 0.0, 0.0]);
assert_eq!(d.occlusion_uv_rot_offset, [1.0, 0.0, 0.0, 0.0]);
}
}