1008 lines
32 KiB
Rust
1008 lines
32 KiB
Rust
use crate::{
|
|
model::{self, AlphaMode},
|
|
texture::{self, TextureColorSpace, TextureSamplerDescriptor},
|
|
};
|
|
use anyhow::{Context, bail};
|
|
use eframe::wgpu;
|
|
use eframe::wgpu::util::DeviceExt;
|
|
use glam::{Mat3, Mat4, Vec2, Vec3};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
const WHITE_TEXTURE: &[u8; 4] = &[255, 255, 255, 255];
|
|
const BLACK_TEXTURE: &[u8; 4] = &[0, 0, 0, 255];
|
|
const FLAT_NORMAL_TEXTURE: &[u8; 4] = &[128, 128, 255, 255];
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
struct MaterialParams {
|
|
base_color: [f32; 4],
|
|
metallic_roughness: [f32; 4],
|
|
emissive_alpha: [f32; 4],
|
|
flags: [f32; 4],
|
|
}
|
|
|
|
fn resource_path(file_name: &str) -> PathBuf {
|
|
Path::new(env!("RESOURCE_DIR")).join(file_name)
|
|
}
|
|
|
|
pub fn load_string(file_name: &str) -> anyhow::Result<String> {
|
|
Ok(std::fs::read_to_string(resource_path(file_name))?)
|
|
}
|
|
|
|
pub fn load_binary(file_name: &str) -> anyhow::Result<Vec<u8>> {
|
|
Ok(std::fs::read(resource_path(file_name))?)
|
|
}
|
|
|
|
pub fn load_texture(
|
|
file_name: &str,
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> anyhow::Result<texture::Texture> {
|
|
let data = load_binary(file_name)?;
|
|
texture::Texture::from_bytes(device, queue, &data, file_name)
|
|
}
|
|
|
|
pub fn load_model(
|
|
file_name: &str,
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
|
) -> anyhow::Result<model::Model> {
|
|
match Path::new(file_name)
|
|
.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.map(str::to_ascii_lowercase)
|
|
.as_deref()
|
|
{
|
|
Some("obj") => load_obj_model(file_name, device, queue, texture_bind_group_layout),
|
|
Some("gltf") | Some("glb") => {
|
|
load_gltf_model(file_name, device, queue, texture_bind_group_layout)
|
|
}
|
|
_ => bail!("unsupported model format: {file_name}"),
|
|
}
|
|
}
|
|
|
|
fn load_obj_model(
|
|
file_name: &str,
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
|
) -> anyhow::Result<model::Model> {
|
|
let path = resource_path(file_name);
|
|
let (models, obj_materials) = tobj::load_obj(
|
|
&path,
|
|
&tobj::LoadOptions {
|
|
single_index: true,
|
|
triangulate: true,
|
|
..Default::default()
|
|
},
|
|
)?;
|
|
|
|
let mut materials = Vec::new();
|
|
for material in obj_materials? {
|
|
let base_color = material
|
|
.diffuse
|
|
.map(|diffuse| [diffuse[0], diffuse[1], diffuse[2], 1.0])
|
|
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
|
|
let base_color_texture = match material.diffuse_texture.as_deref() {
|
|
Some(texture_path) => load_texture(texture_path, device, queue)?,
|
|
None => default_texture(
|
|
device,
|
|
queue,
|
|
&material.name,
|
|
TextureColorSpace::Srgb,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
};
|
|
|
|
let material_textures = MaterialTextures {
|
|
base_color_texture,
|
|
metallic_roughness_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{} metallic-roughness", material.name),
|
|
TextureColorSpace::Linear,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
normal_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{} normal", material.name),
|
|
TextureColorSpace::Linear,
|
|
FLAT_NORMAL_TEXTURE,
|
|
)?,
|
|
occlusion_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{} occlusion", material.name),
|
|
TextureColorSpace::Linear,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
emissive_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{} emissive", material.name),
|
|
TextureColorSpace::Srgb,
|
|
BLACK_TEXTURE,
|
|
)?,
|
|
};
|
|
let params = MaterialParams {
|
|
base_color,
|
|
metallic_roughness: [0.0, 0.5, 1.0, 1.0],
|
|
emissive_alpha: [0.0, 0.0, 0.0, 0.0],
|
|
flags: [0.5, 0.0, 0.0, 0.0],
|
|
};
|
|
log_material_debug(
|
|
materials.len(),
|
|
&material.name,
|
|
AlphaMode::Opaque,
|
|
0.5,
|
|
base_color[3],
|
|
false,
|
|
);
|
|
materials.push(create_material(
|
|
device,
|
|
texture_bind_group_layout,
|
|
material.name,
|
|
material_textures,
|
|
params,
|
|
AlphaMode::Opaque,
|
|
0.5,
|
|
base_color,
|
|
false,
|
|
));
|
|
}
|
|
|
|
if materials.is_empty() {
|
|
materials.push(create_color_material(
|
|
"default",
|
|
[1.0, 1.0, 1.0, 1.0],
|
|
device,
|
|
queue,
|
|
texture_bind_group_layout,
|
|
)?);
|
|
}
|
|
|
|
let meshes = models
|
|
.into_iter()
|
|
.map(|m| {
|
|
let mut vertices = (0..m.mesh.positions.len() / 3)
|
|
.map(|i| model::ModelVertex {
|
|
position: [
|
|
m.mesh.positions[i * 3],
|
|
m.mesh.positions[i * 3 + 1],
|
|
m.mesh.positions[i * 3 + 2],
|
|
],
|
|
tex_coords: if m.mesh.texcoords.len() >= i * 2 + 2 {
|
|
[m.mesh.texcoords[i * 2], 1.0 - m.mesh.texcoords[i * 2 + 1]]
|
|
} else {
|
|
[0.0, 0.0]
|
|
},
|
|
normal: if m.mesh.normals.len() >= i * 3 + 3 {
|
|
[
|
|
m.mesh.normals[i * 3],
|
|
m.mesh.normals[i * 3 + 1],
|
|
m.mesh.normals[i * 3 + 2],
|
|
]
|
|
} else {
|
|
[0.0, 1.0, 0.0]
|
|
},
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
tangent: [1.0, 0.0, 0.0, 1.0],
|
|
})
|
|
.collect::<Vec<_>>();
|
|
generate_tangents(&mut vertices, &m.mesh.indices);
|
|
|
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some(&format!("{} Vertex Buffer", m.name)),
|
|
contents: bytemuck::cast_slice(&vertices),
|
|
usage: wgpu::BufferUsages::VERTEX,
|
|
});
|
|
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some(&format!("{} Index Buffer", m.name)),
|
|
contents: bytemuck::cast_slice(&m.mesh.indices),
|
|
usage: wgpu::BufferUsages::INDEX,
|
|
});
|
|
|
|
model::Mesh {
|
|
name: m.name,
|
|
vertex_buffer,
|
|
index_buffer,
|
|
num_elements: m.mesh.indices.len() as u32,
|
|
material: m.mesh.material_id.unwrap_or(0),
|
|
center: mesh_center(&vertices),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
Ok(model::Model { meshes, materials })
|
|
}
|
|
|
|
fn load_gltf_model(
|
|
file_name: &str,
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
|
) -> anyhow::Result<model::Model> {
|
|
let path = resource_path(file_name);
|
|
let (document, buffers, images) =
|
|
gltf::import(&path).with_context(|| format!("failed to import {file_name}"))?;
|
|
|
|
let mut materials = Vec::new();
|
|
for material in document.materials() {
|
|
materials.push(create_gltf_material(
|
|
&material,
|
|
&images,
|
|
device,
|
|
queue,
|
|
texture_bind_group_layout,
|
|
)?);
|
|
}
|
|
|
|
let default_material = materials.len();
|
|
materials.push(create_color_material(
|
|
"default",
|
|
[1.0, 1.0, 1.0, 1.0],
|
|
device,
|
|
queue,
|
|
texture_bind_group_layout,
|
|
)?);
|
|
|
|
let mut meshes = Vec::new();
|
|
let scene = document
|
|
.default_scene()
|
|
.or_else(|| document.scenes().next())
|
|
.context("glTF has no scene")?;
|
|
for node in scene.nodes() {
|
|
append_gltf_node_meshes(
|
|
&node,
|
|
Mat4::IDENTITY,
|
|
&buffers,
|
|
device,
|
|
&mut meshes,
|
|
default_material,
|
|
)?;
|
|
}
|
|
|
|
Ok(model::Model { meshes, materials })
|
|
}
|
|
|
|
struct MaterialTextures {
|
|
base_color_texture: texture::Texture,
|
|
metallic_roughness_texture: texture::Texture,
|
|
normal_texture: texture::Texture,
|
|
occlusion_texture: texture::Texture,
|
|
emissive_texture: texture::Texture,
|
|
}
|
|
|
|
fn create_gltf_material(
|
|
material: &gltf::Material<'_>,
|
|
images: &[gltf::image::Data],
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
|
) -> anyhow::Result<model::Material> {
|
|
let pbr = material.pbr_metallic_roughness();
|
|
let name = material.name().unwrap_or("gltf_material");
|
|
let normal_info = material.normal_texture();
|
|
let occlusion_info = material.occlusion_texture();
|
|
|
|
let material_textures = MaterialTextures {
|
|
base_color_texture: texture_from_gltf_texture(
|
|
pbr.base_color_texture().map(|info| info.texture()),
|
|
images,
|
|
device,
|
|
queue,
|
|
&format!("{name} baseColor"),
|
|
TextureColorSpace::Srgb,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
metallic_roughness_texture: texture_from_gltf_texture(
|
|
pbr.metallic_roughness_texture().map(|info| info.texture()),
|
|
images,
|
|
device,
|
|
queue,
|
|
&format!("{name} metallicRoughness"),
|
|
TextureColorSpace::Linear,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
normal_texture: texture_from_gltf_texture(
|
|
normal_info.as_ref().map(|info| info.texture()),
|
|
images,
|
|
device,
|
|
queue,
|
|
&format!("{name} normal"),
|
|
TextureColorSpace::Linear,
|
|
FLAT_NORMAL_TEXTURE,
|
|
)?,
|
|
occlusion_texture: texture_from_gltf_texture(
|
|
occlusion_info.as_ref().map(|info| info.texture()),
|
|
images,
|
|
device,
|
|
queue,
|
|
&format!("{name} occlusion"),
|
|
TextureColorSpace::Linear,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
emissive_texture: texture_from_gltf_texture(
|
|
material.emissive_texture().map(|info| info.texture()),
|
|
images,
|
|
device,
|
|
queue,
|
|
&format!("{name} emissive"),
|
|
TextureColorSpace::Srgb,
|
|
BLACK_TEXTURE,
|
|
)?,
|
|
};
|
|
|
|
let alpha_mode = match material.alpha_mode() {
|
|
gltf::material::AlphaMode::Opaque => AlphaMode::Opaque,
|
|
gltf::material::AlphaMode::Mask => AlphaMode::Mask,
|
|
gltf::material::AlphaMode::Blend => AlphaMode::Blend,
|
|
};
|
|
let normal_scale = normal_info.as_ref().map(|info| info.scale()).unwrap_or(1.0);
|
|
let occlusion_strength = occlusion_info
|
|
.as_ref()
|
|
.map(|info| info.strength())
|
|
.unwrap_or(1.0);
|
|
let emissive_factor = material.emissive_factor();
|
|
let base_color_factor = pbr.base_color_factor();
|
|
let alpha_cutoff = material.alpha_cutoff().unwrap_or(0.5);
|
|
let double_sided = material.double_sided();
|
|
let params = MaterialParams {
|
|
base_color: base_color_factor,
|
|
metallic_roughness: [
|
|
pbr.metallic_factor(),
|
|
pbr.roughness_factor(),
|
|
normal_scale,
|
|
occlusion_strength,
|
|
],
|
|
emissive_alpha: [
|
|
emissive_factor[0],
|
|
emissive_factor[1],
|
|
emissive_factor[2],
|
|
alpha_mode.shader_value(),
|
|
],
|
|
flags: [
|
|
alpha_cutoff,
|
|
0.0,
|
|
if normal_info.is_some() { 1.0 } else { 0.0 },
|
|
if double_sided { 1.0 } else { 0.0 },
|
|
],
|
|
};
|
|
log_material_debug(
|
|
material.index().unwrap_or(usize::MAX),
|
|
name,
|
|
alpha_mode,
|
|
alpha_cutoff,
|
|
base_color_factor[3],
|
|
double_sided,
|
|
);
|
|
|
|
Ok(create_material(
|
|
device,
|
|
texture_bind_group_layout,
|
|
name.to_owned(),
|
|
material_textures,
|
|
params,
|
|
alpha_mode,
|
|
alpha_cutoff,
|
|
base_color_factor,
|
|
double_sided,
|
|
))
|
|
}
|
|
|
|
fn create_color_material(
|
|
name: &str,
|
|
color: [f32; 4],
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
|
) -> anyhow::Result<model::Material> {
|
|
let material_textures = MaterialTextures {
|
|
base_color_texture: default_texture(
|
|
device,
|
|
queue,
|
|
name,
|
|
TextureColorSpace::Srgb,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
metallic_roughness_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{name} metallic-roughness"),
|
|
TextureColorSpace::Linear,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
normal_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{name} normal"),
|
|
TextureColorSpace::Linear,
|
|
FLAT_NORMAL_TEXTURE,
|
|
)?,
|
|
occlusion_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{name} occlusion"),
|
|
TextureColorSpace::Linear,
|
|
WHITE_TEXTURE,
|
|
)?,
|
|
emissive_texture: default_texture(
|
|
device,
|
|
queue,
|
|
&format!("{name} emissive"),
|
|
TextureColorSpace::Srgb,
|
|
BLACK_TEXTURE,
|
|
)?,
|
|
};
|
|
let params = MaterialParams {
|
|
base_color: color,
|
|
metallic_roughness: [1.0, 1.0, 1.0, 1.0],
|
|
emissive_alpha: [0.0, 0.0, 0.0, 0.0],
|
|
flags: [0.5, 0.0, 0.0, 0.0],
|
|
};
|
|
log_material_debug(usize::MAX, name, AlphaMode::Opaque, 0.5, color[3], false);
|
|
|
|
Ok(create_material(
|
|
device,
|
|
texture_bind_group_layout,
|
|
name.to_owned(),
|
|
material_textures,
|
|
params,
|
|
AlphaMode::Opaque,
|
|
0.5,
|
|
color,
|
|
false,
|
|
))
|
|
}
|
|
|
|
fn create_material(
|
|
device: &wgpu::Device,
|
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
|
name: String,
|
|
textures: MaterialTextures,
|
|
params: MaterialParams,
|
|
alpha_mode: AlphaMode,
|
|
alpha_cutoff: f32,
|
|
base_color_factor: [f32; 4],
|
|
double_sided: bool,
|
|
) -> model::Material {
|
|
let params_buffer = create_material_params_buffer(device, &name, params);
|
|
let bind_group = create_texture_bind_group(
|
|
device,
|
|
texture_bind_group_layout,
|
|
&name,
|
|
&textures,
|
|
¶ms_buffer,
|
|
);
|
|
|
|
model::Material {
|
|
name,
|
|
alpha_mode,
|
|
alpha_cutoff,
|
|
base_color_factor,
|
|
double_sided,
|
|
base_color_texture: textures.base_color_texture,
|
|
metallic_roughness_texture: textures.metallic_roughness_texture,
|
|
normal_texture: textures.normal_texture,
|
|
occlusion_texture: textures.occlusion_texture,
|
|
emissive_texture: textures.emissive_texture,
|
|
params_buffer,
|
|
bind_group,
|
|
}
|
|
}
|
|
|
|
fn default_texture(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
label: &str,
|
|
color_space: TextureColorSpace,
|
|
rgba: &[u8; 4],
|
|
) -> anyhow::Result<texture::Texture> {
|
|
texture::Texture::from_rgba8_with_sampler(
|
|
device,
|
|
queue,
|
|
rgba,
|
|
1,
|
|
1,
|
|
label,
|
|
color_space,
|
|
TextureSamplerDescriptor::default(),
|
|
)
|
|
}
|
|
|
|
fn create_material_params_buffer(
|
|
device: &wgpu::Device,
|
|
name: &str,
|
|
params: MaterialParams,
|
|
) -> wgpu::Buffer {
|
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some(&format!("{name} Material Params Buffer")),
|
|
contents: bytemuck::cast_slice(&[params]),
|
|
usage: wgpu::BufferUsages::UNIFORM,
|
|
})
|
|
}
|
|
|
|
fn create_texture_bind_group(
|
|
device: &wgpu::Device,
|
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
|
name: &str,
|
|
textures: &MaterialTextures,
|
|
params_buffer: &wgpu::Buffer,
|
|
) -> wgpu::BindGroup {
|
|
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some(&format!("{name} Texture Bind Group")),
|
|
layout: texture_bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::TextureView(&textures.base_color_texture.view),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: wgpu::BindingResource::Sampler(&textures.base_color_texture.sampler),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 2,
|
|
resource: params_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 3,
|
|
resource: wgpu::BindingResource::TextureView(
|
|
&textures.metallic_roughness_texture.view,
|
|
),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 4,
|
|
resource: wgpu::BindingResource::Sampler(
|
|
&textures.metallic_roughness_texture.sampler,
|
|
),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 5,
|
|
resource: wgpu::BindingResource::TextureView(&textures.normal_texture.view),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 6,
|
|
resource: wgpu::BindingResource::Sampler(&textures.normal_texture.sampler),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 7,
|
|
resource: wgpu::BindingResource::TextureView(&textures.occlusion_texture.view),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 8,
|
|
resource: wgpu::BindingResource::Sampler(&textures.occlusion_texture.sampler),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 9,
|
|
resource: wgpu::BindingResource::TextureView(&textures.emissive_texture.view),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 10,
|
|
resource: wgpu::BindingResource::Sampler(&textures.emissive_texture.sampler),
|
|
},
|
|
],
|
|
})
|
|
}
|
|
|
|
fn texture_from_gltf_texture(
|
|
gltf_texture: Option<gltf::Texture<'_>>,
|
|
images: &[gltf::image::Data],
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
label: &str,
|
|
color_space: TextureColorSpace,
|
|
fallback_rgba: &[u8; 4],
|
|
) -> anyhow::Result<texture::Texture> {
|
|
let Some(gltf_texture) = gltf_texture else {
|
|
return default_texture(device, queue, label, color_space, fallback_rgba);
|
|
};
|
|
|
|
let image_index = gltf_texture.source().index();
|
|
let image = images
|
|
.get(image_index)
|
|
.with_context(|| format!("glTF texture references missing image {image_index}"))?;
|
|
let rgba = gltf_image_to_rgba8(image)?;
|
|
let sampler = gltf_sampler_descriptor(&gltf_texture.sampler());
|
|
texture::Texture::from_rgba8_with_sampler(
|
|
device,
|
|
queue,
|
|
&rgba,
|
|
image.width,
|
|
image.height,
|
|
&format!("{label} image {image_index} {color_space:?}"),
|
|
color_space,
|
|
sampler,
|
|
)
|
|
}
|
|
|
|
fn gltf_sampler_descriptor(sampler: &gltf::texture::Sampler<'_>) -> TextureSamplerDescriptor {
|
|
use gltf::texture::{MagFilter, MinFilter};
|
|
|
|
let mag_filter = match sampler.mag_filter().unwrap_or(MagFilter::Linear) {
|
|
MagFilter::Nearest => wgpu::FilterMode::Nearest,
|
|
MagFilter::Linear => wgpu::FilterMode::Linear,
|
|
};
|
|
let (min_filter, mipmap_filter) = match sampler.min_filter().unwrap_or(MinFilter::Linear) {
|
|
MinFilter::Nearest | MinFilter::NearestMipmapNearest | MinFilter::NearestMipmapLinear => {
|
|
(wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Nearest)
|
|
}
|
|
MinFilter::Linear | MinFilter::LinearMipmapNearest => {
|
|
(wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Nearest)
|
|
}
|
|
MinFilter::LinearMipmapLinear => (wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Linear),
|
|
};
|
|
|
|
TextureSamplerDescriptor {
|
|
address_mode_u: gltf_wrap_mode(sampler.wrap_s()),
|
|
address_mode_v: gltf_wrap_mode(sampler.wrap_t()),
|
|
mag_filter,
|
|
min_filter,
|
|
mipmap_filter,
|
|
}
|
|
}
|
|
|
|
fn gltf_wrap_mode(mode: gltf::texture::WrappingMode) -> wgpu::AddressMode {
|
|
use gltf::texture::WrappingMode;
|
|
|
|
match mode {
|
|
WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
|
|
WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
|
|
WrappingMode::Repeat => wgpu::AddressMode::Repeat,
|
|
}
|
|
}
|
|
|
|
fn append_gltf_node_meshes(
|
|
node: &gltf::Node<'_>,
|
|
parent_transform: Mat4,
|
|
buffers: &[gltf::buffer::Data],
|
|
device: &wgpu::Device,
|
|
meshes: &mut Vec<model::Mesh>,
|
|
default_material: usize,
|
|
) -> anyhow::Result<()> {
|
|
let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
|
|
let world_transform = parent_transform * local_transform;
|
|
|
|
if let Some(gltf_mesh) = node.mesh() {
|
|
let node_name = node.name().unwrap_or("gltf_node");
|
|
let mesh_index = gltf_mesh.index();
|
|
let mesh_name = node
|
|
.name()
|
|
.or_else(|| gltf_mesh.name())
|
|
.unwrap_or("gltf_mesh");
|
|
for (primitive_index, primitive) in gltf_mesh.primitives().enumerate() {
|
|
append_gltf_primitive_mesh(
|
|
mesh_name,
|
|
primitive_index,
|
|
&primitive,
|
|
world_transform,
|
|
buffers,
|
|
device,
|
|
meshes,
|
|
default_material,
|
|
node_name,
|
|
mesh_index,
|
|
)?;
|
|
}
|
|
}
|
|
|
|
for child in node.children() {
|
|
append_gltf_node_meshes(
|
|
&child,
|
|
world_transform,
|
|
buffers,
|
|
device,
|
|
meshes,
|
|
default_material,
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn append_gltf_primitive_mesh(
|
|
mesh_name: &str,
|
|
primitive_index: usize,
|
|
primitive: &gltf::Primitive<'_>,
|
|
world_transform: Mat4,
|
|
buffers: &[gltf::buffer::Data],
|
|
device: &wgpu::Device,
|
|
meshes: &mut Vec<model::Mesh>,
|
|
default_material: usize,
|
|
node_name: &str,
|
|
mesh_index: usize,
|
|
) -> anyhow::Result<()> {
|
|
let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
|
|
let positions = reader
|
|
.read_positions()
|
|
.with_context(|| format!("{mesh_name} primitive {primitive_index} has no positions"))?
|
|
.collect::<Vec<_>>();
|
|
let normals = reader
|
|
.read_normals()
|
|
.map(|iter| iter.collect::<Vec<_>>())
|
|
.unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
|
|
let tex_coords = reader
|
|
.read_tex_coords(0)
|
|
.map(|coords| coords.into_f32().collect::<Vec<_>>())
|
|
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
|
|
let colors = reader
|
|
.read_colors(0)
|
|
.map(|colors| colors.into_rgba_f32().collect::<Vec<_>>())
|
|
.unwrap_or_else(|| vec![[1.0, 1.0, 1.0, 1.0]; positions.len()]);
|
|
let tangents = reader.read_tangents().map(|iter| iter.collect::<Vec<_>>());
|
|
let has_tangents = tangents.is_some();
|
|
let tangents = tangents.unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 1.0]; positions.len()]);
|
|
|
|
let normal_transform = Mat3::from_mat4(world_transform.inverse().transpose());
|
|
let mut vertices = positions
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, position)| {
|
|
let world_position = world_transform
|
|
.transform_point3(Vec3::from_array(*position))
|
|
.to_array();
|
|
let world_normal = normal_transform
|
|
.mul_vec3(Vec3::from_array(
|
|
normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]),
|
|
))
|
|
.normalize_or_zero()
|
|
.to_array();
|
|
let tangent = tangents.get(index).copied().unwrap_or([1.0, 0.0, 0.0, 1.0]);
|
|
let world_tangent = normal_transform
|
|
.mul_vec3(Vec3::new(tangent[0], tangent[1], tangent[2]))
|
|
.normalize_or_zero();
|
|
|
|
model::ModelVertex {
|
|
position: world_position,
|
|
tex_coords: tex_coords.get(index).copied().unwrap_or([0.0, 0.0]),
|
|
normal: world_normal,
|
|
color: colors.get(index).copied().unwrap_or([1.0, 1.0, 1.0, 1.0]),
|
|
tangent: [
|
|
world_tangent.x,
|
|
world_tangent.y,
|
|
world_tangent.z,
|
|
tangent[3],
|
|
],
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let indices = reader
|
|
.read_indices()
|
|
.map(|indices| indices.into_u32().collect::<Vec<_>>())
|
|
.unwrap_or_else(|| (0..vertices.len() as u32).collect());
|
|
let center = mesh_center(&vertices);
|
|
|
|
if !has_tangents {
|
|
generate_tangents(&mut vertices, &indices);
|
|
}
|
|
|
|
log_primitive_material_debug(
|
|
node_name,
|
|
mesh_index,
|
|
primitive_index,
|
|
&primitive.material(),
|
|
default_material,
|
|
);
|
|
|
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some(&format!(
|
|
"{mesh_name} Primitive {primitive_index} Vertex Buffer"
|
|
)),
|
|
contents: bytemuck::cast_slice(&vertices),
|
|
usage: wgpu::BufferUsages::VERTEX,
|
|
});
|
|
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some(&format!(
|
|
"{mesh_name} Primitive {primitive_index} Index Buffer"
|
|
)),
|
|
contents: bytemuck::cast_slice(&indices),
|
|
usage: wgpu::BufferUsages::INDEX,
|
|
});
|
|
|
|
meshes.push(model::Mesh {
|
|
name: format!("{mesh_name}_{primitive_index}"),
|
|
vertex_buffer,
|
|
index_buffer,
|
|
num_elements: indices.len() as u32,
|
|
material: primitive.material().index().unwrap_or(default_material),
|
|
center,
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn mesh_center(vertices: &[model::ModelVertex]) -> [f32; 3] {
|
|
if vertices.is_empty() {
|
|
return [0.0, 0.0, 0.0];
|
|
}
|
|
|
|
let sum = vertices.iter().fold(Vec3::ZERO, |sum, vertex| {
|
|
sum + Vec3::from_array(vertex.position)
|
|
});
|
|
(sum / vertices.len() as f32).to_array()
|
|
}
|
|
|
|
fn log_material_debug(
|
|
index: usize,
|
|
name: &str,
|
|
alpha_mode: AlphaMode,
|
|
alpha_cutoff: f32,
|
|
base_color_factor_alpha: f32,
|
|
double_sided: bool,
|
|
) {
|
|
log::warn!(
|
|
"material index={index} name=\"{name}\" alpha_mode={} alpha_cutoff={alpha_cutoff:.3} base_color_factor_alpha={base_color_factor_alpha:.3} double_sided={double_sided} selected_pipeline={}",
|
|
alpha_mode.as_str(),
|
|
selected_pipeline_label(alpha_mode, double_sided),
|
|
);
|
|
}
|
|
|
|
fn log_primitive_material_debug(
|
|
node_name: &str,
|
|
mesh_index: usize,
|
|
primitive_index: usize,
|
|
material: &gltf::Material<'_>,
|
|
default_material: usize,
|
|
) {
|
|
let pbr = material.pbr_metallic_roughness();
|
|
let material_index = material.index().unwrap_or(default_material);
|
|
let material_name = material.name().unwrap_or("default");
|
|
log::warn!(
|
|
"primitive node=\"{node_name}\" mesh_index={mesh_index} primitive_index={primitive_index} material_index={material_index} material_name=\"{material_name}\" baseColorFactor={:?} metallicFactor={:.3} roughnessFactor={:.3} alphaMode={:?}",
|
|
pbr.base_color_factor(),
|
|
pbr.metallic_factor(),
|
|
pbr.roughness_factor(),
|
|
material.alpha_mode(),
|
|
);
|
|
}
|
|
|
|
fn selected_pipeline_label(alpha_mode: AlphaMode, double_sided: bool) -> String {
|
|
format!(
|
|
"{}_{}",
|
|
alpha_mode.as_str(),
|
|
if double_sided {
|
|
"DOUBLE_SIDED"
|
|
} else {
|
|
"SINGLE_SIDED"
|
|
}
|
|
)
|
|
}
|
|
|
|
fn generate_tangents(vertices: &mut [model::ModelVertex], indices: &[u32]) {
|
|
let mut accumulated = vec![Vec3::ZERO; vertices.len()];
|
|
|
|
for triangle in indices.chunks_exact(3) {
|
|
let [i0, i1, i2] = [
|
|
triangle[0] as usize,
|
|
triangle[1] as usize,
|
|
triangle[2] as usize,
|
|
];
|
|
if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() {
|
|
continue;
|
|
}
|
|
|
|
let p0 = Vec3::from_array(vertices[i0].position);
|
|
let p1 = Vec3::from_array(vertices[i1].position);
|
|
let p2 = Vec3::from_array(vertices[i2].position);
|
|
let uv0 = Vec2::from_array(vertices[i0].tex_coords);
|
|
let uv1 = Vec2::from_array(vertices[i1].tex_coords);
|
|
let uv2 = Vec2::from_array(vertices[i2].tex_coords);
|
|
|
|
let delta_pos1 = p1 - p0;
|
|
let delta_pos2 = p2 - p0;
|
|
let delta_uv1 = uv1 - uv0;
|
|
let delta_uv2 = uv2 - uv0;
|
|
let determinant = delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x;
|
|
if determinant.abs() < 1e-6 {
|
|
continue;
|
|
}
|
|
|
|
let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) / determinant;
|
|
accumulated[i0] += tangent;
|
|
accumulated[i1] += tangent;
|
|
accumulated[i2] += tangent;
|
|
}
|
|
|
|
for (vertex, tangent) in vertices.iter_mut().zip(accumulated) {
|
|
let normal = Vec3::from_array(vertex.normal).normalize_or_zero();
|
|
let tangent = (tangent - normal * normal.dot(tangent)).normalize_or_zero();
|
|
let tangent = if tangent.length_squared() > 0.0 {
|
|
tangent
|
|
} else {
|
|
fallback_tangent(normal)
|
|
};
|
|
vertex.tangent = [tangent.x, tangent.y, tangent.z, 1.0];
|
|
}
|
|
}
|
|
|
|
fn fallback_tangent(normal: Vec3) -> Vec3 {
|
|
let axis = if normal.y.abs() < 0.9 {
|
|
Vec3::Y
|
|
} else {
|
|
Vec3::X
|
|
};
|
|
normal.cross(axis).normalize_or_zero()
|
|
}
|
|
|
|
fn gltf_image_to_rgba8(image: &gltf::image::Data) -> anyhow::Result<Vec<u8>> {
|
|
use gltf::image::Format;
|
|
|
|
match image.format {
|
|
Format::R8G8B8A8 => Ok(image.pixels.clone()),
|
|
Format::R8G8B8 => {
|
|
let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
|
|
for rgb in image.pixels.chunks_exact(3) {
|
|
rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
|
|
}
|
|
Ok(rgba)
|
|
}
|
|
Format::R8 => {
|
|
let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
|
|
for value in &image.pixels {
|
|
rgba.extend_from_slice(&[*value, *value, *value, 255]);
|
|
}
|
|
Ok(rgba)
|
|
}
|
|
Format::R8G8 => {
|
|
let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
|
|
for rg in image.pixels.chunks_exact(2) {
|
|
rgba.extend_from_slice(&[rg[0], rg[1], 0, 255]);
|
|
}
|
|
Ok(rgba)
|
|
}
|
|
Format::R16 => rgba_from_chunks(&image.pixels, 1, 2, read_u16_component),
|
|
Format::R16G16 => rgba_from_chunks(&image.pixels, 2, 2, read_u16_component),
|
|
Format::R16G16B16 => rgba_from_chunks(&image.pixels, 3, 2, read_u16_component),
|
|
Format::R16G16B16A16 => rgba_from_chunks(&image.pixels, 4, 2, read_u16_component),
|
|
Format::R32G32B32FLOAT => rgba_from_chunks(&image.pixels, 3, 4, read_f32_component),
|
|
Format::R32G32B32A32FLOAT => rgba_from_chunks(&image.pixels, 4, 4, read_f32_component),
|
|
}
|
|
}
|
|
|
|
fn rgba_from_chunks(
|
|
pixels: &[u8],
|
|
channels: usize,
|
|
component_width: usize,
|
|
read_component: fn(&[u8]) -> u8,
|
|
) -> anyhow::Result<Vec<u8>> {
|
|
let pixel_width = channels * component_width;
|
|
if pixel_width == 0 || pixels.len() % pixel_width != 0 {
|
|
bail!("invalid glTF image byte length for {channels} channels");
|
|
}
|
|
|
|
let mut rgba = Vec::with_capacity((pixels.len() / pixel_width) * 4);
|
|
for pixel in pixels.chunks_exact(pixel_width) {
|
|
let r = read_component(&pixel[0..component_width]);
|
|
let g = if channels > 1 {
|
|
read_component(&pixel[component_width..component_width * 2])
|
|
} else {
|
|
r
|
|
};
|
|
let b = if channels > 2 {
|
|
read_component(&pixel[component_width * 2..component_width * 3])
|
|
} else {
|
|
r
|
|
};
|
|
let a = if channels > 3 {
|
|
read_component(&pixel[component_width * 3..component_width * 4])
|
|
} else {
|
|
255
|
|
};
|
|
rgba.extend_from_slice(&[r, g, b, a]);
|
|
}
|
|
Ok(rgba)
|
|
}
|
|
|
|
fn read_u16_component(bytes: &[u8]) -> u8 {
|
|
let value = u16::from_ne_bytes([bytes[0], bytes[1]]);
|
|
(value / 257) as u8
|
|
}
|
|
|
|
fn read_f32_component(bytes: &[u8]) -> u8 {
|
|
let value = f32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
|
|
(value.clamp(0.0, 1.0) * 255.0).round() as u8
|
|
}
|