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