khora_io/asset/decoders/mesh/
gltf.rs1use 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#[derive(Clone)]
35pub struct GltfDecoder {
36 resolver: Arc<dyn GltfResourceResolver>,
37}
38
39impl GltfDecoder {
40 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}