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 { Ok(std::fs::read_to_string(resource_path(file_name))?) } pub fn load_binary(file_name: &str) -> anyhow::Result> { Ok(std::fs::read(resource_path(file_name))?) } pub fn load_texture( file_name: &str, device: &wgpu::Device, queue: &wgpu::Queue, ) -> anyhow::Result { 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 { 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 { 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::>(); 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::>(); 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 { 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 { 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 { 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::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>, images: &[gltf::image::Data], device: &wgpu::Device, queue: &wgpu::Queue, label: &str, color_space: TextureColorSpace, fallback_rgba: &[u8; 4], ) -> anyhow::Result { 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, 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, 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::>(); let normals = reader .read_normals() .map(|iter| iter.collect::>()) .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::>()) .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); let colors = reader .read_colors(0) .map(|colors| colors.into_rgba_f32().collect::>()) .unwrap_or_else(|| vec![[1.0, 1.0, 1.0, 1.0]; positions.len()]); let tangents = reader.read_tangents().map(|iter| iter.collect::>()); 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::>(); let indices = reader .read_indices() .map(|indices| indices.into_u32().collect::>()) .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> { 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> { 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 }