diff --git a/src/gltf_loader.rs b/src/gltf_loader.rs new file mode 100644 index 0000000..81b24b7 --- /dev/null +++ b/src/gltf_loader.rs @@ -0,0 +1,180 @@ +//! GPU-ready mesh loader for **glTF 2.0** (internal helper) +//! +//! Converts the first primitive of a glTF document into our engine `Model`. + +use anyhow::{Context, Result}; +use glium::{backend::Facade, IndexBuffer, VertexBuffer}; +use glium::index::PrimitiveType; +use gltf::mesh::util::ReadIndices; +use std::{fmt::Debug, path::Path}; +use crate::model::{Vertex, Mesh, Material, Model}; +use glium::texture::{RawImage2d, Texture2d, SrgbTexture2d}; +use glium::uniforms::{SamplerWrapFunction, MinifySamplerFilter, MagnifySamplerFilter}; +use gltf::image::Format as GltfFormat; +use glam::Vec2; + +/// Load a glTF 2.0 file from disk and upload the first primitive to the GPU. +pub fn load_gltf(path: P, facade: &F) -> Result +where + P: AsRef + Debug, + F: Facade + ?Sized, +{ + // -- parse the asset & bring buffer blobs into memory -- + let (doc, buffers, images) = gltf::import(path.as_ref()).context("failed to import glTF file")?; + + // -- grab the very first mesh / primitive -- + let mesh = doc.meshes().next().context("glTF has no meshes")?; + let primitive = mesh.primitives().next().context("mesh has no primitives")?; + + // ---------- MATERIAL ---------- + let mut mat = Material::default(); + + let mat_idx = primitive.material().index().context("primitive has no material")?; + let material = doc.materials().nth(mat_idx).unwrap(); + let pbr = material.pbr_metallic_roughness(); + + // Factors -------------------------------------------------- + mat.base_color_factor = pbr.base_color_factor(); + mat.metal_factor = pbr.metallic_factor(); + mat.roughness_factor = pbr.roughness_factor(); + mat.emissive_factor = material.emissive_factor(); + + // Helper to update sampler settings from glTF sampler + fn update_sampler(mat: &mut Material, t: &gltf::texture::Texture<'_>) { + let sampler_info = t.sampler(); + mat.sampler.wrap_function.0 = match sampler_info.wrap_s() { + gltf::texture::WrappingMode::ClampToEdge => SamplerWrapFunction::Clamp, + gltf::texture::WrappingMode::MirroredRepeat => SamplerWrapFunction::Mirror, + gltf::texture::WrappingMode::Repeat => SamplerWrapFunction::Repeat, + }; + mat.sampler.wrap_function.1 = match sampler_info.wrap_t() { + gltf::texture::WrappingMode::ClampToEdge => SamplerWrapFunction::Clamp, + gltf::texture::WrappingMode::MirroredRepeat => SamplerWrapFunction::Mirror, + gltf::texture::WrappingMode::Repeat => SamplerWrapFunction::Repeat, + }; + if let Some(f) = sampler_info.mag_filter() { + mat.sampler.magnify_filter = match f { + gltf::texture::MagFilter::Nearest => MagnifySamplerFilter::Nearest, + gltf::texture::MagFilter::Linear => MagnifySamplerFilter::Linear, + }; + } + if let Some(f) = sampler_info.min_filter() { + mat.sampler.minify_filter = match f { + gltf::texture::MinFilter::Nearest => MinifySamplerFilter::Nearest, + gltf::texture::MinFilter::Linear => MinifySamplerFilter::Linear, + gltf::texture::MinFilter::NearestMipmapNearest => MinifySamplerFilter::NearestMipmapNearest, + gltf::texture::MinFilter::NearestMipmapLinear => MinifySamplerFilter::NearestMipmapLinear, + gltf::texture::MinFilter::LinearMipmapNearest => MinifySamplerFilter::LinearMipmapNearest, + gltf::texture::MinFilter::LinearMipmapLinear => MinifySamplerFilter::LinearMipmapLinear, + }; + } + } + + // Base-color texture (sRGB) + if let Some(info) = pbr.base_color_texture() { + update_sampler(&mut mat, &info.texture()); + let view = info.texture().source().index(); + mat.base_color = Some(glium_srgb_texture(facade, &images[view])?); + } + + // Metallic-Roughness (linear) + if let Some(info) = pbr.metallic_roughness_texture() { + update_sampler(&mut mat, &info.texture()); + let view = info.texture().source().index(); + mat.metallic_roughness = Some(glium_linear_texture(facade, &images[view])?); + } + + // Normal map (linear) + if let Some(info) = primitive.material().normal_texture() { + update_sampler(&mut mat, &info.texture()); + let view = info.texture().source().index(); + mat.normal = Some(glium_linear_texture(facade, &images[view])?); + } + + // Occlusion (linear) + if let Some(info) = primitive.material().occlusion_texture() { + update_sampler(&mut mat, &info.texture()); + let view = info.texture().source().index(); + mat.occlusion = Some(glium_linear_texture(facade, &images[view])?); + } + + // Emissive (sRGB) + if let Some(info) = primitive.material().emissive_texture() { + update_sampler(&mut mat, &info.texture()); + let view = info.texture().source().index(); + mat.emissive = Some(glium_srgb_texture(facade, &images[view])?); + } + + // KHR_texture_transform + if let Some(tex) = pbr.base_color_texture() { + if let Some(xform) = tex.texture_transform() { + mat.uv_offset = Vec2::new(xform.offset()[0], xform.offset()[1]); + mat.uv_scale = Vec2::new(xform.scale()[0], xform.scale()[1]); + } + } + + // ---- Vertex/index data ---- + let reader = primitive.reader(|buf| Some(&buffers[buf.index()].0)); + + let positions: Vec<[f32; 3]> = reader.read_positions().context("missing POSITION")?.collect(); + let normals: Vec<[f32; 3]> = reader.read_normals().context("missing NORMAL")?.collect(); + let tex_coords: Vec<[f32; 2]> = reader.read_tex_coords(0).map(|tc| tc.into_f32().collect()).unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); + let indices: Vec = reader.read_indices().context("missing indices")?.into_u32().collect(); + + // Interleave + let vertices: Vec = (0..positions.len()).map(|i| Vertex { position: positions[i], normal: normals[i], tex_coords: tex_coords[i] }).collect(); + + let vbuf = VertexBuffer::immutable(facade, &vertices)?; + let ibuf = IndexBuffer ::immutable(facade, PrimitiveType::TrianglesList, &indices)?; + + Ok(Model { mesh: Mesh { vbuf, ibuf }, material: mat }) +} + +/// Linear-space texture (RGBA8) from glTF image data. +fn glium_linear_texture(facade: &F, img: &gltf::image::Data) -> Result +where + F: Facade + ?Sized, +{ + let rgba = to_rgba(img); + let raw = RawImage2d::from_raw_rgba(rgba, (img.width, img.height)); + Ok(Texture2d::new(facade, raw)?) +} + +/// sRGB texture from glTF image data. +fn glium_srgb_texture(facade: &F, img: &gltf::image::Data) -> Result +where + F: Facade + ?Sized, +{ + let rgba = to_rgba(img); + let raw = RawImage2d::from_raw_rgba(rgba, (img.width, img.height)); + Ok(SrgbTexture2d::new(facade, raw)?) +} + +/// Convert various glTF image formats to RGBA8 as expected by glium. +fn to_rgba(img: &gltf::image::Data) -> Vec { + match img.format { + GltfFormat::R8G8B8A8 => img.pixels.clone(), + GltfFormat::R8G8B8 => { + // Expand RGB to RGBA with alpha=255 + img.pixels + .chunks(3) + .flat_map(|rgb| [rgb[0], rgb[1], rgb[2], 255u8]) + .collect() + } + GltfFormat::R8G8 => { + // Treat RG as luminance+alpha? For simplicity, replicate first channel into RGB, second as alpha. + img.pixels + .chunks(2) + .flat_map(|rg| [rg[0], rg[0], rg[0], rg[1]]) + .collect() + } + GltfFormat::R8 => { + // Grayscale: replicate into RGB, alpha=255 + img.pixels + .iter() + .flat_map(|l| [*l, *l, *l, 255u8]) + .collect() + } + _ => img.pixels.clone(), + } +} \ No newline at end of file diff --git a/src/model.rs b/src/model.rs index 815e2a2..e474fe4 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,124 +1,57 @@ -//! GPU-ready mesh loader for **glTF 2.0** -//! -//! Loads the first mesh/primitive found in a .gltf/.glb file. - -use anyhow::{Context, Result}; +use glium::texture::{RawImage2d, SrgbTexture2d, Texture2d}; +use glium::uniforms::SamplerBehavior; +use glam::{Vec2}; use glium::{backend::Facade, implement_vertex, IndexBuffer, VertexBuffer}; use glium::index::PrimitiveType; -use gltf::mesh::util::ReadIndices; -use std::{fmt::Debug, path::Path}; #[derive(Copy, Clone)] pub struct Vertex { - pub position: [f32; 3], - pub normal: [f32; 3], + pub position: [f32; 3], + pub normal: [f32; 3], + pub tex_coords: [f32; 2], } -implement_vertex!(Vertex, position, normal); +implement_vertex!(Vertex, position, normal, tex_coords); pub struct Mesh { pub vbuf: VertexBuffer, pub ibuf: IndexBuffer, } -/// Load a glTF 2.0 file from disk and upload the first primitive to the GPU. -pub fn load_gltf(path: P, facade: &F) -> Result -where - P: AsRef + Debug, // `gltf::import` wants Debug for diagnostics :contentReference[oaicite:3]{index=3} - F: Facade + ?Sized, -{ - // -- parse the asset & bring buffer blobs into memory -- - let (doc, buffers, _images) = - gltf::import(path.as_ref()).context("failed to import glTF file")?; // :contentReference[oaicite:4]{index=4} - - // -- grab the very first mesh / primitive -- - let mesh = doc.meshes().next().context("glTF has no meshes")?; - let primitive = mesh.primitives().next().context("mesh has no primitives")?; - - // -- read vertex and index streams using the util::Reader helper -- - let reader = primitive.reader(|buf| Some(&buffers[buf.index()].0)); // Reader pattern :contentReference[oaicite:5]{index=5} - - let positions : Vec<[f32; 3]> = reader - .read_positions() - .context("primitive is missing POSITION attribute")? // POSITION is mandatory :contentReference[oaicite:6]{index=6} - .collect(); - - let normals : Vec<[f32; 3]> = reader - .read_normals() - .context("primitive is missing NORMAL attribute")? - .collect(); - - let indices : Vec = reader - .read_indices() - .context("primitive has no indices")? - .into_u32() - .collect(); // ReadIndices enum :contentReference[oaicite:7]{index=7} - - // -- interleave into our engine's Vertex struct -- - let vertices: Vec = positions - .into_iter() - .zip(normals.into_iter()) - .map(|(p, n)| Vertex { position: p, normal: n }) - .collect(); - - // -- immutable GPU buffers (fast path in glium) -- - let vbuf = VertexBuffer::immutable(facade, &vertices)?; // Immutable VBO :contentReference[oaicite:8]{index=8} - let ibuf = IndexBuffer ::immutable(facade, PrimitiveType::TrianglesList, &indices)?; - - Ok(Mesh { vbuf, ibuf }) +pub struct Material { + pub base_color: Option, + pub metallic_roughness: Option, + pub normal: Option, + pub occlusion: Option, + pub emissive: Option, + pub sampler: SamplerBehavior, + pub uv_offset: Vec2, + pub uv_scale: Vec2, + pub base_color_factor: [f32; 4], + pub emissive_factor: [f32; 3], + pub metal_factor: f32, + pub roughness_factor: f32, } -/// Create a unit cube (edge length = 2) with per-face normals. -pub fn cube(facade: &F) -> Result -where - F: Facade + ?Sized, -{ - // 24 unique vertices (4 per face) so that each face has a flat normal. - let vertices: [Vertex; 24] = [ - // Front (+Z) - Vertex { position: [-1.0, -1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, - Vertex { position: [ 1.0, -1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, - Vertex { position: [ 1.0, 1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, - Vertex { position: [-1.0, 1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, - - // Back (-Z) - Vertex { position: [ 1.0, -1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, - Vertex { position: [-1.0, -1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, - Vertex { position: [-1.0, 1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, - Vertex { position: [ 1.0, 1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, - - // Left (-X) - Vertex { position: [-1.0, -1.0, -1.0], normal: [-1.0, 0.0, 0.0] }, - Vertex { position: [-1.0, -1.0, 1.0], normal: [-1.0, 0.0, 0.0] }, - Vertex { position: [-1.0, 1.0, 1.0], normal: [-1.0, 0.0, 0.0] }, - Vertex { position: [-1.0, 1.0, -1.0], normal: [-1.0, 0.0, 0.0] }, - - // Right (+X) - Vertex { position: [ 1.0, -1.0, 1.0], normal: [ 1.0, 0.0, 0.0] }, - Vertex { position: [ 1.0, -1.0, -1.0], normal: [ 1.0, 0.0, 0.0] }, - Vertex { position: [ 1.0, 1.0, -1.0], normal: [ 1.0, 0.0, 0.0] }, - Vertex { position: [ 1.0, 1.0, 1.0], normal: [ 1.0, 0.0, 0.0] }, - - // Top (+Y) - Vertex { position: [-1.0, 1.0, 1.0], normal: [ 0.0, 1.0, 0.0] }, - Vertex { position: [ 1.0, 1.0, 1.0], normal: [ 0.0, 1.0, 0.0] }, - Vertex { position: [ 1.0, 1.0, -1.0], normal: [ 0.0, 1.0, 0.0] }, - Vertex { position: [-1.0, 1.0, -1.0], normal: [ 0.0, 1.0, 0.0] }, - - // Bottom (-Y) - Vertex { position: [-1.0, -1.0, -1.0], normal: [ 0.0, -1.0, 0.0] }, - Vertex { position: [ 1.0, -1.0, -1.0], normal: [ 0.0, -1.0, 0.0] }, - Vertex { position: [ 1.0, -1.0, 1.0], normal: [ 0.0, -1.0, 0.0] }, - Vertex { position: [-1.0, -1.0, 1.0], normal: [ 0.0, -1.0, 0.0] }, - ]; - - let mut indices: Vec = Vec::with_capacity(36); - for face in 0..6 { - let o = (face * 4) as u32; - indices.extend_from_slice(&[o, o + 1, o + 2, o, o + 2, o + 3]); +impl Default for Material { + fn default() -> Self { + Self { + base_color: None, + metallic_roughness: None, + normal: None, + occlusion: None, + emissive: None, + sampler: SamplerBehavior::default(), + uv_offset: Vec2::ZERO, + uv_scale: Vec2::ONE, + base_color_factor: [1.0; 4], + emissive_factor: [0.0; 3], + metal_factor: 1.0, + roughness_factor: 1.0, + } } - - let vbuf = VertexBuffer::immutable(facade, &vertices)?; - let ibuf = IndexBuffer::immutable(facade, PrimitiveType::TrianglesList, &indices)?; - - Ok(Mesh { vbuf, ibuf }) +} + +pub struct Model { + pub mesh: Mesh, + pub material: Material, }