Skip to main content

box3d_rust/compound/
serialize.rs

1//! Compound blob serialization (convert to/from bytes).
2//!
3//! Port of `b3ConvertCompoundToBytes` / `b3ConvertBytesToCompound`.
4//!
5//! SPDX-FileCopyrightText: 2025 Erin Catto
6//! SPDX-License-Identifier: MIT
7
8use super::types::{
9    CompoundCapsule, CompoundData, CompoundSphere, HullInstance, MeshInstance,
10    COMPOUND_CONVEX_SIZE, COMPOUND_DATA_SIZE, COMPOUND_VERSION, DYNAMIC_TREE_SIZE,
11    HULL_INSTANCE_SIZE, MAX_COMPOUND_MESH_MATERIALS, MESH_INSTANCE_SIZE, TREE_NODE_SIZE,
12};
13use crate::core::NULL_INDEX;
14use crate::dynamic_tree::{DynamicTree, TreeNode, ALLOCATED_NODE, DYNAMIC_TREE_VERSION, LEAF_NODE};
15use crate::geometry::{SurfaceMaterial, SURFACE_MATERIAL_SIZE};
16use crate::hull::{HullData, HullFace, HullHalfEdge, HullVertex, HULL_DATA_SIZE, HULL_VERSION};
17use crate::math_functions::{
18    Aabb, Matrix3, Plane, Quat, Transform, Vec3, MAT3_ZERO, QUAT_IDENTITY, TRANSFORM_IDENTITY,
19    VEC3_ONE, VEC3_ZERO,
20};
21use crate::mesh::{
22    MeshData, MeshNode, MeshTriangle, MESH_DATA_SIZE, MESH_NODE_SIZE, MESH_TRIANGLE_SIZE,
23    MESH_VERSION,
24};
25
26fn write_u64(buf: &mut Vec<u8>, v: u64) {
27    buf.extend_from_slice(&v.to_le_bytes());
28}
29fn write_u32(buf: &mut Vec<u8>, v: u32) {
30    buf.extend_from_slice(&v.to_le_bytes());
31}
32fn write_i32(buf: &mut Vec<u8>, v: i32) {
33    buf.extend_from_slice(&v.to_le_bytes());
34}
35fn write_f32(buf: &mut Vec<u8>, v: f32) {
36    buf.extend_from_slice(&v.to_le_bytes());
37}
38fn write_vec3(buf: &mut Vec<u8>, v: Vec3) {
39    write_f32(buf, v.x);
40    write_f32(buf, v.y);
41    write_f32(buf, v.z);
42}
43fn write_quat(buf: &mut Vec<u8>, q: Quat) {
44    write_vec3(buf, q.v);
45    write_f32(buf, q.s);
46}
47fn write_transform(buf: &mut Vec<u8>, t: Transform) {
48    write_vec3(buf, t.p);
49    write_quat(buf, t.q);
50}
51fn write_aabb(buf: &mut Vec<u8>, a: Aabb) {
52    write_vec3(buf, a.lower_bound);
53    write_vec3(buf, a.upper_bound);
54}
55fn pad_to(buf: &mut Vec<u8>, len: usize) {
56    if buf.len() < len {
57        buf.resize(len, 0);
58    }
59}
60
61fn write_tree_node(buf: &mut Vec<u8>, node: &TreeNode) {
62    write_aabb(buf, node.aabb);
63    write_u64(buf, node.category_bits);
64    if node.flags & LEAF_NODE != 0 {
65        write_u64(buf, node.user_data);
66    } else {
67        write_i32(buf, node.child1);
68        write_i32(buf, node.child2);
69    }
70    // parent/next union: free nodes use next; allocated use parent
71    if node.flags & ALLOCATED_NODE != 0 {
72        write_i32(buf, node.parent);
73    } else {
74        write_i32(buf, node.next);
75    }
76    buf.extend_from_slice(&node.height.to_le_bytes());
77    buf.extend_from_slice(&node.flags.to_le_bytes());
78}
79
80fn write_dynamic_tree_header(buf: &mut Vec<u8>, tree: &DynamicTree, nodes_ptr_zeroed: bool) {
81    let start = buf.len();
82    write_u64(buf, tree.version());
83    // nodes pointer — scrubbed to null for serialization
84    if nodes_ptr_zeroed {
85        buf.extend_from_slice(&0u64.to_le_bytes());
86    } else {
87        buf.extend_from_slice(&0u64.to_le_bytes());
88    }
89    write_i32(buf, tree.root);
90    write_i32(buf, tree.node_count());
91    write_i32(buf, tree.node_capacity());
92    write_i32(buf, tree.proxy_count());
93    write_i32(buf, tree.free_list);
94    // pad to 8 for leafIndices*
95    pad_to(buf, start + 40);
96    // leafIndices, leafBoxes, leafCenters, binIndices — all null
97    buf.extend_from_slice(&0u64.to_le_bytes());
98    buf.extend_from_slice(&0u64.to_le_bytes());
99    buf.extend_from_slice(&0u64.to_le_bytes());
100    buf.extend_from_slice(&0u64.to_le_bytes());
101    write_i32(buf, tree.rebuild_capacity);
102    pad_to(buf, start + DYNAMIC_TREE_SIZE);
103}
104
105fn write_compound_header(buf: &mut Vec<u8>, c: &CompoundData) {
106    write_u64(buf, c.version);
107    write_i32(buf, c.byte_count);
108    write_i32(buf, c.node_offset);
109    write_dynamic_tree_header(buf, &c.tree, true);
110    write_i32(buf, c.material_offset);
111    write_i32(buf, c.material_count);
112    write_i32(buf, c.capsule_offset);
113    write_i32(buf, c.capsule_count);
114    write_i32(buf, c.hull_offset);
115    write_i32(buf, c.hull_count);
116    write_i32(buf, c.shared_hull_count);
117    write_i32(buf, c.mesh_offset);
118    write_i32(buf, c.mesh_count);
119    write_i32(buf, c.shared_mesh_count);
120    write_i32(buf, c.sphere_offset);
121    write_i32(buf, c.sphere_count);
122    debug_assert_eq!(buf.len(), COMPOUND_DATA_SIZE);
123}
124
125fn write_capsule(buf: &mut Vec<u8>, c: &CompoundCapsule) {
126    write_vec3(buf, c.capsule.center1);
127    write_vec3(buf, c.capsule.center2);
128    write_f32(buf, c.capsule.radius);
129    write_i32(buf, c.material_index);
130}
131
132fn write_sphere(buf: &mut Vec<u8>, s: &CompoundSphere) {
133    write_vec3(buf, s.sphere.center);
134    write_f32(buf, s.sphere.radius);
135    write_i32(buf, s.material_index);
136}
137
138fn write_hull_instance(buf: &mut Vec<u8>, h: &HullInstance) {
139    write_transform(buf, h.transform);
140    write_u32(buf, h.hull_offset);
141    write_u32(buf, h.material_index);
142}
143
144fn write_mesh_instance(buf: &mut Vec<u8>, m: &MeshInstance) {
145    write_transform(buf, m.transform);
146    write_vec3(buf, m.scale);
147    write_u32(buf, m.mesh_offset);
148    for i in 0..MAX_COMPOUND_MESH_MATERIALS {
149        write_u32(buf, m.material_indices[i]);
150    }
151}
152
153impl CompoundData {
154    /// Serialize to the C contiguous blob layout.
155    pub fn to_bytes(&self) -> Vec<u8> {
156        let mut buf = Vec::with_capacity(self.byte_count as usize);
157        write_compound_header(&mut buf, self);
158
159        pad_to(&mut buf, self.node_offset as usize);
160        for node in &self.tree.nodes {
161            write_tree_node(&mut buf, node);
162        }
163
164        pad_to(&mut buf, self.material_offset as usize);
165        for mat in &self.materials {
166            buf.extend_from_slice(&mat.to_bytes());
167        }
168
169        pad_to(&mut buf, self.capsule_offset as usize);
170        for cap in &self.capsules {
171            write_capsule(&mut buf, cap);
172        }
173
174        pad_to(&mut buf, self.hull_offset as usize);
175        for inst in &self.hull_instances {
176            write_hull_instance(&mut buf, inst);
177        }
178        // Shared hull blobs at their recorded offsets
179        for (i, hull) in self.shared_hulls.iter().enumerate() {
180            let offset = self
181                .hull_instances
182                .iter()
183                .find(|inst| inst.shared_index as usize == i)
184                .map(|inst| inst.hull_offset as usize)
185                .unwrap_or(0);
186            if offset > 0 {
187                pad_to(&mut buf, offset);
188                buf.extend_from_slice(&hull.to_bytes());
189            }
190        }
191
192        pad_to(&mut buf, self.mesh_offset as usize);
193        for inst in &self.mesh_instances {
194            write_mesh_instance(&mut buf, inst);
195        }
196        for (i, mesh) in self.shared_meshes.iter().enumerate() {
197            let offset = self
198                .mesh_instances
199                .iter()
200                .find(|inst| inst.shared_index as usize == i)
201                .map(|inst| inst.mesh_offset as usize)
202                .unwrap_or(0);
203            if offset > 0 {
204                pad_to(&mut buf, offset);
205                buf.extend_from_slice(&mesh.to_bytes());
206            }
207        }
208
209        pad_to(&mut buf, self.sphere_offset as usize);
210        for sph in &self.spheres {
211            write_sphere(&mut buf, sph);
212        }
213
214        pad_to(&mut buf, self.byte_count as usize);
215        debug_assert_eq!(buf.len(), self.byte_count as usize);
216        buf
217    }
218}
219
220/// Scrub and return the compound as a byte buffer. (b3ConvertCompoundToBytes)
221pub fn convert_compound_to_bytes(compound: &CompoundData) -> Vec<u8> {
222    compound.to_bytes()
223}
224
225fn read_u64(buf: &[u8], o: usize) -> u64 {
226    u64::from_le_bytes(buf[o..o + 8].try_into().unwrap())
227}
228fn read_u32(buf: &[u8], o: usize) -> u32 {
229    u32::from_le_bytes(buf[o..o + 4].try_into().unwrap())
230}
231fn read_i32(buf: &[u8], o: usize) -> i32 {
232    i32::from_le_bytes(buf[o..o + 4].try_into().unwrap())
233}
234fn read_f32(buf: &[u8], o: usize) -> f32 {
235    f32::from_le_bytes(buf[o..o + 4].try_into().unwrap())
236}
237fn read_vec3(buf: &[u8], o: usize) -> Vec3 {
238    Vec3 {
239        x: read_f32(buf, o),
240        y: read_f32(buf, o + 4),
241        z: read_f32(buf, o + 8),
242    }
243}
244fn read_quat(buf: &[u8], o: usize) -> Quat {
245    Quat {
246        v: read_vec3(buf, o),
247        s: read_f32(buf, o + 12),
248    }
249}
250fn read_transform(buf: &[u8], o: usize) -> Transform {
251    Transform {
252        p: read_vec3(buf, o),
253        q: read_quat(buf, o + 12),
254    }
255}
256fn read_aabb(buf: &[u8], o: usize) -> Aabb {
257    Aabb {
258        lower_bound: read_vec3(buf, o),
259        upper_bound: read_vec3(buf, o + 12),
260    }
261}
262
263fn read_tree_node(buf: &[u8], o: usize) -> TreeNode {
264    let aabb = read_aabb(buf, o);
265    let category_bits = read_u64(buf, o + 24);
266    let union8 = read_u64(buf, o + 32);
267    let parent_or_next = read_i32(buf, o + 40);
268    let height = u16::from_le_bytes(buf[o + 44..o + 46].try_into().unwrap());
269    let flags = u16::from_le_bytes(buf[o + 46..o + 48].try_into().unwrap());
270
271    let (child1, child2, user_data) = if flags & LEAF_NODE != 0 {
272        (NULL_INDEX, NULL_INDEX, union8)
273    } else {
274        let c1 = read_i32(buf, o + 32);
275        let c2 = read_i32(buf, o + 36);
276        (c1, c2, 0)
277    };
278
279    let (parent, next) = if flags & ALLOCATED_NODE != 0 {
280        (parent_or_next, NULL_INDEX)
281    } else {
282        (NULL_INDEX, parent_or_next)
283    };
284
285    TreeNode {
286        aabb,
287        category_bits,
288        child1,
289        child2,
290        user_data,
291        parent,
292        next,
293        height,
294        flags,
295    }
296}
297
298fn read_plane(buf: &[u8], o: usize) -> Plane {
299    Plane {
300        normal: read_vec3(buf, o),
301        offset: read_f32(buf, o + 12),
302    }
303}
304
305fn read_matrix3(buf: &[u8], o: usize) -> Matrix3 {
306    Matrix3 {
307        cx: read_vec3(buf, o),
308        cy: read_vec3(buf, o + 12),
309        cz: read_vec3(buf, o + 24),
310    }
311}
312
313fn read_hull_data(buf: &[u8]) -> Option<HullData> {
314    if buf.len() < HULL_DATA_SIZE {
315        return None;
316    }
317    let version = read_u64(buf, 0);
318    if version != HULL_VERSION {
319        return None;
320    }
321    let byte_count = read_i32(buf, 8);
322    if byte_count as usize > buf.len() || byte_count < HULL_DATA_SIZE as i32 {
323        return None;
324    }
325    let hash = read_u32(buf, 12);
326    let aabb = read_aabb(buf, 16);
327    let surface_area = read_f32(buf, 40);
328    let volume = read_f32(buf, 44);
329    let inner_radius = read_f32(buf, 48);
330    let center = read_vec3(buf, 52);
331    let central_inertia = read_matrix3(buf, 64);
332    let vertex_count = read_i32(buf, 100);
333    let vertex_offset = read_i32(buf, 104);
334    let point_offset = read_i32(buf, 108);
335    let edge_count = read_i32(buf, 112);
336    let edge_offset = read_i32(buf, 116);
337    let face_count = read_i32(buf, 120);
338    let face_offset = read_i32(buf, 124);
339    let plane_offset = read_i32(buf, 128);
340    let padding = read_i32(buf, 132);
341
342    let mut vertices = Vec::with_capacity(vertex_count as usize);
343    for i in 0..vertex_count as usize {
344        let o = vertex_offset as usize + i;
345        if o >= buf.len() {
346            return None;
347        }
348        vertices.push(HullVertex { edge: buf[o] });
349    }
350
351    let mut points = Vec::with_capacity(vertex_count as usize);
352    for i in 0..vertex_count as usize {
353        let o = point_offset as usize + i * 12;
354        points.push(read_vec3(buf, o));
355    }
356
357    let mut edges = Vec::with_capacity(edge_count as usize);
358    for i in 0..edge_count as usize {
359        let o = edge_offset as usize + i * 4;
360        edges.push(HullHalfEdge {
361            next: buf[o],
362            twin: buf[o + 1],
363            origin: buf[o + 2],
364            face: buf[o + 3],
365        });
366    }
367
368    let mut faces = Vec::with_capacity(face_count as usize);
369    for i in 0..face_count as usize {
370        let o = face_offset as usize + i;
371        faces.push(HullFace { edge: buf[o] });
372    }
373
374    let mut planes = Vec::with_capacity(face_count as usize);
375    for i in 0..face_count as usize {
376        let o = plane_offset as usize + i * 16;
377        planes.push(read_plane(buf, o));
378    }
379
380    Some(HullData {
381        version,
382        byte_count,
383        hash,
384        aabb,
385        surface_area,
386        volume,
387        inner_radius,
388        center,
389        central_inertia,
390        vertex_count,
391        vertex_offset,
392        point_offset,
393        edge_count,
394        edge_offset,
395        face_count,
396        face_offset,
397        plane_offset,
398        padding,
399        vertices,
400        points,
401        edges,
402        faces,
403        planes,
404    })
405}
406
407fn read_mesh_data(buf: &[u8]) -> Option<MeshData> {
408    if buf.len() < MESH_DATA_SIZE {
409        return None;
410    }
411    let version = read_u64(buf, 0);
412    if version != MESH_VERSION {
413        return None;
414    }
415    let byte_count = read_i32(buf, 8);
416    if byte_count as usize > buf.len() || byte_count < MESH_DATA_SIZE as i32 {
417        return None;
418    }
419    let hash = read_u32(buf, 12);
420    let bounds = read_aabb(buf, 16);
421    let surface_area = read_f32(buf, 40);
422    let tree_height = read_i32(buf, 44);
423    let degenerate_count = read_i32(buf, 48);
424    let node_offset = read_i32(buf, 52);
425    let node_count = read_i32(buf, 56);
426    let vertex_offset = read_i32(buf, 60);
427    let vertex_count = read_i32(buf, 64);
428    let triangle_offset = read_i32(buf, 68);
429    let triangle_count = read_i32(buf, 72);
430    let material_offset = read_i32(buf, 76);
431    let material_count = read_i32(buf, 80);
432    let flags_offset = read_i32(buf, 84);
433
434    let mut nodes = Vec::with_capacity(node_count as usize);
435    for i in 0..node_count as usize {
436        let o = node_offset as usize + i * MESH_NODE_SIZE;
437        nodes.push(MeshNode {
438            lower_bound: read_vec3(buf, o),
439            data: read_u32(buf, o + 12),
440            upper_bound: read_vec3(buf, o + 16),
441            triangle_offset: read_u32(buf, o + 28),
442        });
443    }
444
445    let mut vertices = Vec::with_capacity(vertex_count as usize);
446    for i in 0..vertex_count as usize {
447        vertices.push(read_vec3(buf, vertex_offset as usize + i * 12));
448    }
449
450    let mut triangles = Vec::with_capacity(triangle_count as usize);
451    for i in 0..triangle_count as usize {
452        let o = triangle_offset as usize + i * MESH_TRIANGLE_SIZE;
453        triangles.push(MeshTriangle {
454            index1: read_i32(buf, o),
455            index2: read_i32(buf, o + 4),
456            index3: read_i32(buf, o + 8),
457        });
458    }
459
460    let mat_start = material_offset as usize;
461    let material_indices = buf[mat_start..mat_start + material_count as usize].to_vec();
462    let flags_start = flags_offset as usize;
463    let flags = if flags_offset > 0 {
464        buf[flags_start..flags_start + triangle_count as usize].to_vec()
465    } else {
466        Vec::new()
467    };
468
469    Some(MeshData {
470        version,
471        byte_count,
472        hash,
473        bounds,
474        surface_area,
475        tree_height,
476        degenerate_count,
477        node_offset,
478        node_count,
479        vertex_offset,
480        vertex_count,
481        triangle_offset,
482        triangle_count,
483        material_offset,
484        material_count,
485        flags_offset,
486        nodes,
487        vertices,
488        triangles,
489        material_indices,
490        flags,
491    })
492}
493
494/// Restore a compound from a byte buffer. (b3ConvertBytesToCompound)
495pub fn convert_bytes_to_compound(bytes: &[u8]) -> Option<CompoundData> {
496    if bytes.len() < COMPOUND_DATA_SIZE {
497        return None;
498    }
499
500    let version = read_u64(bytes, 0);
501    if version != COMPOUND_VERSION {
502        return None;
503    }
504
505    let byte_count = read_i32(bytes, 8);
506    if byte_count < COMPOUND_DATA_SIZE as i32 {
507        return None;
508    }
509    if bytes.len() != byte_count as usize {
510        return None;
511    }
512
513    let node_offset = read_i32(bytes, 12);
514    if node_offset <= 0 {
515        return None;
516    }
517
518    // Tree header starts at offset 16
519    let tree_version = read_u64(bytes, 16);
520    let root = read_i32(bytes, 16 + 16);
521    let node_count = read_i32(bytes, 16 + 20);
522    let node_capacity = read_i32(bytes, 16 + 24);
523    let proxy_count = read_i32(bytes, 16 + 28);
524    let free_list = read_i32(bytes, 16 + 32);
525    let rebuild_capacity = read_i32(bytes, 16 + 72);
526
527    let material_offset = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE);
528    let material_count = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 4);
529    let capsule_offset = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 8);
530    let capsule_count = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 12);
531    let hull_offset = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 16);
532    let hull_count = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 20);
533    let shared_hull_count = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 24);
534    let mesh_offset = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 28);
535    let mesh_count = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 32);
536    let shared_mesh_count = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 36);
537    let sphere_offset = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 40);
538    let sphere_count = read_i32(bytes, 16 + DYNAMIC_TREE_SIZE + 44);
539
540    let mut nodes = Vec::with_capacity(node_capacity as usize);
541    for i in 0..node_capacity as usize {
542        let o = node_offset as usize + i * TREE_NODE_SIZE;
543        nodes.push(read_tree_node(bytes, o));
544    }
545
546    let mut tree = DynamicTree::new(0);
547    tree.version = if tree_version != 0 {
548        tree_version
549    } else {
550        DYNAMIC_TREE_VERSION
551    };
552    tree.nodes = nodes;
553    tree.root = root;
554    tree.node_count = node_count;
555    tree.free_list = free_list;
556    tree.proxy_count = proxy_count;
557    tree.rebuild_capacity = rebuild_capacity;
558
559    let mut materials = Vec::with_capacity(material_count as usize);
560    for i in 0..material_count as usize {
561        let o = material_offset as usize + i * SURFACE_MATERIAL_SIZE;
562        materials.push(SurfaceMaterial::from_bytes(
563            &bytes[o..o + SURFACE_MATERIAL_SIZE],
564        ));
565    }
566
567    let mut capsules = Vec::with_capacity(capsule_count as usize);
568    for i in 0..capsule_count as usize {
569        let o = capsule_offset as usize + i * COMPOUND_CONVEX_SIZE;
570        capsules.push(CompoundCapsule {
571            capsule: crate::geometry::Capsule {
572                center1: read_vec3(bytes, o),
573                center2: read_vec3(bytes, o + 12),
574                radius: read_f32(bytes, o + 24),
575            },
576            material_index: read_i32(bytes, o + 28),
577        });
578    }
579
580    let mut hull_instances = Vec::with_capacity(hull_count as usize);
581    for i in 0..hull_count as usize {
582        let o = hull_offset as usize + i * HULL_INSTANCE_SIZE;
583        hull_instances.push(HullInstance {
584            transform: read_transform(bytes, o),
585            shared_index: 0, // fixed up below
586            material_index: read_u32(bytes, o + 32),
587            hull_offset: read_u32(bytes, o + 28),
588        });
589    }
590
591    // Collect unique hull offsets in first-seen order to rebuild shared_hulls
592    let mut shared_hulls = Vec::new();
593    let mut offset_to_shared: Vec<(u32, usize)> = Vec::new();
594    for inst in &mut hull_instances {
595        let off = inst.hull_offset;
596        if let Some((_, idx)) = offset_to_shared.iter().find(|(o, _)| *o == off) {
597            inst.shared_index = *idx as u32;
598        } else {
599            let idx = shared_hulls.len();
600            let hull_bytes = &bytes[off as usize..];
601            // byte_count is at offset 8 of the hull header
602            let hull_byte_count = read_i32(hull_bytes, 8) as usize;
603            let hull = read_hull_data(&hull_bytes[..hull_byte_count])?;
604            shared_hulls.push(hull);
605            offset_to_shared.push((off, idx));
606            inst.shared_index = idx as u32;
607        }
608    }
609    debug_assert_eq!(shared_hulls.len() as i32, shared_hull_count);
610
611    let mut mesh_instances = Vec::with_capacity(mesh_count as usize);
612    for i in 0..mesh_count as usize {
613        let o = mesh_offset as usize + i * MESH_INSTANCE_SIZE;
614        let mut material_indices = [0u32; MAX_COMPOUND_MESH_MATERIALS];
615        for j in 0..MAX_COMPOUND_MESH_MATERIALS {
616            material_indices[j] = read_u32(bytes, o + 44 + j * 4);
617        }
618        mesh_instances.push(MeshInstance {
619            transform: read_transform(bytes, o),
620            scale: read_vec3(bytes, o + 28),
621            shared_index: 0,
622            material_indices,
623            mesh_offset: read_u32(bytes, o + 40),
624        });
625    }
626
627    let mut shared_meshes = Vec::new();
628    let mut mesh_offset_to_shared: Vec<(u32, usize)> = Vec::new();
629    for inst in &mut mesh_instances {
630        let off = inst.mesh_offset;
631        if let Some((_, idx)) = mesh_offset_to_shared.iter().find(|(o, _)| *o == off) {
632            inst.shared_index = *idx as u32;
633        } else {
634            let idx = shared_meshes.len();
635            let mesh_bytes = &bytes[off as usize..];
636            let mesh_byte_count = read_i32(mesh_bytes, 8) as usize;
637            let mesh = read_mesh_data(&mesh_bytes[..mesh_byte_count])?;
638            shared_meshes.push(mesh);
639            mesh_offset_to_shared.push((off, idx));
640            inst.shared_index = idx as u32;
641        }
642    }
643    debug_assert_eq!(shared_meshes.len() as i32, shared_mesh_count);
644
645    let mut spheres = Vec::with_capacity(sphere_count as usize);
646    for i in 0..sphere_count as usize {
647        let o = sphere_offset as usize + i * COMPOUND_CONVEX_SIZE;
648        spheres.push(CompoundSphere {
649            sphere: crate::geometry::Sphere {
650                center: read_vec3(bytes, o),
651                radius: read_f32(bytes, o + 12),
652            },
653            material_index: read_i32(bytes, o + 16),
654        });
655    }
656
657    Some(CompoundData {
658        version,
659        byte_count,
660        node_offset,
661        tree,
662        material_offset,
663        material_count,
664        capsule_offset,
665        capsule_count,
666        hull_offset,
667        hull_count,
668        shared_hull_count,
669        mesh_offset,
670        mesh_count,
671        shared_mesh_count,
672        sphere_offset,
673        sphere_count,
674        materials,
675        capsules,
676        hull_instances,
677        shared_hulls,
678        mesh_instances,
679        shared_meshes,
680        spheres,
681    })
682}
683
684// Silence unused import warnings for constants used only in docs/debug.
685#[allow(dead_code)]
686fn _keep() {
687    let _ = (
688        TRANSFORM_IDENTITY,
689        QUAT_IDENTITY,
690        VEC3_ZERO,
691        VEC3_ONE,
692        MAT3_ZERO,
693    );
694}