Skip to main content

viewport_lib/runtime/plugins/skeleton_plugin/
plugin.rs

1//! SkeletonPlugin: drives CPU linear blend skinning from a runtime Pose.
2
3use crate::resources::mesh_store::MeshId;
4use crate::resources::SkinWeights;
5use crate::runtime::context::RuntimeStepContext;
6use crate::runtime::output::{SkinnedMeshUpdate, SkinnedPoseUpdate};
7use crate::runtime::plugin::{RuntimePlugin, phase};
8use super::skeleton::{JointMatrices, Pose, Skeleton, apply_skin};
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 { return };
112        let matrices = JointMatrices::compute(&self.skeleton, pose);
113        match self.path {
114            SkinningPath::Cpu => {
115                let (positions, normals) = apply_skin(
116                    &self.cpu_positions,
117                    &self.cpu_normals,
118                    &self.skin_weights,
119                    &matrices,
120                );
121                ctx.output.skinned_mesh_updates.push(SkinnedMeshUpdate {
122                    mesh_id: self.mesh_id,
123                    positions,
124                    normals,
125                });
126            }
127            SkinningPath::Gpu => {
128                let joint_matrices: Vec<glam::Mat4> = matrices
129                    .as_slice()
130                    .iter()
131                    .map(|m| glam::Mat4::from(*m))
132                    .collect();
133                ctx.output.skinned_pose_updates.push(SkinnedPoseUpdate {
134                    mesh_id: self.mesh_id,
135                    instance_id: 0,
136                    joint_matrices,
137                });
138            }
139        }
140    }
141}