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,
}