Skip to main content

viewport_lib/runtime/
snapshot.rs

1//! Transform snapshot table for smooth physics-driven rendering.
2//!
3//! When using a fixed timestep, the render rate and simulation step rate differ.
4//! Rendering directly from physics state causes visible jitter at the seam between
5//! steps. Instead, store the previous and current transform for each physics-driven
6//! node, then lerp between them using [`FixedTimestep::alpha`](super::timestep::FixedTimestep::alpha).
7//!
8//! The runtime populates this table automatically when applying
9//! [`NodeTransformOp`](super::output::NodeTransformOp)s during `Writeback`.
10//! Read it via [`ViewportRuntime::snapshots`](super::ViewportRuntime::snapshots).
11
12use std::collections::HashMap;
13
14use crate::interaction::select::selection::NodeId;
15
16/// Previous and current world-space transforms for one physics-driven node.
17#[derive(Debug, Clone, Copy)]
18pub struct TransformSnapshot {
19    /// Transform from the previous fixed step.
20    pub prev: glam::Affine3A,
21    /// Transform from the most recent fixed step.
22    pub curr: glam::Affine3A,
23}
24
25/// Per-node transform snapshot table for rendering interpolation.
26///
27/// Keyed by [`NodeId`]. Lives inside [`super::ViewportRuntime`], separate from
28/// the scene graph, so apps that do not use physics pay no overhead.
29pub struct TransformSnapshotTable {
30    entries: HashMap<NodeId, TransformSnapshot>,
31}
32
33impl TransformSnapshotTable {
34    pub(super) fn new() -> Self {
35        Self {
36            entries: HashMap::new(),
37        }
38    }
39
40    /// Record a new transform for a node, shifting the current value to `prev`.
41    ///
42    /// On the first call for a node, both `prev` and `curr` are set to
43    /// `transform` to avoid a spurious lerp from the origin.
44    pub fn update(&mut self, id: NodeId, transform: glam::Affine3A) {
45        let entry = self.entries.entry(id).or_insert(TransformSnapshot {
46            prev: transform,
47            curr: transform,
48        });
49        entry.prev = entry.curr;
50        entry.curr = transform;
51    }
52
53    /// Get the raw snapshot for a node.
54    pub fn get(&self, id: NodeId) -> Option<&TransformSnapshot> {
55        self.entries.get(&id)
56    }
57
58    /// Compute the interpolated transform for a node at blend factor `alpha`.
59    ///
60    /// `alpha` comes from [`FixedTimestep::alpha`](super::timestep::FixedTimestep::alpha):
61    /// `0.0` returns `prev`, `1.0` returns `curr`, values between lerp translation
62    /// and scale while slerping rotation.
63    ///
64    /// Returns `None` if no snapshot exists for the node.
65    pub fn interpolated(&self, id: NodeId, alpha: f32) -> Option<glam::Affine3A> {
66        let s = self.entries.get(&id)?;
67        let (prev_scale, prev_rot, prev_trans) =
68            glam::Mat4::from(s.prev).to_scale_rotation_translation();
69        let (curr_scale, curr_rot, curr_trans) =
70            glam::Mat4::from(s.curr).to_scale_rotation_translation();
71        Some(glam::Affine3A::from_scale_rotation_translation(
72            prev_scale.lerp(curr_scale, alpha),
73            prev_rot.slerp(curr_rot, alpha),
74            prev_trans.lerp(curr_trans, alpha),
75        ))
76    }
77
78    /// Remove the snapshot for a node.
79    pub fn remove(&mut self, id: NodeId) {
80        self.entries.remove(&id);
81    }
82
83    /// Clear all snapshots.
84    pub fn clear(&mut self) {
85        self.entries.clear();
86    }
87
88    /// Number of nodes tracked.
89    pub fn len(&self) -> usize {
90        self.entries.len()
91    }
92
93    /// Whether the table is empty.
94    pub fn is_empty(&self) -> bool {
95        self.entries.is_empty()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn translation(x: f32) -> glam::Affine3A {
104        glam::Affine3A::from_translation(glam::Vec3::new(x, 0.0, 0.0))
105    }
106
107    #[test]
108    fn test_first_update_sets_both_to_same() {
109        let mut table = TransformSnapshotTable::new();
110        let t = translation(1.0);
111        table.update(1, t);
112        let s = table.get(1).unwrap();
113        assert_eq!(s.prev, t);
114        assert_eq!(s.curr, t);
115    }
116
117    #[test]
118    fn test_second_update_shifts_prev() {
119        let mut table = TransformSnapshotTable::new();
120        let t0 = translation(0.0);
121        let t1 = translation(1.0);
122        table.update(1, t0);
123        table.update(1, t1);
124        let s = table.get(1).unwrap();
125        assert_eq!(s.prev, t0);
126        assert_eq!(s.curr, t1);
127    }
128
129    #[test]
130    fn test_interpolated_at_zero_returns_prev() {
131        let mut table = TransformSnapshotTable::new();
132        table.update(1, translation(0.0));
133        table.update(1, translation(4.0));
134        let t = table.interpolated(1, 0.0).unwrap();
135        assert!((t.translation.x).abs() < 1e-5, "x was {}", t.translation.x);
136    }
137
138    #[test]
139    fn test_interpolated_at_one_returns_curr() {
140        let mut table = TransformSnapshotTable::new();
141        table.update(1, translation(0.0));
142        table.update(1, translation(4.0));
143        let t = table.interpolated(1, 1.0).unwrap();
144        assert!(
145            (t.translation.x - 4.0).abs() < 1e-5,
146            "x was {}",
147            t.translation.x
148        );
149    }
150
151    #[test]
152    fn test_interpolated_midpoint() {
153        let mut table = TransformSnapshotTable::new();
154        table.update(1, translation(0.0));
155        table.update(1, translation(4.0));
156        let t = table.interpolated(1, 0.5).unwrap();
157        assert!(
158            (t.translation.x - 2.0).abs() < 1e-5,
159            "x was {}",
160            t.translation.x
161        );
162    }
163
164    #[test]
165    fn test_missing_node_returns_none() {
166        let table = TransformSnapshotTable::new();
167        assert!(table.interpolated(999, 0.5).is_none());
168        assert!(table.get(999).is_none());
169    }
170
171    #[test]
172    fn test_remove() {
173        let mut table = TransformSnapshotTable::new();
174        table.update(1, translation(1.0));
175        table.remove(1);
176        assert!(table.get(1).is_none());
177        assert!(table.is_empty());
178    }
179
180    #[test]
181    fn test_clear() {
182        let mut table = TransformSnapshotTable::new();
183        table.update(1, translation(1.0));
184        table.update(2, translation(2.0));
185        table.clear();
186        assert!(table.is_empty());
187        assert_eq!(table.len(), 0);
188    }
189}