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