Move gltf_loader to a separate module, new GLTF features
- glTF loader now supports textures. I was able to successfully load multiple blender models with different texturing methods. - Added Material to represent textures and material properties.
This commit is contained in:
parent
3db5237909
commit
0ecdb2cb6f
2 changed files with 221 additions and 108 deletions
180
src/gltf_loader.rs
Normal file
180
src/gltf_loader.rs
Normal file
|
|
@ -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<P, F>(path: P, facade: &F) -> Result<Model>
|
||||||
|
where
|
||||||
|
P: AsRef<Path> + 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<u32> = reader.read_indices().context("missing indices")?.into_u32().collect();
|
||||||
|
|
||||||
|
// Interleave
|
||||||
|
let vertices: Vec<Vertex> = (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<F>(facade: &F, img: &gltf::image::Data) -> Result<Texture2d>
|
||||||
|
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<F>(facade: &F, img: &gltf::image::Data) -> Result<SrgbTexture2d>
|
||||||
|
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<u8> {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/model.rs
145
src/model.rs
|
|
@ -1,124 +1,57 @@
|
||||||
//! GPU-ready mesh loader for **glTF 2.0**
|
use glium::texture::{RawImage2d, SrgbTexture2d, Texture2d};
|
||||||
//!
|
use glium::uniforms::SamplerBehavior;
|
||||||
//! Loads the first mesh/primitive found in a .gltf/.glb file.
|
use glam::{Vec2};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use glium::{backend::Facade, implement_vertex, IndexBuffer, VertexBuffer};
|
use glium::{backend::Facade, implement_vertex, IndexBuffer, VertexBuffer};
|
||||||
use glium::index::PrimitiveType;
|
use glium::index::PrimitiveType;
|
||||||
use gltf::mesh::util::ReadIndices;
|
|
||||||
use std::{fmt::Debug, path::Path};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct Vertex {
|
pub struct Vertex {
|
||||||
pub position: [f32; 3],
|
pub position: [f32; 3],
|
||||||
pub normal: [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 struct Mesh {
|
||||||
pub vbuf: VertexBuffer<Vertex>,
|
pub vbuf: VertexBuffer<Vertex>,
|
||||||
pub ibuf: IndexBuffer<u32>,
|
pub ibuf: IndexBuffer<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a glTF 2.0 file from disk and upload the first primitive to the GPU.
|
pub struct Material {
|
||||||
pub fn load_gltf<P, F>(path: P, facade: &F) -> Result<Mesh>
|
pub base_color: Option<SrgbTexture2d>,
|
||||||
where
|
pub metallic_roughness: Option<Texture2d>,
|
||||||
P: AsRef<Path> + Debug, // `gltf::import` wants Debug for diagnostics :contentReference[oaicite:3]{index=3}
|
pub normal: Option<Texture2d>,
|
||||||
F: Facade + ?Sized,
|
pub occlusion: Option<Texture2d>,
|
||||||
{
|
pub emissive: Option<SrgbTexture2d>,
|
||||||
// -- parse the asset & bring buffer blobs into memory --
|
pub sampler: SamplerBehavior,
|
||||||
let (doc, buffers, _images) =
|
pub uv_offset: Vec2,
|
||||||
gltf::import(path.as_ref()).context("failed to import glTF file")?; // :contentReference[oaicite:4]{index=4}
|
pub uv_scale: Vec2,
|
||||||
|
pub base_color_factor: [f32; 4],
|
||||||
// -- grab the very first mesh / primitive --
|
pub emissive_factor: [f32; 3],
|
||||||
let mesh = doc.meshes().next().context("glTF has no meshes")?;
|
pub metal_factor: f32,
|
||||||
let primitive = mesh.primitives().next().context("mesh has no primitives")?;
|
pub roughness_factor: f32,
|
||||||
|
|
||||||
// -- 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<u32> = 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<Vertex> = 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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a unit cube (edge length = 2) with per-face normals.
|
impl Default for Material {
|
||||||
pub fn cube<F>(facade: &F) -> Result<Mesh>
|
fn default() -> Self {
|
||||||
where
|
Self {
|
||||||
F: Facade + ?Sized,
|
base_color: None,
|
||||||
{
|
metallic_roughness: None,
|
||||||
// 24 unique vertices (4 per face) so that each face has a flat normal.
|
normal: None,
|
||||||
let vertices: [Vertex; 24] = [
|
occlusion: None,
|
||||||
// Front (+Z)
|
emissive: None,
|
||||||
Vertex { position: [-1.0, -1.0, 1.0], normal: [ 0.0, 0.0, 1.0] },
|
sampler: SamplerBehavior::default(),
|
||||||
Vertex { position: [ 1.0, -1.0, 1.0], normal: [ 0.0, 0.0, 1.0] },
|
uv_offset: Vec2::ZERO,
|
||||||
Vertex { position: [ 1.0, 1.0, 1.0], normal: [ 0.0, 0.0, 1.0] },
|
uv_scale: Vec2::ONE,
|
||||||
Vertex { position: [-1.0, 1.0, 1.0], normal: [ 0.0, 0.0, 1.0] },
|
base_color_factor: [1.0; 4],
|
||||||
|
emissive_factor: [0.0; 3],
|
||||||
// Back (-Z)
|
metal_factor: 1.0,
|
||||||
Vertex { position: [ 1.0, -1.0, -1.0], normal: [ 0.0, 0.0, -1.0] },
|
roughness_factor: 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<u32> = 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]);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let vbuf = VertexBuffer::immutable(facade, &vertices)?;
|
|
||||||
let ibuf = IndexBuffer::immutable(facade, PrimitiveType::TrianglesList, &indices)?;
|
pub struct Model {
|
||||||
|
pub mesh: Mesh,
|
||||||
Ok(Mesh { vbuf, ibuf })
|
pub material: Material,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue