1use std::collections::HashMap;
39use std::fmt;
40
41use serde_json::{json, Map, Value};
42
43use crate::presentation::{AlphaMode, GltfMaterial, Material};
44use crate::scene::{GeometryRef, Instance, RenderScene};
45
46#[derive(Debug, Clone, PartialEq)]
48#[non_exhaustive]
49pub struct GlbOptions {
50 pub y_up: bool,
53 pub origin_on_root: bool,
58}
59
60impl Default for GlbOptions {
61 fn default() -> Self {
62 Self {
63 y_up: true,
64 origin_on_root: true,
65 }
66 }
67}
68
69impl GlbOptions {
70 pub fn with_y_up(mut self, y_up: bool) -> Self {
72 self.y_up = y_up;
73 self
74 }
75
76 pub fn with_origin_on_root(mut self, origin_on_root: bool) -> Self {
78 self.origin_on_root = origin_on_root;
79 self
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum GlbError {
87 TooLarge { bytes: u64 },
89}
90
91impl fmt::Display for GlbError {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 match self {
94 Self::TooLarge { bytes } => {
95 write!(f, "GLB would be {bytes} bytes, more than 4 GiB")
96 }
97 }
98 }
99}
100
101impl std::error::Error for GlbError {}
102
103const ARRAY_BUFFER: u32 = 34962;
104const ELEMENT_ARRAY_BUFFER: u32 = 34963;
105const FLOAT: u32 = 5126;
106const UNSIGNED_INT: u32 = 5125;
107const POINTS: u32 = 0;
108const LINES: u32 = 1;
109const TRIANGLES: u32 = 4;
110
111const Z_UP_TO_Y_UP: [f64; 4] = [
113 -std::f64::consts::FRAC_1_SQRT_2,
114 0.0,
115 0.0,
116 std::f64::consts::FRAC_1_SQRT_2,
117];
118
119pub fn to_glb(scene: &RenderScene, options: &GlbOptions) -> Result<Vec<u8>, GlbError> {
137 let mut writer = Writer::default();
138 let json = writer.document(scene, options);
139 let mut json = serde_json::to_vec(&json).expect("JSON values always serialize");
140 pad(&mut json, b' ');
141 let bin = writer.bin;
142
143 let bin_chunk = if bin.is_empty() {
144 0
145 } else {
146 8 + bin.len() as u64
147 };
148 let total = 12 + 8 + json.len() as u64 + bin_chunk;
149 let total = u32::try_from(total).map_err(|_| GlbError::TooLarge { bytes: total })?;
150
151 let mut out = Vec::with_capacity(total as usize);
152 out.extend_from_slice(b"glTF");
153 out.extend_from_slice(&2u32.to_le_bytes());
154 out.extend_from_slice(&total.to_le_bytes());
155 out.extend_from_slice(&(json.len() as u32).to_le_bytes());
156 out.extend_from_slice(b"JSON");
157 out.extend_from_slice(&json);
158 if !bin.is_empty() {
159 out.extend_from_slice(&(bin.len() as u32).to_le_bytes());
160 out.extend_from_slice(b"BIN\0");
161 out.extend_from_slice(&bin);
162 }
163 Ok(out)
164}
165
166fn pad(bytes: &mut Vec<u8>, with: u8) {
167 while bytes.len() % 4 != 0 {
168 bytes.push(with);
169 }
170}
171
172fn num(v: f32) -> Value {
175 json!(f64::from(v))
176}
177
178fn clamp01(v: f32) -> f32 {
179 if v.is_nan() {
180 0.0
181 } else {
182 v.clamp(0.0, 1.0)
183 }
184}
185
186#[derive(Clone, Copy)]
188struct Written {
189 position: usize,
190 normal: Option<usize>,
191 color: Option<usize>,
192 indices: Option<usize>,
193 mode: u32,
194}
195
196#[derive(Default)]
197struct Writer {
198 bin: Vec<u8>,
199 views: Vec<Value>,
200 accessors: Vec<Value>,
201}
202
203impl Writer {
204 fn view(&mut self, bytes: impl IntoIterator<Item = [u8; 4]>, target: u32) -> usize {
205 pad(&mut self.bin, 0);
206 let offset = self.bin.len();
207 for b in bytes {
208 self.bin.extend_from_slice(&b);
209 }
210 self.views.push(json!({
211 "buffer": 0,
212 "byteOffset": offset,
213 "byteLength": self.bin.len() - offset,
214 "target": target,
215 }));
216 self.views.len() - 1
217 }
218
219 fn vec3(&mut self, values: &[[f32; 3]], min_max: bool) -> usize {
220 let view = self.view(
221 values.iter().flatten().map(|v| v.to_le_bytes()),
222 ARRAY_BUFFER,
223 );
224 let mut accessor = json!({
225 "bufferView": view,
226 "componentType": FLOAT,
227 "count": values.len(),
228 "type": "VEC3",
229 });
230 if min_max {
231 let mut min = [f32::INFINITY; 3];
232 let mut max = [f32::NEG_INFINITY; 3];
233 for v in values {
234 for k in 0..3 {
235 min[k] = min[k].min(v[k]);
236 max[k] = max[k].max(v[k]);
237 }
238 }
239 accessor["min"] = json!(min.map(num));
240 accessor["max"] = json!(max.map(num));
241 }
242 self.accessors.push(accessor);
243 self.accessors.len() - 1
244 }
245
246 fn indices(&mut self, indices: &[u32]) -> usize {
247 let view = self.view(
248 indices.iter().map(|i| i.to_le_bytes()),
249 ELEMENT_ARRAY_BUFFER,
250 );
251 self.accessors.push(json!({
252 "bufferView": view,
253 "componentType": UNSIGNED_INT,
254 "count": indices.len(),
255 "type": "SCALAR",
256 }));
257 self.accessors.len() - 1
258 }
259
260 fn geometry(&mut self, scene: &RenderScene, geometry: GeometryRef) -> Written {
261 match geometry {
262 GeometryRef::Mesh(i) => {
263 let m = &scene.meshes[i];
264 Written {
265 position: self.vec3(&m.positions, true),
266 normal: Some(self.vec3(&m.normals, false)),
267 color: None,
268 indices: Some(self.indices(&m.indices)),
269 mode: TRIANGLES,
270 }
271 }
272 GeometryRef::Lines(i) => {
273 let l = &scene.lines[i];
274 Written {
275 position: self.vec3(&l.positions, true),
276 normal: None,
277 color: None,
278 indices: Some(self.indices(&l.indices)),
279 mode: LINES,
280 }
281 }
282 GeometryRef::Points(i) => {
283 let p = &scene.points[i];
284 let colors = p.colors.as_ref().map(|colors| {
285 let clamped: Vec<[f32; 3]> = colors.iter().map(|c| c.map(clamp01)).collect();
286 self.vec3(&clamped, false)
287 });
288 Written {
289 position: self.vec3(&p.positions, true),
290 normal: None,
291 color: colors,
292 indices: None,
293 mode: POINTS,
294 }
295 }
296 }
297 }
298
299 fn document(&mut self, scene: &RenderScene, options: &GlbOptions) -> Value {
300 let mut written: HashMap<GeometryRef, Written> = HashMap::new();
301 let mut meshes: Vec<Value> = Vec::new();
302 let mut mesh_ids: HashMap<(GeometryRef, usize), usize> = HashMap::new();
303 let mut materials: Vec<Value> = Vec::new();
305 let mut material_ids: HashMap<usize, usize> = HashMap::new();
306 let mut nodes: Vec<Value> = vec![Value::Null];
307 for instance in &scene.instances {
308 let w = *written
309 .entry(instance.geometry)
310 .or_insert_with(|| self.geometry(scene, instance.geometry));
311 let mesh = *mesh_ids
312 .entry((instance.geometry, instance.material))
313 .or_insert_with(|| {
314 let material_id = *material_ids.entry(instance.material).or_insert_with(|| {
315 materials.push(material(&scene.materials[instance.material]));
316 materials.len() - 1
317 });
318 meshes.push(mesh(&w, material_id));
319 meshes.len() - 1
320 });
321 nodes.push(instance_node(instance, mesh));
322 }
323
324 let mut root = Map::new();
325 root.insert("name".into(), json!("IFCX"));
326 if nodes.len() > 1 {
327 root.insert(
328 "children".into(),
329 json!((1..nodes.len()).collect::<Vec<_>>()),
330 );
331 }
332 let o = scene.origin;
333 let translation = match (options.origin_on_root, options.y_up) {
334 (false, _) => [0.0; 3],
335 (true, false) => o,
336 (true, true) => [o[0], o[2], -o[1]],
337 };
338 if translation != [0.0; 3] {
339 root.insert("translation".into(), json!(translation));
340 }
341 if options.y_up {
342 root.insert("rotation".into(), json!(Z_UP_TO_Y_UP));
343 }
344 root.insert(
345 "extras".into(),
346 json!({"ifcxOrigin": o, "ifcxUpAxis": if options.y_up { "Y" } else { "Z" }}),
347 );
348 nodes[0] = Value::Object(root);
349
350 let mut document = json!({
351 "asset": {
352 "version": "2.0",
353 "generator": concat!("openbim-ifcx-geometry ", env!("CARGO_PKG_VERSION")),
354 },
355 "scene": 0,
356 "scenes": [{"name": "IFCX", "nodes": [0]}],
357 "nodes": nodes,
358 });
359 if !meshes.is_empty() {
360 document["meshes"] = Value::Array(meshes);
361 document["materials"] = Value::Array(materials);
362 document["accessors"] = Value::Array(std::mem::take(&mut self.accessors));
363 document["bufferViews"] = Value::Array(std::mem::take(&mut self.views));
364 pad(&mut self.bin, 0);
365 document["buffers"] = json!([{"byteLength": self.bin.len()}]);
366 }
367 document
368 }
369}
370
371fn mesh(w: &Written, material: usize) -> Value {
372 let mut attributes = Map::new();
373 attributes.insert("POSITION".into(), json!(w.position));
374 if let Some(n) = w.normal {
375 attributes.insert("NORMAL".into(), json!(n));
376 }
377 if let Some(c) = w.color {
378 attributes.insert("COLOR_0".into(), json!(c));
379 }
380 let mut primitive = json!({"attributes": attributes, "material": material, "mode": w.mode});
381 if let Some(i) = w.indices {
382 primitive["indices"] = json!(i);
383 }
384 json!({"primitives": [primitive]})
385}
386
387fn instance_node(instance: &Instance, mesh: usize) -> Value {
388 let mut node = json!({
389 "name": instance.path,
390 "mesh": mesh,
391 "extras": {"ifcxNode": instance.node},
392 });
393 const IDENTITY: [f32; 16] = [
394 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
395 ];
396 if instance.matrix != IDENTITY {
397 node["matrix"] = json!(instance.matrix.map(num));
398 }
399 node
400}
401
402fn material(material: &Material) -> Value {
403 match material {
404 Material::Basic(basic) => {
405 let [r, g, b] = basic.color.map(clamp01);
406 let a = clamp01(basic.opacity);
407 let mut m = json!({
408 "pbrMetallicRoughness": {
409 "baseColorFactor": [num(r), num(g), num(b), num(a)],
410 "metallicFactor": 0.0,
411 "roughnessFactor": 1.0,
412 },
413 });
414 if a < 1.0 {
415 m["alphaMode"] = json!("BLEND");
416 }
417 m
418 }
419 Material::Pbr(pbr) => pbr_material(pbr),
420 }
421}
422
423fn pbr_material(pbr: &GltfMaterial) -> Value {
424 let mut m = json!({
425 "pbrMetallicRoughness": {
426 "baseColorFactor": pbr.base_color_factor.map(clamp01).map(num),
427 "metallicFactor": num(clamp01(pbr.metallic_factor)),
428 "roughnessFactor": num(clamp01(pbr.roughness_factor)),
429 },
430 });
431 if pbr.emissive_factor != [0.0; 3] {
432 m["emissiveFactor"] = json!(pbr.emissive_factor.map(clamp01).map(num));
433 }
434 match pbr.alpha_mode {
435 AlphaMode::Opaque => {}
436 AlphaMode::Blend => m["alphaMode"] = json!("BLEND"),
437 AlphaMode::Mask => {
438 m["alphaMode"] = json!("MASK");
439 let cutoff = if pbr.alpha_cutoff.is_nan() {
441 0.5
442 } else {
443 pbr.alpha_cutoff.clamp(0.0, f32::MAX)
444 };
445 m["alphaCutoff"] = num(cutoff);
446 }
447 }
448 if pbr.double_sided {
449 m["doubleSided"] = json!(true);
450 }
451 m
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn chunks(glb: &[u8]) -> (Value, &[u8]) {
459 let u32_at = |i: usize| u32::from_le_bytes(glb[i..i + 4].try_into().unwrap()) as usize;
460 assert_eq!(&glb[0..4], b"glTF");
461 assert_eq!(u32_at(4), 2);
462 assert_eq!(u32_at(8), glb.len());
463 let json_len = u32_at(12);
464 assert_eq!(&glb[16..20], b"JSON");
465 let json = serde_json::from_slice(&glb[20..20 + json_len]).unwrap();
466 let rest = &glb[20 + json_len..];
467 if rest.is_empty() {
468 return (json, rest);
469 }
470 assert_eq!(&rest[4..8], b"BIN\0");
471 let bin_len = u32::from_le_bytes(rest[0..4].try_into().unwrap()) as usize;
472 assert_eq!(rest.len(), 8 + bin_len);
473 (json, &rest[8..])
474 }
475
476 #[test]
477 fn empty_scene_has_only_a_root() {
478 let glb = to_glb(&RenderScene::default(), &GlbOptions::default()).unwrap();
479 assert_eq!(glb.len() % 4, 0);
480 let (json, bin) = chunks(&glb);
481 assert!(bin.is_empty());
482 assert_eq!(json["nodes"].as_array().unwrap().len(), 1);
483 assert!(json.get("buffers").is_none());
484 assert!(json["nodes"][0].get("translation").is_none());
485 }
486
487 #[test]
488 fn clamps_out_of_range_factors() {
489 let m = material(&Material::Basic(crate::BasicMaterial {
490 color: [1.5, -0.2, 0.5],
491 opacity: 0.25,
492 }));
493 assert_eq!(
494 m["pbrMetallicRoughness"]["baseColorFactor"],
495 json!([1.0, 0.0, 0.5, 0.25])
496 );
497 assert_eq!(m["alphaMode"], "BLEND");
498 }
499
500 #[test]
503 fn alpha_cutoff_is_always_a_finite_number() {
504 for (cutoff, written) in [
505 (f32::INFINITY, f64::from(f32::MAX)),
506 (f32::NEG_INFINITY, 0.0),
507 (f32::NAN, 0.5),
508 (0.25, 0.25),
509 ] {
510 let m = pbr_material(&GltfMaterial {
511 alpha_mode: AlphaMode::Mask,
512 alpha_cutoff: cutoff,
513 ..GltfMaterial::default()
514 });
515 assert_eq!(m["alphaCutoff"].as_f64(), Some(written), "{cutoff}");
516 }
517 }
518
519 #[test]
520 fn exact_f32_numbers() {
521 assert_eq!(num(0.1).as_f64().unwrap() as f32, 0.1f32);
522 assert_eq!(num(0.1).as_f64().unwrap(), f64::from(0.1f32));
523 }
524}