1use crate::WriteError;
14use animsmith_core::model::{Document, Interpolation, Property, TrackValues};
15use base64::Engine as _;
16use serde_json::{Value, json};
17use std::path::Path;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub struct WriteSummary {
30 pub nodes: usize,
32 pub animations: usize,
34 pub meshes: usize,
36 pub primitive_positions: usize,
38 pub materials: usize,
40 pub clips_without_writable_tracks: usize,
42}
43
44struct BufferBuilder {
45 bytes: Vec<u8>,
46 views: Vec<Value>,
47 accessors: Vec<Value>,
48}
49
50impl BufferBuilder {
51 fn new() -> Self {
52 Self {
53 bytes: Vec::new(),
54 views: Vec::new(),
55 accessors: Vec::new(),
56 }
57 }
58
59 fn push(&mut self, data: &[f32], kind: &str, with_min_max: bool) -> usize {
62 let components = match kind {
63 "SCALAR" => 1,
64 "VEC2" => 2,
65 "VEC3" => 3,
66 "MAT4" => 16,
67 _ => 4,
68 };
69 let offset = self.bytes.len();
70 for v in data {
71 self.bytes.extend_from_slice(&v.to_le_bytes());
72 }
73 let view = self.views.len();
74 self.views.push(json!({
75 "buffer": 0,
76 "byteOffset": offset,
77 "byteLength": data.len() * 4,
78 }));
79 let mut accessor = json!({
80 "bufferView": view,
81 "componentType": 5126,
82 "count": data.len() / components,
83 "type": kind,
84 });
85 if with_min_max && !data.is_empty() {
86 let mut min = vec![f32::MAX; components];
88 let mut max = vec![f32::MIN; components];
89 for (i, v) in data.iter().enumerate() {
90 let c = i % components;
91 min[c] = min[c].min(*v);
92 max[c] = max[c].max(*v);
93 }
94 accessor["min"] = json!(min);
95 accessor["max"] = json!(max);
96 }
97 let index = self.accessors.len();
98 self.accessors.push(accessor);
99 index
100 }
101}
102
103impl BufferBuilder {
104 fn push_indices(&mut self, data: &[u32]) -> usize {
106 let offset = self.bytes.len();
107 for v in data {
108 self.bytes.extend_from_slice(&v.to_le_bytes());
109 }
110 let view = self.views.len();
111 self.views.push(json!({
112 "buffer": 0,
113 "byteOffset": offset,
114 "byteLength": data.len() * 4,
115 }));
116 let index = self.accessors.len();
117 self.accessors.push(json!({
118 "bufferView": view,
119 "componentType": 5125,
120 "count": data.len(),
121 "type": "SCALAR",
122 }));
123 index
124 }
125
126 fn push_view(&mut self, data: &[u8]) -> usize {
128 while !self.bytes.len().is_multiple_of(4) {
129 self.bytes.push(0);
130 }
131 let offset = self.bytes.len();
132 self.bytes.extend_from_slice(data);
133 let view = self.views.len();
134 self.views.push(json!({
135 "buffer": 0,
136 "byteOffset": offset,
137 "byteLength": data.len(),
138 }));
139 view
140 }
141
142 fn push_u16(&mut self, data: &[u16], kind: &str) -> usize {
144 let components = if kind == "VEC4" { 4 } else { 1 };
145 let offset = self.bytes.len();
146 for v in data {
147 self.bytes.extend_from_slice(&v.to_le_bytes());
148 }
149 while !self.bytes.len().is_multiple_of(4) {
150 self.bytes.push(0);
151 }
152 let view = self.views.len();
153 self.views.push(json!({
154 "buffer": 0,
155 "byteOffset": offset,
156 "byteLength": data.len() * 2,
157 }));
158 let index = self.accessors.len();
159 self.accessors.push(json!({
160 "bufferView": view,
161 "componentType": 5123,
162 "count": data.len() / components,
163 "type": kind,
164 }));
165 index
166 }
167}
168
169fn document_to_json(doc: &Document, buffer_uri: Option<String>, buffer_len: usize) -> Value {
170 let mut nodes: Vec<Value> = Vec::with_capacity(doc.skeleton.bones.len());
171 for (id, bone) in doc.skeleton.bones.iter().enumerate() {
172 let children: Vec<usize> = doc
173 .skeleton
174 .bones
175 .iter()
176 .enumerate()
177 .filter(|(_, b)| b.parent == Some(id))
178 .map(|(i, _)| i)
179 .collect();
180 let mut node = json!({
181 "name": bone.name,
182 "translation": bone.rest.translation.to_array(),
183 "rotation": bone.rest.rotation.to_array(),
184 "scale": bone.rest.scale.to_array(),
185 });
186 if !children.is_empty() {
187 node["children"] = json!(children);
188 }
189 nodes.push(node);
190 }
191 let roots: Vec<usize> = doc
192 .skeleton
193 .bones
194 .iter()
195 .enumerate()
196 .filter(|(_, b)| b.parent.is_none())
197 .map(|(i, _)| i)
198 .collect();
199
200 let mut root = json!({
201 "asset": {
202 "version": "2.0",
203 "generator": format!("animsmith {}", env!("CARGO_PKG_VERSION")),
204 },
205 "scene": 0,
206 "scenes": [{ "nodes": roots }],
207 "nodes": nodes,
208 });
209 if buffer_len > 0 {
216 let mut buffer = json!({ "byteLength": buffer_len });
217 if let Some(uri) = buffer_uri {
218 buffer["uri"] = json!(uri);
219 }
220 root["buffers"] = json!([buffer]);
221 }
222 root
223}
224
225fn glb_len_u32(field: &'static str, len: usize) -> Result<u32, WriteError> {
229 u32::try_from(len).map_err(|_| WriteError::TooLarge { field, bytes: len })
230}
231
232#[derive(Debug)]
234struct GlbLengths {
235 total: u32,
237 json: u32,
239 bin: Option<u32>,
242}
243
244fn plan_glb_lengths(json_len: usize, bin_len: usize) -> Result<GlbLengths, WriteError> {
251 let json = glb_len_u32("JSON chunk", json_len)?;
252 let (bin, bin_bytes) = if bin_len > 0 {
253 (Some(glb_len_u32("BIN chunk", bin_len)?), 8 + bin_len)
254 } else {
255 (None, 0)
256 };
257 let total = glb_len_u32("total GLB length", 12 + 8 + json_len + bin_bytes)?;
258 Ok(GlbLengths { total, json, bin })
259}
260
261pub fn write(doc: &Document, path: &Path) -> Result<WriteSummary, WriteError> {
275 let assets = &doc.assets;
276 let mut buffers = BufferBuilder::new();
277 let mut animations: Vec<Value> = Vec::new();
278 let mut clips_without_writable_tracks = 0usize;
279
280 for clip in &doc.clips {
281 let mut samplers: Vec<Value> = Vec::new();
282 let mut channels: Vec<Value> = Vec::new();
283 for track in &clip.tracks {
284 if track.times.is_empty() || track.bone >= doc.skeleton.bones.len() {
285 continue;
286 }
287 let input = buffers.push(&track.times, "SCALAR", true);
288 let output = match &track.values {
289 TrackValues::Vec3s(v) => {
290 let flat: Vec<f32> = v.iter().flat_map(|x| x.to_array()).collect();
291 buffers.push(&flat, "VEC3", false)
292 }
293 TrackValues::Quats(v) => {
294 let flat: Vec<f32> = v.iter().flat_map(|q| q.to_array()).collect();
295 buffers.push(&flat, "VEC4", false)
296 }
297 };
298 let interpolation = match track.interpolation {
299 Interpolation::Linear => "LINEAR",
300 Interpolation::Step => "STEP",
301 Interpolation::CubicSpline => "CUBICSPLINE",
302 };
303 let target_path = match track.property {
304 Property::Translation => "translation",
305 Property::Rotation => "rotation",
306 Property::Scale => "scale",
307 };
308 let sampler = samplers.len();
309 samplers.push(json!({
310 "input": input,
311 "output": output,
312 "interpolation": interpolation,
313 }));
314 channels.push(json!({
315 "sampler": sampler,
316 "target": {
317 "node": track.bone,
318 "path": target_path,
319 },
320 }));
321 }
322 if channels.is_empty() {
323 clips_without_writable_tracks += 1;
324 } else {
325 animations.push(json!({
326 "name": clip.name,
327 "samplers": samplers,
328 "channels": channels,
329 }));
330 }
331 }
332
333 let mut meshes_json: Vec<Value> = Vec::new();
334 let mut skins_json: Vec<Value> = Vec::new();
335 let mut node_attach: Vec<(usize, usize, Option<usize>)> = Vec::new();
337 for mesh in &assets.meshes {
338 let mut prims: Vec<Value> = Vec::new();
339 for prim in &mesh.primitives {
340 let flat: Vec<f32> = prim.positions.iter().flat_map(|v| v.to_array()).collect();
341 let mut attributes = json!({
342 "POSITION": buffers.push(&flat, "VEC3", true),
344 });
345 if !prim.normals.is_empty() {
346 let flat: Vec<f32> = prim.normals.iter().flat_map(|v| v.to_array()).collect();
347 attributes["NORMAL"] = json!(buffers.push(&flat, "VEC3", false));
348 }
349 if !prim.uvs.is_empty() {
350 let flat: Vec<f32> = prim.uvs.iter().flatten().copied().collect();
351 attributes["TEXCOORD_0"] = json!(buffers.push(&flat, "VEC2", false));
352 }
353 if !prim.joints.is_empty() {
354 let flat_j: Vec<u16> = prim.joints.iter().flatten().copied().collect();
355 attributes["JOINTS_0"] = json!(buffers.push_u16(&flat_j, "VEC4"));
356 let flat_w: Vec<f32> = prim.weights.iter().flatten().copied().collect();
357 attributes["WEIGHTS_0"] = json!(buffers.push(&flat_w, "VEC4", false));
358 }
359 let mut value = json!({ "attributes": attributes });
360 if !prim.indices.is_empty() {
361 value["indices"] = json!(buffers.push_indices(&prim.indices));
362 }
363 if let Some(material) = prim.material {
364 value["material"] = json!(material);
365 }
366 prims.push(value);
367 }
368 let mesh_index = meshes_json.len();
369 meshes_json.push(json!({ "name": mesh.name, "primitives": prims }));
370
371 let skin_index = if mesh.skin_joints.is_empty() {
372 None
373 } else {
374 let mut ibms: Vec<f32> = Vec::with_capacity(mesh.skin_joints.len() * 16);
375 for (slot, &joint) in mesh.skin_joints.iter().enumerate() {
376 let m = mesh
377 .skin_ibms
378 .get(slot)
379 .copied()
380 .or_else(|| doc.skeleton.bones.get(joint).and_then(|b| b.inverse_bind))
381 .unwrap_or(glam::Mat4::IDENTITY);
382 ibms.extend_from_slice(&m.to_cols_array());
383 }
384 let accessor = buffers.push(&ibms, "MAT4", false);
385 let index = skins_json.len();
386 skins_json.push(json!({
387 "joints": mesh.skin_joints,
388 "inverseBindMatrices": accessor,
389 }));
390 Some(index)
391 };
392 node_attach.push((mesh.node, mesh_index, skin_index));
399 }
400
401 let mut images_json: Vec<Value> = Vec::new();
404 let mut textures_json: Vec<Value> = Vec::new();
405 let mut material_texture_index: Vec<Option<usize>> = vec![None; assets.materials.len()];
406 for (mi, material) in assets.materials.iter().enumerate() {
407 if let Some(texture) = &material.base_color_texture {
408 let view = buffers.push_view(&texture.bytes);
409 let image = images_json.len();
410 images_json.push(json!({ "bufferView": view, "mimeType": texture.mime }));
411 material_texture_index[mi] = Some(textures_json.len());
412 textures_json.push(json!({ "source": image }));
413 }
414 }
415
416 let binary = path
417 .extension()
418 .and_then(|e| e.to_str())
419 .is_some_and(|e| e.eq_ignore_ascii_case("glb"));
420
421 let uri = if binary {
422 None
423 } else {
424 Some(format!(
425 "data:application/octet-stream;base64,{}",
426 base64::engine::general_purpose::STANDARD.encode(&buffers.bytes)
427 ))
428 };
429 let mut root = document_to_json(doc, uri, buffers.bytes.len());
430 if !buffers.views.is_empty() {
433 root["bufferViews"] = Value::Array(buffers.views);
434 }
435 if !buffers.accessors.is_empty() {
436 root["accessors"] = Value::Array(buffers.accessors);
437 }
438 if !animations.is_empty() {
439 root["animations"] = Value::Array(animations);
440 }
441 if !meshes_json.is_empty() {
442 for (node, mesh_index, skin_index) in &node_attach {
443 match skin_index {
444 Some(skin) => {
445 let nodes = root["nodes"].as_array_mut().expect("nodes array");
446 let holder = nodes.len();
447 nodes.push(json!({
448 "name": format!("{}_skinned", assets.meshes[*mesh_index].name),
449 "mesh": mesh_index,
450 "skin": skin,
451 }));
452 root["scenes"][0]["nodes"]
453 .as_array_mut()
454 .expect("scene roots")
455 .push(json!(holder));
456 }
457 None => {
458 let node_value = &mut root["nodes"][*node];
459 node_value["mesh"] = json!(mesh_index);
460 }
461 }
462 }
463 root["meshes"] = Value::Array(meshes_json);
464 if !skins_json.is_empty() {
465 root["skins"] = Value::Array(skins_json);
466 }
467 if !assets.materials.is_empty() {
468 root["materials"] = Value::Array(
469 assets
470 .materials
471 .iter()
472 .enumerate()
473 .map(|(mi, m)| {
474 let mut pbr = json!({
475 "baseColorFactor": m.base_color,
476 "metallicFactor": m.metallic,
477 "roughnessFactor": m.roughness,
478 });
479 if let Some(slot) = material_texture_index[mi] {
480 pbr["baseColorTexture"] = json!({ "index": slot });
481 }
482 json!({ "name": m.name, "pbrMetallicRoughness": pbr })
483 })
484 .collect(),
485 );
486 if !images_json.is_empty() {
487 root["images"] = Value::Array(images_json);
488 root["textures"] = Value::Array(textures_json);
489 }
490 }
491 }
492
493 let array_len = |key: &str| root.get(key).and_then(Value::as_array).map_or(0, Vec::len);
494 let primitive_positions = root
495 .get("meshes")
496 .and_then(Value::as_array)
497 .into_iter()
498 .flatten()
499 .filter_map(|mesh| mesh.get("primitives").and_then(Value::as_array))
500 .flatten()
501 .filter_map(|primitive| {
502 primitive
503 .pointer("/attributes/POSITION")
504 .and_then(Value::as_u64)
505 .and_then(|index| usize::try_from(index).ok())
506 })
507 .filter_map(|index| {
508 root.get("accessors")
509 .and_then(Value::as_array)
510 .and_then(|accessors| accessors.get(index))
511 .and_then(|accessor| accessor.get("count"))
512 .and_then(Value::as_u64)
513 .and_then(|count| usize::try_from(count).ok())
514 })
515 .sum();
516 let animations = array_len("animations");
517 let summary = WriteSummary {
518 nodes: array_len("nodes"),
519 animations,
520 meshes: array_len("meshes"),
521 primitive_positions,
522 materials: array_len("materials"),
523 clips_without_writable_tracks,
524 };
525
526 let io_err = |e: std::io::Error| WriteError::Io {
527 path: path.display().to_string(),
528 source: e,
529 };
530 if binary {
531 let mut json_bytes = serde_json::to_vec(&root)?;
532 while !json_bytes.len().is_multiple_of(4) {
533 json_bytes.push(b' ');
534 }
535 let mut bin = buffers.bytes;
536 while !bin.len().is_multiple_of(4) {
537 bin.push(0);
538 }
539 let lengths = plan_glb_lengths(json_bytes.len(), bin.len())?;
542 let mut out = Vec::with_capacity(lengths.total as usize);
543 out.extend_from_slice(b"glTF");
544 out.extend_from_slice(&2u32.to_le_bytes());
545 out.extend_from_slice(&lengths.total.to_le_bytes());
546 out.extend_from_slice(&lengths.json.to_le_bytes());
547 out.extend_from_slice(b"JSON");
548 out.extend_from_slice(&json_bytes);
549 if let Some(bin_len) = lengths.bin {
552 out.extend_from_slice(&bin_len.to_le_bytes());
553 out.extend_from_slice(b"BIN\0");
554 out.extend_from_slice(&bin);
555 }
556 std::fs::write(path, out).map_err(io_err)?;
557 } else {
558 let text = serde_json::to_string_pretty(&root)?;
559 std::fs::write(path, text).map_err(io_err)?;
560 }
561 Ok(summary)
562}
563
564#[cfg(test)]
565mod tests {
566 use super::{GlbLengths, glb_len_u32, plan_glb_lengths};
567 use crate::WriteError;
568
569 #[test]
570 fn glb_len_u32_accepts_up_to_the_u32_limit() {
571 assert_eq!(glb_len_u32("x", 0).unwrap(), 0);
572 assert_eq!(glb_len_u32("x", 1234).unwrap(), 1234);
573 assert_eq!(glb_len_u32("x", u32::MAX as usize).unwrap(), u32::MAX);
574 }
575
576 #[test]
579 #[cfg(target_pointer_width = "64")]
580 fn glb_len_u32_rejects_over_4gib() {
581 let too_big = u32::MAX as usize + 1;
582 let err = glb_len_u32("total GLB length", too_big).unwrap_err();
583 let msg = err.to_string();
584 assert!(
585 matches!(err, WriteError::TooLarge { field: "total GLB length", bytes } if bytes == too_big),
586 "expected TooLarge naming the field and size"
587 );
588 assert!(
589 msg.contains("4 GiB") && msg.contains("total GLB length"),
590 "message must name the limit and field: {msg}"
591 );
592 }
593
594 #[test]
598 fn plan_glb_lengths_derives_the_three_fields() {
599 let GlbLengths { total, json, bin } = plan_glb_lengths(8, 16).unwrap();
600 assert_eq!((total, json, bin), (12 + 8 + 8 + 8 + 16, 8, Some(16)));
601 let GlbLengths { total, json, bin } = plan_glb_lengths(8, 0).unwrap();
603 assert_eq!((total, json, bin), (12 + 8 + 8, 8, None));
604 }
605
606 #[test]
611 #[cfg(target_pointer_width = "64")]
612 fn plan_glb_lengths_attributes_each_overflowing_field() {
613 let over = u32::MAX as usize + 1;
614 let ok = 8usize;
615 let field = |r: Result<GlbLengths, WriteError>| match r.unwrap_err() {
616 WriteError::TooLarge { field, .. } => field,
617 other => panic!("expected TooLarge, got {other:?}"),
618 };
619 assert_eq!(field(plan_glb_lengths(over, ok)), "JSON chunk");
620 assert_eq!(field(plan_glb_lengths(ok, over)), "BIN chunk");
621 let half = u32::MAX as usize / 2 + 1;
623 assert_eq!(field(plan_glb_lengths(half, half)), "total GLB length");
624 }
625}