viewport_lib/runtime/output.rs
1//! Runtime output types: transform ops, selection ops, contact events, and generic events.
2
3use crate::camera::camera::{Camera, CameraTarget};
4use crate::interaction::selection::{NodeId, Selection};
5use crate::resources::mesh_store::MeshId;
6use super::events::RuntimeEventBus;
7
8/// Write buffer for transform changes produced by plugins.
9///
10/// Passed into [`super::context::RuntimeStepContext`] so plugins can record
11/// transform changes without directly mutating the scene. The runtime flushes
12/// all ops to the scene after the `Writeback` phase.
13#[derive(Default)]
14pub struct TransformWriteback {
15 ops: Vec<NodeTransformOp>,
16}
17
18impl TransformWriteback {
19 /// Record a new local-space transform for a scene node.
20 ///
21 /// For physics-driven nodes with no parent, local space equals world space.
22 /// If the same node is written more than once, all ops are kept and applied
23 /// in order (last write wins after scene propagation).
24 pub fn set(&mut self, id: NodeId, transform: glam::Affine3A) {
25 self.ops.push(NodeTransformOp { id, transform });
26 }
27
28 /// Consume the buffer and return all recorded ops.
29 pub(super) fn into_ops(self) -> Vec<NodeTransformOp> {
30 self.ops
31 }
32}
33
34/// A transform write targeting one scene node.
35#[derive(Debug, Clone)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct NodeTransformOp {
38 /// Target scene node.
39 pub id: NodeId,
40 /// New local-space transform for the node. For physics-driven root nodes,
41 /// this is the world-space transform.
42 pub transform: glam::Affine3A,
43}
44
45/// A change to the selection state.
46///
47/// Produced by runtime plugins and returned in [`RuntimeOutput::selection_ops`].
48/// The runtime applies these to the app-owned [`Selection`] during each step.
49#[derive(Debug, Clone)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub enum SelectionOp {
52 /// Clear all selected nodes and select one.
53 SelectOne(NodeId),
54 /// Toggle a node's selected state.
55 Toggle(NodeId),
56 /// Add a node to the selection.
57 Add(NodeId),
58 /// Remove a node from the selection.
59 Remove(NodeId),
60 /// Add multiple nodes to the selection.
61 Extend(Vec<NodeId>),
62 /// Clear the selection.
63 Clear,
64 /// Replace the selection with the given set.
65 SelectAll(Vec<NodeId>),
66}
67
68impl SelectionOp {
69 /// Apply this operation to a [`Selection`].
70 pub fn apply_to(&self, selection: &mut Selection) {
71 match self {
72 SelectionOp::SelectOne(id) => selection.select_one(*id),
73 SelectionOp::Toggle(id) => selection.toggle(*id),
74 SelectionOp::Add(id) => selection.add(*id),
75 SelectionOp::Remove(id) => selection.remove(*id),
76 SelectionOp::Extend(ids) => selection.extend(ids.iter().copied()),
77 SelectionOp::Clear => selection.clear(),
78 SelectionOp::SelectAll(ids) => selection.select_all(ids.iter().copied()),
79 }
80 }
81}
82
83/// A contact event produced by a physics plugin during the `Simulate` phase.
84///
85/// Returned in [`RuntimeOutput::contact_events`] for the app to use in game logic,
86/// sound, or effects. Not applied to the scene by the runtime.
87#[derive(Debug, Clone)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct ContactEvent {
90 /// First node involved in the contact.
91 pub node_a: NodeId,
92 /// Second node involved in the contact.
93 pub node_b: NodeId,
94 /// Contact normal pointing from `node_a` toward `node_b`, in world space.
95 pub world_normal: glam::Vec3,
96 /// Magnitude of the impulse applied at the contact point.
97 pub impulse: f32,
98 /// World-space position of the contact point.
99 ///
100 /// Use this for placing sound sources, particle effects, or decals at the
101 /// collision site. Simple plugins that do not compute a contact point may
102 /// leave this as `Vec3::ZERO`.
103 pub contact_point: glam::Vec3,
104}
105
106/// A camera state change produced by a runtime plugin.
107///
108/// Accumulated in [`RuntimeOutput::camera_commands`] and applied to the app-owned
109/// [`Camera`] by calling [`RuntimeOutput::apply_camera_commands`].
110///
111/// Commands are applied in the order they were emitted (plugin priority order,
112/// then registration order within the same priority). Each command builds on the
113/// result of the previous one in the same frame.
114///
115/// This is independent of [`super::CameraFollow`]: `camera_follow_target` and
116/// `camera_commands` coexist and the app decides which to apply.
117#[derive(Debug, Clone)]
118pub enum CameraCommand {
119 /// Set the orbit center (pivot point) to an absolute world position.
120 SetCenter(glam::Vec3),
121 /// Add a world-space delta to the orbit center.
122 OffsetCenter(glam::Vec3),
123 /// Set the camera distance from the center. Clamped to a small positive value.
124 SetDistance(f32),
125 /// Set the camera orientation.
126 SetOrientation(glam::Quat),
127 /// Blend center, distance, and orientation toward a target state.
128 ///
129 /// `weight` is in `[0, 1]`. At `1.0` the camera snaps to `target` immediately.
130 /// Smaller values produce smooth motion when emitted every frame.
131 BlendToward {
132 /// Target camera state to blend toward.
133 target: CameraTarget,
134 /// Blend weight in `[0, 1]`.
135 weight: f32,
136 },
137}
138
139/// A per-mesh deformation update produced by a skinning plugin.
140///
141/// Returned in [`RuntimeOutput::skinned_mesh_updates`]. Apply after `step()`:
142///
143/// ```rust,ignore
144/// for u in &output.skinned_mesh_updates {
145/// renderer.resources_mut()
146/// .write_mesh_positions_normals(queue, u.mesh_id, &u.positions, &u.normals)
147/// .ok();
148/// }
149/// ```
150pub struct SkinnedMeshUpdate {
151 /// The mesh to deform.
152 pub mesh_id: MeshId,
153 /// Skinned vertex positions in local space.
154 pub positions: Vec<[f32; 3]>,
155 /// Skinned vertex normals.
156 pub normals: Vec<[f32; 3]>,
157}
158
159/// A per-instance joint palette update produced by a skinning plugin on the
160/// GPU path.
161///
162/// Returned in [`RuntimeOutput::skinned_pose_updates`]. Apply after `step()`
163/// by calling [`crate::ViewportGpuResources::set_skin_palette`]:
164///
165/// ```rust,ignore
166/// for u in &output.skinned_pose_updates {
167/// renderer.resources_mut()
168/// .set_skin_palette(&queue, u.mesh_id, u.instance_id, &u.joint_matrices);
169/// }
170/// ```
171pub struct SkinnedPoseUpdate {
172 /// The skinned mesh to drive.
173 pub mesh_id: MeshId,
174 /// Which instance of the mesh this palette is for. Use `0` for single-
175 /// instance meshes.
176 pub instance_id: u32,
177 /// Per-joint skinning matrices in topological order, ready for upload to
178 /// the GPU joint palette storage buffer.
179 pub joint_matrices: Vec<glam::Mat4>,
180}
181
182/// Output produced by one call to [`super::ViewportRuntime::step`].
183///
184/// `node_transform_ops` have already been applied to the scene and the snapshot
185/// table when this is returned. The other fields are for the app to read and
186/// act on as needed.
187///
188/// Plugin-authored events of any type are collected in `events`. Use
189/// `output.events.read::<T>()` or `output.events.drain::<T>()` to consume them.
190/// Plugin-authored camera changes are in `camera_commands`; apply them with
191/// `output.apply_camera_commands(&mut camera)`.
192#[derive(Default)]
193#[non_exhaustive]
194pub struct RuntimeOutput {
195 /// Transform ops applied to the scene during this step.
196 pub node_transform_ops: Vec<NodeTransformOp>,
197 /// Selection changes produced by runtime plugins, already applied to the
198 /// app-owned [`Selection`].
199 pub selection_ops: Vec<SelectionOp>,
200 /// Contact events from physics plugins. Empty if no physics plugin is active.
201 pub contact_events: Vec<ContactEvent>,
202 /// Suggested camera center computed from the active [`super::CameraFollow`] binding.
203 ///
204 /// `Some` when a `CameraFollow::Node` target was resolved this step; `None`
205 /// when no follow binding is set or the target node was not found. Apply to
206 /// `camera.center` for orbit-camera follow behavior.
207 pub camera_follow_target: Option<glam::Vec3>,
208 /// Camera commands emitted by plugins this frame. Apply with
209 /// [`Self::apply_camera_commands`]. Empty when no camera plugin is active.
210 pub camera_commands: Vec<CameraCommand>,
211 /// Generic typed event bus. Plugins emit events via `ctx.output.events.emit(MyEvent { .. })`.
212 /// The app reads them after `step()` via `output.events.read::<MyEvent>()` or
213 /// `output.events.drain::<MyEvent>()`. Events are cleared each frame because
214 /// `RuntimeOutput` is constructed fresh on every `step` call.
215 pub events: RuntimeEventBus,
216 /// Per-mesh deformation updates from skinning plugins on the CPU path.
217 /// Apply after `step()` by calling `write_mesh_positions_normals` on each
218 /// entry. Empty when no [`super::plugins::SkeletonPlugin`] is active on the
219 /// CPU path.
220 pub skinned_mesh_updates: Vec<SkinnedMeshUpdate>,
221 /// Per-instance joint palette updates from skinning plugins on the GPU
222 /// path. Apply after `step()` by calling
223 /// [`crate::ViewportGpuResources::set_skin_palette`] on each entry. Empty
224 /// when no skinning plugin is active on the GPU path.
225 pub skinned_pose_updates: Vec<SkinnedPoseUpdate>,
226}
227
228impl RuntimeOutput {
229 /// Apply all camera commands in emission order to `camera`.
230 ///
231 /// Call this after [`super::ViewportRuntime::step`] returns, before rendering.
232 /// Has no effect if `camera_commands` is empty.
233 ///
234 /// Commands are applied sequentially: each one builds on the result of the
235 /// previous. A `BlendToward` command blends from whatever state the camera
236 /// is in after all prior commands, not from the frame-start state.
237 pub fn apply_camera_commands(&self, camera: &mut Camera) {
238 for cmd in &self.camera_commands {
239 match cmd {
240 CameraCommand::SetCenter(c) => {
241 camera.center = *c;
242 }
243 CameraCommand::OffsetCenter(d) => {
244 camera.center += *d;
245 }
246 CameraCommand::SetDistance(d) => {
247 camera.set_distance(*d);
248 }
249 CameraCommand::SetOrientation(q) => {
250 camera.orientation = q.normalize();
251 }
252 CameraCommand::BlendToward { target, weight } => {
253 let w = weight.clamp(0.0, 1.0);
254 camera.center = camera.center.lerp(target.center, w);
255 camera.distance = camera.distance + (target.distance - camera.distance) * w;
256 camera.distance = camera.distance.max(0.001);
257 camera.orientation = camera.orientation
258 .slerp(target.orientation.normalize(), w)
259 .normalize();
260 }
261 }
262 }
263 }
264}