Skip to main content

khora_io/asset/decoders/mesh/
gltf.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! GLTF mesh decoder with support for both embedded and external resources.
16
17use super::GltfResourceResolver;
18use anyhow::Result;
19use base64::Engine;
20use gltf::{mesh::Reader, Buffer};
21use khora_core::{
22    math::{geometry::Aabb, Vec2, Vec3, Vec4},
23    renderer::api::{
24        pipeline::enums::{PrimitiveTopology, VertexFormat},
25        pipeline::VertexAttributeDescriptor,
26        scene::Mesh,
27    },
28};
29use std::{error::Error, sync::Arc};
30
31use crate::asset::AssetDecoder;
32
33/// Decoder for GLTF meshes, configured with a resource resolver.
34#[derive(Clone)]
35pub struct GltfDecoder {
36    resolver: Arc<dyn GltfResourceResolver>,
37}
38
39impl GltfDecoder {
40    /// Creates a new GLTF decoder with the given resource resolver.
41    pub fn new(resolver: Arc<dyn GltfResourceResolver>) -> Self {
42        Self { resolver }
43    }
44}
45
46impl AssetDecoder<Mesh> for GltfDecoder {
47    fn load(&self, bytes: &[u8]) -> Result<Mesh, Box<dyn Error + Send + Sync>> {
48        let gltf = gltf::Gltf::from_slice(bytes)
49            .map_err(|e| format!("Failed to parse GLTF file: {}", e))?;
50
51        let buffer_data = self
52            .load_buffer_data(&gltf, &*self.resolver)
53            .map_err(|e| format!("Failed to load GLTF buffer data: {}", e))?;
54
55        let mesh = gltf
56            .document
57            .meshes()
58            .next()
59            .ok_or("No meshes found in GLTF file")?;
60        let primitive = mesh
61            .primitives()
62            .next()
63            .ok_or("No primitives found in mesh")?;
64
65        let get_buffer_data = |buffer: Buffer<'_>| Some(buffer_data[buffer.index()].as_slice());
66        let reader = primitive.reader(get_buffer_data);
67
68        let positions = self.extract_positions(&reader)?;
69        let normals = self.extract_normals(&reader);
70        let tex_coords = self.extract_tex_coords(&reader);
71        let tangents = self.extract_tangents(&reader);
72        let colors = self.extract_colors(&reader);
73        let indices = self.extract_indices(&reader);
74
75        let bounding_box = {
76            let bb = primitive.bounding_box();
77            let min = Vec3::new(bb.min[0], bb.min[1], bb.min[2]);
78            let max = Vec3::new(bb.max[0], bb.max[1], bb.max[2]);
79            Aabb::from_min_max(min, max)
80        };
81
82        let vertex_layout = self.build_vertex_layout(
83            normals.is_some(),
84            tex_coords.is_some(),
85            tangents.is_some(),
86            colors.is_some(),
87        );
88
89        Ok(Mesh {
90            positions,
91            normals,
92            tex_coords,
93            tangents,
94            colors,
95            indices,
96            primitive_type: self.map_primitive_type(primitive.mode()),
97            bounding_box,
98            vertex_layout,
99        })
100    }
101}
102
103impl GltfDecoder {
104    fn load_buffer_data(
105        &self,
106        gltf: &gltf::Gltf,
107        resolver: &dyn GltfResourceResolver,
108    ) -> Result<Vec<Vec<u8>>, Box<dyn Error + Send + Sync>> {
109        let mut buffer_data = Vec::new();
110        for buffer in gltf.buffers() {
111            match buffer.source() {
112                gltf::buffer::Source::Bin => {
113                    if let Some(blob) = gltf.blob.as_deref() {
114                        buffer_data.push(blob.to_vec());
115                    } else {
116                        return Err("GLB file references binary chunk but it is missing".into());
117                    }
118                }
119                gltf::buffer::Source::Uri(uri) => {
120                    if uri.starts_with("data:") {
121                        buffer_data.push(self.decode_data_uri(uri)?);
122                    } else {
123                        buffer_data.push(resolver.resolve_buffer(uri)?);
124                    }
125                }
126            }
127        }
128        Ok(buffer_data)
129    }
130
131    fn decode_data_uri(&self, uri: &str) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
132        let prefix = "data:application/octet-stream;base64,";
133        if let Some(base64_data) = uri.strip_prefix(prefix) {
134            base64::engine::general_purpose::STANDARD
135                .decode(base64_data)
136                .map_err(Into::into)
137        } else if let Some(base64_data) = uri.strip_prefix("data:application/gltf-buffer;base64,") {
138            base64::engine::general_purpose::STANDARD
139                .decode(base64_data)
140                .map_err(Into::into)
141        } else {
142            Err(format!("Unsupported data URI format: {}", uri).into())
143        }
144    }
145
146    fn extract_positions<'a, 's, F>(
147        &self,
148        reader: &Reader<'a, 's, F>,
149    ) -> Result<Vec<Vec3>, Box<dyn Error + Send + Sync>>
150    where
151        F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
152    {
153        reader
154            .read_positions()
155            .map(|iter| iter.map(|[x, y, z]| Vec3::new(x, y, z)).collect())
156            .ok_or_else(|| "Vertex positions attribute not found".into())
157    }
158
159    fn extract_normals<'a, 's, F>(&self, reader: &Reader<'a, 's, F>) -> Option<Vec<Vec3>>
160    where
161        F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
162    {
163        reader
164            .read_normals()
165            .map(|iter| iter.map(|[x, y, z]| Vec3::new(x, y, z)).collect())
166    }
167
168    fn extract_tex_coords<'a, 's, F>(&self, reader: &Reader<'a, 's, F>) -> Option<Vec<Vec2>>
169    where
170        F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
171    {
172        reader
173            .read_tex_coords(0)
174            .map(|iter| iter.into_f32().map(|[x, y]| Vec2::new(x, y)).collect())
175    }
176
177    fn extract_tangents<'a, 's, F>(&self, reader: &Reader<'a, 's, F>) -> Option<Vec<Vec4>>
178    where
179        F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
180    {
181        reader
182            .read_tangents()
183            .map(|iter| iter.map(|[x, y, z, w]| Vec4::new(x, y, z, w)).collect())
184    }
185
186    fn extract_colors<'a, 's, F>(&self, reader: &Reader<'a, 's, F>) -> Option<Vec<Vec4>>
187    where
188        F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
189    {
190        reader.read_colors(0).map(|iter| {
191            iter.into_rgba_f32()
192                .map(|[r, g, b, a]| Vec4::new(r, g, b, a))
193                .collect()
194        })
195    }
196
197    fn extract_indices<'a, 's, F>(&self, reader: &Reader<'a, 's, F>) -> Option<Vec<u32>>
198    where
199        F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
200    {
201        reader.read_indices().map(|iter| iter.into_u32().collect())
202    }
203
204    fn build_vertex_layout(
205        &self,
206        has_normals: bool,
207        has_tex_coords: bool,
208        has_tangents: bool,
209        has_colors: bool,
210    ) -> Vec<VertexAttributeDescriptor> {
211        let mut layout = Vec::new();
212        let mut shader_location = 0;
213        let mut offset = 0;
214
215        layout.push(VertexAttributeDescriptor {
216            shader_location,
217            format: VertexFormat::Float32x3,
218            offset: offset as u64,
219        });
220        shader_location += 1;
221        offset += std::mem::size_of::<Vec3>();
222
223        if has_normals {
224            layout.push(VertexAttributeDescriptor {
225                shader_location,
226                format: VertexFormat::Float32x3,
227                offset: offset as u64,
228            });
229            shader_location += 1;
230            offset += std::mem::size_of::<Vec3>();
231        }
232        if has_tex_coords {
233            layout.push(VertexAttributeDescriptor {
234                shader_location,
235                format: VertexFormat::Float32x2,
236                offset: offset as u64,
237            });
238            shader_location += 1;
239            offset += std::mem::size_of::<Vec2>();
240        }
241        if has_tangents {
242            layout.push(VertexAttributeDescriptor {
243                shader_location,
244                format: VertexFormat::Float32x4,
245                offset: offset as u64,
246            });
247            shader_location += 1;
248            offset += std::mem::size_of::<Vec4>();
249        }
250        if has_colors {
251            layout.push(VertexAttributeDescriptor {
252                shader_location,
253                format: VertexFormat::Float32x4,
254                offset: offset as u64,
255            });
256        }
257        layout
258    }
259
260    fn map_primitive_type(&self, mode: gltf::mesh::Mode) -> PrimitiveTopology {
261        match mode {
262            gltf::mesh::Mode::Triangles => PrimitiveTopology::TriangleList,
263            gltf::mesh::Mode::TriangleStrip => PrimitiveTopology::TriangleStrip,
264            gltf::mesh::Mode::Lines => PrimitiveTopology::LineList,
265            gltf::mesh::Mode::LineStrip => PrimitiveTopology::LineStrip,
266            gltf::mesh::Mode::Points => PrimitiveTopology::PointList,
267            _ => PrimitiveTopology::TriangleList,
268        }
269    }
270}