Skip to main content

engvis_renderer/
gltf_loader.rs

1use engvis_core::{
2    Aabb, LightingEnvironment, Mesh, MeshVertex, PbrMaterial, Scene, SceneNode,
3    SubMesh,
4};
5use glam::{Affine3A, Quat, Vec3};
6use crate::texture_cache::TextureCache;
7
8/// Convert gltf image data to image::RgbaImage
9fn gltf_image_to_rgba(img: &gltf::image::Data) -> image::RgbaImage {
10    let pixels = &img.pixels;
11    let width = img.width;
12    let height = img.height;
13
14    match img.format {
15        gltf::image::Format::R8G8B8A8 => {
16            image::RgbaImage::from_raw(width, height, pixels.clone())
17                .unwrap_or_else(|| image::RgbaImage::new(width, height))
18        }
19        gltf::image::Format::R8G8B8 => {
20            let mut rgba = Vec::with_capacity(pixels.len() / 3 * 4);
21            for chunk in pixels.chunks(3) {
22                if chunk.len() == 3 {
23                    rgba.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
24                }
25            }
26            image::RgbaImage::from_raw(width, height, rgba)
27                .unwrap_or_else(|| image::RgbaImage::new(width, height))
28        }
29        _ => {
30            image::RgbaImage::from_raw(width, height, pixels.clone())
31                .unwrap_or_else(|| image::RgbaImage::new(width, height))
32        }
33    }
34}
35
36#[derive(Debug)]
37pub enum GltfLoadError {
38    Io(std::io::Error),
39    Gltf(gltf::Error),
40    Image(image::ImageError),
41}
42
43impl From<std::io::Error> for GltfLoadError {
44    fn from(e: std::io::Error) -> Self {
45        GltfLoadError::Io(e)
46    }
47}
48
49impl From<gltf::Error> for GltfLoadError {
50    fn from(e: gltf::Error) -> Self {
51        GltfLoadError::Gltf(e)
52    }
53}
54
55impl From<image::ImageError> for GltfLoadError {
56    fn from(e: image::ImageError) -> Self {
57        GltfLoadError::Image(e)
58    }
59}
60
61impl std::fmt::Display for GltfLoadError {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            GltfLoadError::Io(e) => write!(f, "IO error: {}", e),
65            GltfLoadError::Gltf(e) => write!(f, "glTF error: {}", e),
66            GltfLoadError::Image(e) => write!(f, "Image error: {}", e),
67        }
68    }
69}
70
71impl std::error::Error for GltfLoadError {}
72
73/// Load a glTF file into a `(Scene, Aabb)` pair.
74/// The returned Aabb is the scene's world-space bounding box, computed from
75/// the node hierarchy (not the local AABB of each mesh).
76pub fn load_gltf(
77    path: &str,
78    device: &wgpu::Device,
79    queue: &wgpu::Queue,
80    texture_cache: &mut TextureCache,
81) -> Result<(Scene, Aabb), GltfLoadError> {
82    let (gltf, buffers, images) = gltf::import(path)?;
83
84    let mut meshes = Vec::new();
85    let mut materials = Vec::new();
86    let mut nodes = Vec::new();
87
88    // 1. Load materials
89    for mat in gltf.materials() {
90        let pbr = mat.pbr_metallic_roughness();
91        let mut material = PbrMaterial {
92            name: mat.name().unwrap_or("unnamed").to_string(),
93            albedo: pbr.base_color_factor(),
94            metallic: pbr.metallic_factor(),
95            roughness: pbr.roughness_factor(),
96            emissive: mat.emissive_factor(),
97            normal_scale: mat.normal_texture().map_or(1.0, |t| t.scale()),
98            alpha_cutoff: mat.alpha_cutoff().unwrap_or(0.5),
99            albedo_texture: None,
100            metallic_roughness_texture: None,
101            normal_texture: None,
102            emissive_texture: None,
103        };
104
105        // Upload albedo texture
106        if let Some(info) = pbr.base_color_texture() {
107            let tex_index = info.texture().index();
108            if tex_index < images.len() {
109                let img = gltf_image_to_rgba(&images[tex_index]);
110                let idx = texture_cache.upload_image(
111                    device,
112                    queue,
113                    &img,
114                    "gltf_albedo",
115                    true,
116                );
117                material.albedo_texture = Some(idx);
118            }
119        }
120
121        // Upload metallic-roughness texture
122        if let Some(info) = pbr.metallic_roughness_texture() {
123            let tex_index = info.texture().index();
124            if tex_index < images.len() {
125                let img = gltf_image_to_rgba(&images[tex_index]);
126                let idx = texture_cache.upload_image(
127                    device,
128                    queue,
129                    &img,
130                    "gltf_mr",
131                    false,
132                );
133                material.metallic_roughness_texture = Some(idx);
134            }
135        }
136
137        // Upload normal texture
138        if let Some(info) = mat.normal_texture() {
139            let tex_index = info.texture().index();
140            if tex_index < images.len() {
141                let img = gltf_image_to_rgba(&images[tex_index]);
142                let idx = texture_cache.upload_image(
143                    device,
144                    queue,
145                    &img,
146                    "gltf_normal",
147                    false,
148                );
149                material.normal_texture = Some(idx);
150            }
151        }
152
153        // Upload emissive texture
154        if let Some(info) = mat.emissive_texture() {
155            let tex_index = info.texture().index();
156            if tex_index < images.len() {
157                let img = gltf_image_to_rgba(&images[tex_index]);
158                let idx = texture_cache.upload_image(
159                    device,
160                    queue,
161                    &img,
162                    "gltf_emissive",
163                    true,
164                );
165                material.emissive_texture = Some(idx);
166            }
167        }
168
169        materials.push(material);
170    }
171
172    // Ensure at least one material
173    if materials.is_empty() {
174        materials.push(PbrMaterial::default());
175    }
176
177    // 2. Load meshes
178    for gltf_mesh in gltf.meshes() {
179        let mut vertices = Vec::new();
180        let mut indices = Vec::new();
181        let mut sub_meshes = Vec::new();
182        let mut aabb = Aabb::empty();
183
184        for primitive in gltf_mesh.primitives() {
185            let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
186
187            let positions: Vec<[f32; 3]> = reader
188                .read_positions()
189                .map(|iter| iter.collect())
190                .unwrap_or_default();
191            let normals: Vec<[f32; 3]> = reader
192                .read_normals()
193                .map(|iter| iter.collect())
194                .unwrap_or_default();
195            let uvs: Vec<[f32; 2]> = reader
196                .read_tex_coords(0)
197                .map(|iter| iter.into_f32().collect())
198                .unwrap_or_default();
199
200            let prim_indices: Vec<u32> = reader
201                .read_indices()
202                .map(|iter| iter.into_u32().collect())
203                .unwrap_or_default();
204
205            // Compute tangents
206            let tangents = if let Some(tan) = reader.read_tangents() {
207                tan.collect::<Vec<[f32; 4]>>()
208            } else {
209                engvis_core::math::compute_tangents(&positions, &normals, &uvs, &prim_indices)
210            };
211
212           let base_vertex = vertices.len() as u32;
213           for (i, &pos) in positions.iter().enumerate() {
214               vertices.push(MeshVertex {
215                   position: pos,
216                   normal: normals.get(i).copied().unwrap_or([0.0, 1.0, 0.0]),
217                   uv: uvs.get(i).copied().unwrap_or([0.0, 0.0]),
218                   tangent: tangents.get(i).copied().unwrap_or([1.0, 0.0, 0.0, 1.0]),
219               });
220               aabb.expand(Vec3::from(pos));
221            }
222
223            let index_offset = indices.len() as u32;
224            let adjusted_indices: Vec<u32> =
225                prim_indices.iter().map(|i| i + base_vertex).collect();
226            indices.extend_from_slice(&adjusted_indices);
227
228            sub_meshes.push(SubMesh {
229                material_index: primitive.material().index().unwrap_or(0),
230                index_offset,
231                index_count: adjusted_indices.len() as u32,
232            });
233        }
234
235        meshes.push(Mesh {
236            name: gltf_mesh.name().unwrap_or("unnamed").to_string(),
237            vertices,
238            indices,
239            sub_meshes,
240            aabb,
241        });
242    }
243
244    // 3. Load node hierarchy
245    fn load_node(node: &gltf::Node) -> SceneNode {
246        let (translation, rotation, scale) = node.transform().decomposed();
247        let local_transform = Affine3A::from_scale_rotation_translation(
248            Vec3::from(scale),
249            Quat::from_array(rotation),
250            Vec3::from(translation),
251        );
252
253        SceneNode {
254            name: node.name().unwrap_or("node").to_string(),
255            local_transform,
256            mesh_index: node.mesh().map(|m| m.index()),
257            children: node.children().map(|child| load_node(&child)).collect(),
258            visible: true,
259        }
260    }
261
262    for gltf_scene in gltf.scenes() {
263        for node in gltf_scene.nodes() {
264            nodes.push(load_node(&node));
265        }
266    }
267
268    let scene = Scene {
269        meshes,
270        materials,
271        nodes,
272        lighting: LightingEnvironment::default(),
273    };
274
275    let aabb = scene.compute_aabb();
276
277    Ok((scene, aabb))
278}