ferravex_play/
scene.rs

1//! Generational scene hierarchy, exact matrix composition, prefabs, a bounded
2//! binary scene format, and skeleton skin palettes. No renderer or asset loader.
3use std::collections::HashMap;
4
5pub type Matrix = [f32; 16];
6pub const IDENTITY: Matrix = [
7    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,
8];
9const MAX_NODES: usize = 100_000;
10const MAX_NAME: usize = 4096;
11const MAX_BYTES: usize = 16 * 1024 * 1024;
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct Transform {
14    pub translation: [f32; 3],
15    pub rotation: [f32; 4],
16    pub scale: [f32; 3],
17}
18impl Default for Transform {
19    fn default() -> Self {
20        Self {
21            translation: [0.0; 3],
22            rotation: [0.0, 0.0, 0.0, 1.0],
23            scale: [1.0; 3],
24        }
25    }
26}
27impl Transform {
28    pub fn valid(self) -> bool {
29        self.translation
30            .into_iter()
31            .chain(self.rotation)
32            .chain(self.scale)
33            .all(f32::is_finite)
34            && self
35                .rotation
36                .into_iter()
37                .map(|n| f64::from(n).powi(2))
38                .sum::<f64>()
39                > 1e-20
40    }
41    /// Column-major T*R*S. Quaternion input is normalized; zero scale is allowed.
42    pub fn matrix(self) -> Result<Matrix, SceneError> {
43        if !self.valid() {
44            return Err(SceneError::InvalidTransform);
45        }
46        let length = self
47            .rotation
48            .into_iter()
49            .map(|v| f64::from(v).powi(2))
50            .sum::<f64>()
51            .sqrt();
52        let [x, y, z, w] = self.rotation.map(|v| (f64::from(v) / length) as f32);
53        let [sx, sy, sz] = self.scale;
54        let [tx, ty, tz] = self.translation;
55        Ok([
56            (1.0 - 2.0 * (y * y + z * z)) * sx,
57            2.0 * (x * y + z * w) * sx,
58            2.0 * (x * z - y * w) * sx,
59            0.0,
60            2.0 * (x * y - z * w) * sy,
61            (1.0 - 2.0 * (x * x + z * z)) * sy,
62            2.0 * (y * z + x * w) * sy,
63            0.0,
64            2.0 * (x * z + y * w) * sz,
65            2.0 * (y * z - x * w) * sz,
66            (1.0 - 2.0 * (x * x + y * y)) * sz,
67            0.0,
68            tx,
69            ty,
70            tz,
71            1.0,
72        ])
73    }
74}
75/// Column-major multiplication. Using matrices preserves shear from nonuniform
76/// parent scale followed by child rotation instead of approximating it as TRS.
77pub fn multiply(a: Matrix, b: Matrix) -> Matrix {
78    std::array::from_fn(|index| {
79        let row = index % 4;
80        let col = index / 4;
81        (0..4)
82            .map(|k| f64::from(a[k * 4 + row]) * f64::from(b[col * 4 + k]))
83            .sum::<f64>() as f32
84    })
85}
86pub fn transform_point(m: Matrix, p: [f32; 3]) -> [f32; 3] {
87    std::array::from_fn(|row| m[row] * p[0] + m[4 + row] * p[1] + m[8 + row] * p[2] + m[12 + row])
88}
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub struct NodeId {
91    index: u32,
92    generation: u32,
93}
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum SceneError {
96    InvalidNode,
97    InvalidTransform,
98    Cycle,
99    Capacity,
100    NameTooLong,
101    InvalidFormat,
102    InvalidSkeleton,
103}
104#[derive(Clone, Debug)]
105pub struct Node {
106    name: String,
107    local: Transform,
108    parent: Option<NodeId>,
109    children: Vec<NodeId>,
110}
111impl Node {
112    pub fn name(&self) -> &str {
113        &self.name
114    }
115    pub fn local(&self) -> Transform {
116        self.local
117    }
118    pub fn parent(&self) -> Option<NodeId> {
119        self.parent
120    }
121    pub fn children(&self) -> &[NodeId] {
122        &self.children
123    }
124}
125struct Slot {
126    generation: u32,
127    node: Option<Node>,
128}
129#[derive(Default)]
130pub struct Scene {
131    slots: Vec<Slot>,
132    free: Vec<u32>,
133    len: usize,
134}
135#[derive(Clone, Debug)]
136pub struct Prefab {
137    nodes: Vec<PrefabNode>,
138}
139#[derive(Clone, Debug)]
140struct PrefabNode {
141    name: String,
142    local: Transform,
143    parent: Option<usize>,
144}
145impl Prefab {
146    pub fn len(&self) -> usize {
147        self.nodes.len()
148    }
149    pub fn is_empty(&self) -> bool {
150        self.nodes.is_empty()
151    }
152}
153impl Scene {
154    pub fn new() -> Self {
155        Self::default()
156    }
157    pub fn len(&self) -> usize {
158        self.len
159    }
160    pub fn is_empty(&self) -> bool {
161        self.len == 0
162    }
163    pub fn node(&self, id: NodeId) -> Result<&Node, SceneError> {
164        self.slots
165            .get(id.index as usize)
166            .filter(|s| s.generation == id.generation)
167            .and_then(|s| s.node.as_ref())
168            .ok_or(SceneError::InvalidNode)
169    }
170    fn node_mut(&mut self, id: NodeId) -> Result<&mut Node, SceneError> {
171        self.slots
172            .get_mut(id.index as usize)
173            .filter(|s| s.generation == id.generation)
174            .and_then(|s| s.node.as_mut())
175            .ok_or(SceneError::InvalidNode)
176    }
177    pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &Node)> {
178        self.slots.iter().enumerate().filter_map(|(index, slot)| {
179            slot.node.as_ref().map(|node| {
180                (
181                    NodeId {
182                        index: index as u32,
183                        generation: slot.generation,
184                    },
185                    node,
186                )
187            })
188        })
189    }
190    pub fn create(
191        &mut self,
192        name: impl Into<String>,
193        local: Transform,
194        parent: Option<NodeId>,
195    ) -> Result<NodeId, SceneError> {
196        let name = name.into();
197        if name.len() > MAX_NAME {
198            return Err(SceneError::NameTooLong);
199        }
200        if !local.valid() {
201            return Err(SceneError::InvalidTransform);
202        }
203        if self.len == MAX_NODES || (self.free.is_empty() && self.slots.len() == MAX_NODES) {
204            return Err(SceneError::Capacity);
205        }
206        if let Some(parent) = parent {
207            self.node(parent)?;
208        }
209        let index = if let Some(index) = self.free.pop() {
210            index
211        } else {
212            self.slots.push(Slot {
213                generation: 0,
214                node: None,
215            });
216            (self.slots.len() - 1) as u32
217        };
218        let slot = &mut self.slots[index as usize];
219        let id = NodeId {
220            index,
221            generation: slot.generation,
222        };
223        slot.node = Some(Node {
224            name,
225            local,
226            parent,
227            children: Vec::new(),
228        });
229        self.len += 1;
230        if let Some(parent) = parent {
231            self.node_mut(parent)?.children.push(id);
232        }
233        Ok(id)
234    }
235    pub fn set_local(&mut self, id: NodeId, local: Transform) -> Result<(), SceneError> {
236        if !local.valid() {
237            return Err(SceneError::InvalidTransform);
238        }
239        self.node_mut(id)?.local = local;
240        Ok(())
241    }
242    /// Reparent while retaining local TRS. Cycles and invalid handles are rejected
243    /// before any links change. World-space position may change.
244    pub fn set_parent(&mut self, id: NodeId, parent: Option<NodeId>) -> Result<(), SceneError> {
245        let old = self.node(id)?.parent;
246        let mut ancestor = parent;
247        while let Some(candidate) = ancestor {
248            if candidate == id {
249                return Err(SceneError::Cycle);
250            }
251            ancestor = self.node(candidate)?.parent;
252        }
253        if old == parent {
254            return Ok(());
255        }
256        if let Some(old) = old {
257            self.node_mut(old)?.children.retain(|child| *child != id);
258        }
259        if let Some(parent) = parent {
260            self.node_mut(parent)?.children.push(id);
261        }
262        self.node_mut(id)?.parent = parent;
263        Ok(())
264    }
265    pub fn world_matrix(&self, id: NodeId) -> Result<Matrix, SceneError> {
266        let node = self.node(id)?;
267        let mut matrix = node.local.matrix()?;
268        let mut parent = node.parent;
269        while let Some(id) = parent {
270            let node = self.node(id)?;
271            matrix = multiply(node.local.matrix()?, matrix);
272            parent = node.parent;
273        }
274        if matrix.into_iter().all(f32::is_finite) {
275            Ok(matrix)
276        } else {
277            Err(SceneError::InvalidTransform)
278        }
279    }
280    pub fn remove_subtree(&mut self, root: NodeId) -> Result<usize, SceneError> {
281        let parent = self.node(root)?.parent;
282        let mut ids = vec![root];
283        let mut index = 0;
284        while index < ids.len() {
285            ids.extend_from_slice(self.node(ids[index])?.children());
286            index += 1;
287        }
288        if let Some(parent) = parent {
289            self.node_mut(parent)?
290                .children
291                .retain(|child| *child != root);
292        }
293        for id in ids.iter().rev() {
294            let slot = &mut self.slots[id.index as usize];
295            slot.node = None;
296            if let Some(next) = slot.generation.checked_add(1) {
297                slot.generation = next;
298                self.free.push(id.index);
299            }
300        }
301        self.len -= ids.len();
302        Ok(ids.len())
303    }
304    /// Captures local transforms and hierarchy; external parent links are omitted.
305    pub fn prefab(&self, root: NodeId) -> Result<Prefab, SceneError> {
306        self.node(root)?;
307        let mut pending = vec![(root, None)];
308        let mut nodes = Vec::new();
309        let mut cursor = 0;
310        while cursor < pending.len() {
311            let (id, parent) = pending[cursor];
312            let node = self.node(id)?;
313            nodes.push(PrefabNode {
314                name: node.name.clone(),
315                local: node.local,
316                parent,
317            });
318            pending.extend(node.children.iter().map(|child| (*child, Some(cursor))));
319            cursor += 1;
320        }
321        Ok(Prefab { nodes })
322    }
323    pub fn instantiate(
324        &mut self,
325        prefab: &Prefab,
326        parent: Option<NodeId>,
327    ) -> Result<NodeId, SceneError> {
328        if let Some(parent) = parent {
329            self.node(parent)?;
330        }
331        let available = MAX_NODES - self.slots.len() + self.free.len();
332        if prefab.nodes.is_empty() || prefab.nodes.len() > available {
333            return Err(SceneError::Capacity);
334        }
335        let mut ids = Vec::with_capacity(prefab.nodes.len());
336        for node in &prefab.nodes {
337            let node_parent = node.parent.map(|index| ids[index]).or(parent);
338            ids.push(self.create(node.name.clone(), node.local, node_parent)?);
339        }
340        Ok(ids[0])
341    }
342    pub fn clone_subtree(
343        &mut self,
344        root: NodeId,
345        parent: Option<NodeId>,
346    ) -> Result<NodeId, SceneError> {
347        self.instantiate(&self.prefab(root)?, parent)
348    }
349    /// GSC1 format: magic, u16 version, u32 count; each node has u32 parent
350    /// (MAX=root), u16 UTF-8 name length, name bytes, and 10 little-endian f32 TRS
351    /// values. Handles are remapped; serialized data never trusts slot generations.
352    pub fn encode(&self) -> Result<Vec<u8>, SceneError> {
353        let nodes: Vec<_> = self.nodes().collect();
354        let indices: HashMap<_, _> = nodes
355            .iter()
356            .enumerate()
357            .map(|(i, (id, _))| (*id, i as u32))
358            .collect();
359        let length = 10
360            + nodes
361                .iter()
362                .map(|(_, node)| 46 + node.name.len())
363                .sum::<usize>();
364        if length > MAX_BYTES {
365            return Err(SceneError::Capacity);
366        }
367        let mut bytes = Vec::with_capacity(length);
368        bytes.extend_from_slice(b"GSC1");
369        bytes.extend_from_slice(&1_u16.to_le_bytes());
370        bytes.extend_from_slice(&(nodes.len() as u32).to_le_bytes());
371        for (_, node) in nodes {
372            bytes.extend_from_slice(
373                &node
374                    .parent
375                    .map_or(u32::MAX, |parent| indices[&parent])
376                    .to_le_bytes(),
377            );
378            bytes.extend_from_slice(&(node.name.len() as u16).to_le_bytes());
379            bytes.extend_from_slice(node.name.as_bytes());
380            for value in node
381                .local
382                .translation
383                .into_iter()
384                .chain(node.local.rotation)
385                .chain(node.local.scale)
386            {
387                bytes.extend_from_slice(&value.to_le_bytes());
388            }
389        }
390        Ok(bytes)
391    }
392    pub fn decode(bytes: &[u8]) -> Result<Self, SceneError> {
393        if bytes.len() > MAX_BYTES {
394            return Err(SceneError::InvalidFormat);
395        }
396        let mut reader = Reader { bytes, cursor: 0 };
397        if reader.take(4)? != b"GSC1" || reader.u16()? != 1 {
398            return Err(SceneError::InvalidFormat);
399        }
400        let count = reader.u32()? as usize;
401        if count > MAX_NODES || count > bytes.len().saturating_sub(10) / 46 {
402            return Err(SceneError::InvalidFormat);
403        }
404        let mut scene = Self::new();
405        let mut ids = Vec::with_capacity(count);
406        let mut parents = Vec::with_capacity(count);
407        for _ in 0..count {
408            let parent = reader.u32()?;
409            if parent != u32::MAX && parent as usize >= count {
410                return Err(SceneError::InvalidFormat);
411            }
412            let len = reader.u16()? as usize;
413            if len > MAX_NAME {
414                return Err(SceneError::InvalidFormat);
415            }
416            let name =
417                std::str::from_utf8(reader.take(len)?).map_err(|_| SceneError::InvalidFormat)?;
418            let mut values = [0.0; 10];
419            for value in &mut values {
420                *value = reader.f32()?;
421            }
422            let transform = Transform {
423                translation: [values[0], values[1], values[2]],
424                rotation: [values[3], values[4], values[5], values[6]],
425                scale: [values[7], values[8], values[9]],
426            };
427            ids.push(
428                scene
429                    .create(name, transform, None)
430                    .map_err(|_| SceneError::InvalidFormat)?,
431            );
432            parents.push(parent);
433        }
434        if reader.cursor != bytes.len() {
435            return Err(SceneError::InvalidFormat);
436        }
437        // Validate the parent graph in linear time before linking. Repeatedly
438        // calling set_parent for a long serialized chain would be quadratic.
439        let mut marks = vec![0_u8; count];
440        let mut chain = Vec::new();
441        for start in 0..count {
442            if marks[start] != 0 {
443                continue;
444            }
445            let mut cursor = Some(start);
446            while let Some(index) = cursor {
447                if marks[index] != 0 {
448                    break;
449                }
450                marks[index] = 1;
451                chain.push(index);
452                cursor = (parents[index] != u32::MAX).then_some(parents[index] as usize);
453            }
454            if cursor.is_some_and(|index| marks[index] == 1) {
455                return Err(SceneError::InvalidFormat);
456            }
457            for index in chain.drain(..) {
458                marks[index] = 2;
459            }
460        }
461        for (index, parent) in parents.into_iter().enumerate() {
462            if parent != u32::MAX {
463                let child_id = ids[index];
464                let parent_id = ids[parent as usize];
465                scene.node_mut(child_id)?.parent = Some(parent_id);
466                scene.node_mut(parent_id)?.children.push(child_id);
467            }
468        }
469        Ok(scene)
470    }
471}
472struct Reader<'a> {
473    bytes: &'a [u8],
474    cursor: usize,
475}
476impl<'a> Reader<'a> {
477    fn take(&mut self, count: usize) -> Result<&'a [u8], SceneError> {
478        let end = self
479            .cursor
480            .checked_add(count)
481            .ok_or(SceneError::InvalidFormat)?;
482        let bytes = self
483            .bytes
484            .get(self.cursor..end)
485            .ok_or(SceneError::InvalidFormat)?;
486        self.cursor = end;
487        Ok(bytes)
488    }
489    fn u16(&mut self) -> Result<u16, SceneError> {
490        Ok(u16::from_le_bytes(
491            self.take(2)?
492                .try_into()
493                .map_err(|_| SceneError::InvalidFormat)?,
494        ))
495    }
496    fn u32(&mut self) -> Result<u32, SceneError> {
497        Ok(u32::from_le_bytes(
498            self.take(4)?
499                .try_into()
500                .map_err(|_| SceneError::InvalidFormat)?,
501        ))
502    }
503    fn f32(&mut self) -> Result<f32, SceneError> {
504        Ok(f32::from_le_bytes(
505            self.take(4)?
506                .try_into()
507                .map_err(|_| SceneError::InvalidFormat)?,
508        ))
509    }
510}
511
512#[derive(Clone, Debug)]
513pub struct Joint {
514    pub parent: Option<usize>,
515    pub inverse_bind: Matrix,
516}
517#[derive(Clone, Debug)]
518pub struct Skeleton {
519    joints: Vec<Joint>,
520}
521impl Skeleton {
522    /// Joints must be parent-before-child; this also proves acyclic hierarchy.
523    pub fn new(joints: Vec<Joint>) -> Result<Self, SceneError> {
524        if joints.is_empty()
525            || joints.len() > 4096
526            || joints.iter().enumerate().any(|(i, joint)| {
527                joint.parent.is_some_and(|parent| parent >= i)
528                    || !joint.inverse_bind.into_iter().all(f32::is_finite)
529            })
530        {
531            return Err(SceneError::InvalidSkeleton);
532        }
533        Ok(Self { joints })
534    }
535    pub fn joint_count(&self) -> usize {
536        self.joints.len()
537    }
538    pub fn world_pose(&self, locals: &[Transform]) -> Result<Vec<Matrix>, SceneError> {
539        if locals.len() != self.joints.len() {
540            return Err(SceneError::InvalidSkeleton);
541        }
542        let mut world = Vec::with_capacity(locals.len());
543        for (joint, local) in self.joints.iter().zip(locals) {
544            let matrix = local.matrix()?;
545            let matrix = joint
546                .parent
547                .map_or(matrix, |parent| multiply(world[parent], matrix));
548            if !matrix.into_iter().all(f32::is_finite) {
549                return Err(SceneError::InvalidTransform);
550            }
551            world.push(matrix);
552        }
553        Ok(world)
554    }
555    /// Model-space joint world pose multiplied by inverse bind, ready for skinning.
556    pub fn skin_palette(&self, locals: &[Transform]) -> Result<Vec<Matrix>, SceneError> {
557        let world = self.world_pose(locals)?;
558        let palette: Vec<_> = world
559            .into_iter()
560            .zip(&self.joints)
561            .map(|(pose, joint)| multiply(pose, joint.inverse_bind))
562            .collect();
563        if palette.iter().flatten().all(|v| v.is_finite()) {
564            Ok(palette)
565        } else {
566            Err(SceneError::InvalidTransform)
567        }
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    fn translated(x: f32, y: f32, z: f32) -> Transform {
575        Transform {
576            translation: [x, y, z],
577            ..Transform::default()
578        }
579    }
580    #[test]
581    fn hierarchy_composes_parent_rotation_and_child_translation() {
582        let mut scene = Scene::new();
583        let root = scene
584            .create(
585                "root",
586                Transform {
587                    rotation: [
588                        0.0,
589                        0.0,
590                        std::f32::consts::FRAC_1_SQRT_2,
591                        std::f32::consts::FRAC_1_SQRT_2,
592                    ],
593                    ..translated(2.0, 0.0, 0.0)
594                },
595                None,
596            )
597            .unwrap();
598        let child = scene
599            .create("child", translated(3.0, 0.0, 0.0), Some(root))
600            .unwrap();
601        let p = transform_point(scene.world_matrix(child).unwrap(), [0.0; 3]);
602        assert!((p[0] - 2.0).abs() < 1e-5);
603        assert!((p[1] - 3.0).abs() < 1e-5);
604        assert_eq!(scene.set_parent(root, Some(child)), Err(SceneError::Cycle));
605        assert_eq!(scene.node(root).unwrap().parent(), None);
606    }
607    #[test]
608    fn prefab_copy_is_independent_and_stale_ids_stay_invalid() {
609        let mut scene = Scene::new();
610        let root = scene.create("root", Transform::default(), None).unwrap();
611        let child = scene
612            .create("child", translated(2.0, 0.0, 0.0), Some(root))
613            .unwrap();
614        let copy = scene.clone_subtree(root, None).unwrap();
615        assert_eq!(scene.len(), 4);
616        let copied_child = scene.node(copy).unwrap().children()[0];
617        scene.set_local(child, translated(5.0, 0.0, 0.0)).unwrap();
618        assert_eq!(
619            scene.node(copied_child).unwrap().local().translation[0],
620            2.0
621        );
622        assert_eq!(scene.remove_subtree(root).unwrap(), 2);
623        scene.create("reuse", Transform::default(), None).unwrap();
624        assert!(matches!(scene.node(root), Err(SceneError::InvalidNode)));
625        assert!(matches!(scene.node(child), Err(SceneError::InvalidNode)));
626    }
627    #[test]
628    fn scene_binary_roundtrip_and_strict_malformed_rejection() {
629        let mut scene = Scene::new();
630        let root = scene.create("父", translated(1.0, 2.0, 3.0), None).unwrap();
631        scene
632            .create("child", translated(4.0, 0.0, 0.0), Some(root))
633            .unwrap();
634        let bytes = scene.encode().unwrap();
635        let decoded = Scene::decode(&bytes).unwrap();
636        assert_eq!(decoded.encode().unwrap(), bytes);
637        for cut in 0..bytes.len() {
638            assert!(Scene::decode(&bytes[..cut]).is_err());
639        }
640        let mut trailing = bytes.clone();
641        trailing.push(0);
642        assert!(Scene::decode(&trailing).is_err());
643        let mut cycle = bytes.clone();
644        cycle[10..14].copy_from_slice(&1_u32.to_le_bytes());
645        assert!(Scene::decode(&cycle).is_err());
646        let mut oversized = bytes.clone();
647        oversized[6..10].copy_from_slice(&u32::MAX.to_le_bytes());
648        assert!(Scene::decode(&oversized).is_err());
649    }
650    #[test]
651    fn skeleton_bind_pose_palette_is_identity_and_animation_moves_child() {
652        let inverse = translated(-2.0, 0.0, 0.0).matrix().unwrap();
653        let skeleton = Skeleton::new(vec![
654            Joint {
655                parent: None,
656                inverse_bind: IDENTITY,
657            },
658            Joint {
659                parent: Some(0),
660                inverse_bind: inverse,
661            },
662        ])
663        .unwrap();
664        let palette = skeleton
665            .skin_palette(&[Transform::default(), translated(2.0, 0.0, 0.0)])
666            .unwrap();
667        assert_eq!(palette, vec![IDENTITY, IDENTITY]);
668        let pose = skeleton
669            .world_pose(&[translated(1.0, 0.0, 0.0), translated(3.0, 0.0, 0.0)])
670            .unwrap();
671        assert_eq!(transform_point(pose[1], [0.0; 3]), [4.0, 0.0, 0.0]);
672        assert!(
673            Skeleton::new(vec![Joint {
674                parent: Some(0),
675                inverse_bind: IDENTITY
676            }])
677            .is_err()
678        );
679        assert!(skeleton.skin_palette(&[]).is_err());
680    }
681}