Skip to main content

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}