viewport_lib/runtime/plugins/skeleton_plugin/plugin.rs
1//! SkeletonPlugin: drives CPU linear blend skinning from a runtime Pose.
2
3use super::skeleton::{JointMatrices, Pose, Skeleton, apply_skin};
4use crate::resources::SkinWeights;
5use crate::resources::mesh_store::MeshId;
6use crate::runtime::context::RuntimeStepContext;
7use crate::runtime::output::{SkinnedMeshUpdate, SkinnedPoseUpdate};
8use crate::runtime::plugin::{RuntimePlugin, phase};
9
10/// Which deformation path a skinning plugin should emit each frame.
11///
12/// `Cpu` runs LBS on the CPU and emits [`SkinnedMeshUpdate`]; the host must
13/// upload deformed positions/normals via `write_mesh_positions_normals`.
14/// `Gpu` emits [`SkinnedPoseUpdate`] carrying joint matrices; the host
15/// uploads them via [`crate::ViewportGpuResources::set_skin_palette`] and the
16/// skinned pipeline variant does LBS in the vertex stage.
17#[derive(Copy, Clone, Debug, PartialEq, Eq)]
18pub enum SkinningPath {
19 /// CPU LBS, re-upload deformed vertices each frame.
20 Cpu,
21 /// GPU LBS, upload only the joint palette each frame.
22 Gpu,
23}
24
25impl Default for SkinningPath {
26 fn default() -> Self {
27 SkinningPath::Cpu
28 }
29}
30
31/// Built-in plugin that applies CPU linear blend skinning each frame.
32///
33/// Store a [`Pose`] in [`super::super::resources::RuntimeResources`] (keyed by
34/// the `Pose` type) during the `ANIMATE` phase or earlier. `SkeletonPlugin`
35/// runs at `POST_SIM`, reads the pose, computes the skinning matrices, and
36/// pushes a [`SkinnedMeshUpdate`] to `ctx.output.skinned_mesh_updates`.
37///
38/// After `step()`, iterate `output.skinned_mesh_updates` and call
39/// `renderer.resources_mut().write_mesh_positions_normals(queue, id, pos, nrm)`
40/// to upload the deformed geometry.
41///
42/// # Example
43///
44/// ```rust,ignore
45/// // Startup: register the plugin with bind-pose mesh data.
46/// let plugin = SkeletonPlugin::new(skeleton, mesh_id, positions, normals, skin_weights);
47/// let runtime = ViewportRuntime::new().with_plugin(plugin);
48///
49/// // Per-frame: write the animated pose before step().
50/// runtime.resources_mut().insert(my_pose);
51///
52/// let output = runtime.step(&mut scene, &mut sel, &frame_ctx);
53///
54/// for u in &output.skinned_mesh_updates {
55/// renderer.resources_mut()
56/// .write_mesh_positions_normals(queue, u.mesh_id, &u.positions, &u.normals)
57/// .ok();
58/// }
59/// ```
60pub struct SkeletonPlugin {
61 /// The skeleton this plugin deforms against.
62 pub skeleton: Skeleton,
63 /// The GPU mesh to update each frame.
64 pub mesh_id: MeshId,
65 /// Which deformation path to emit each frame. Defaults to `Cpu` so existing
66 /// consumers keep working unchanged. Set to `Gpu` when the host has
67 /// uploaded skin weights for `mesh_id` and is draining
68 /// `output.skinned_pose_updates` into `set_skin_palette`.
69 pub path: SkinningPath,
70 cpu_positions: Vec<[f32; 3]>,
71 cpu_normals: Vec<[f32; 3]>,
72 skin_weights: SkinWeights,
73}
74
75impl SkeletonPlugin {
76 /// Create a new `SkeletonPlugin`.
77 ///
78 /// `positions` and `normals` are the bind-pose vertex arrays (same as
79 /// those passed to `upload_mesh_data`). `skin_weights` must have the same
80 /// vertex count as `positions`.
81 pub fn new(
82 skeleton: Skeleton,
83 mesh_id: MeshId,
84 positions: Vec<[f32; 3]>,
85 normals: Vec<[f32; 3]>,
86 skin_weights: SkinWeights,
87 ) -> Self {
88 Self {
89 skeleton,
90 mesh_id,
91 path: SkinningPath::default(),
92 cpu_positions: positions,
93 cpu_normals: normals,
94 skin_weights,
95 }
96 }
97
98 /// Override the deformation path. Builder-style for ergonomic init.
99 pub fn with_path(mut self, path: SkinningPath) -> Self {
100 self.path = path;
101 self
102 }
103}
104
105impl RuntimePlugin for SkeletonPlugin {
106 fn priority(&self) -> i32 {
107 phase::POST_SIM
108 }
109
110 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
111 let Some(pose) = ctx.resources.get::<Pose>() else {
112 return;
113 };
114 let matrices = JointMatrices::compute(&self.skeleton, pose);
115 match self.path {
116 SkinningPath::Cpu => {
117 let (positions, normals) = apply_skin(
118 &self.cpu_positions,
119 &self.cpu_normals,
120 &self.skin_weights,
121 &matrices,
122 );
123 ctx.output.skinned_mesh_updates.push(SkinnedMeshUpdate {
124 mesh_id: self.mesh_id,
125 positions,
126 normals,
127 });
128 }
129 SkinningPath::Gpu => {
130 let joint_matrices: Vec<glam::Mat4> = matrices
131 .as_slice()
132 .iter()
133 .map(|m| glam::Mat4::from(*m))
134 .collect();
135 ctx.output.skinned_pose_updates.push(SkinnedPoseUpdate {
136 mesh_id: self.mesh_id,
137 instance_id: 0,
138 joint_matrices,
139 });
140 }
141 }
142 }
143}