Skip to main content

pebble/wgpu/
skinning.rs

1use std::collections::HashMap;
2
3use crate::{
4    app::{App, SystemStage},
5    assets::{handle::Handle, storage::RawAssetHandle},
6    ecs::{
7        plugin::Plugin,
8        system::{Commands, Local, Query, Res, ResMut},
9        system_condition::{ResourceExists, RunIfExt},
10    },
11};
12
13use super::{
14    backend::WGPUBackend,
15    binding::{BindGroupLayout, BindGroupLayoutBuilder, BindingKind},
16    buffer::Buffer,
17    buffers::{BindGroup, BindGroupBuilder, BufferBuilder},
18    flags::ShaderStages,
19    layout::GlobalLayoutPool,
20    material::Material,
21    player::AnimationPlayer,
22    skinned_mesh::SkinnedMesh,
23};
24
25// ── internal ──────────────────────────────────────────────────────────────────
26
27struct SkinningBatch {
28    matrices:    Buffer,   // storage: capacity * joint_count * mat4
29    _info:       Buffer,   // uniform: SkinningInfo { joint_count: u32, _pad: [u32; 3] }
30    bind_group:  BindGroup,
31    joint_count: u32,
32    capacity:    u32,
33}
34
35impl SkinningBatch {
36    fn new(backend: &WGPUBackend, layout: &BindGroupLayout, joint_count: u32, capacity: u32) -> Self {
37        let matrices = BufferBuilder::empty((capacity as u64) * (joint_count as u64) * 64)
38            .with_label("pebble_skinning:joint_matrices")
39            .with_storage()
40            .build(backend);
41        let info = BufferBuilder::with_data(bytemuck::cast_slice(&[joint_count, 0u32, 0u32, 0u32]))
42            .with_label("pebble_skinning:info")
43            .with_uniform()
44            .build(backend);
45        let bind_group = BindGroupBuilder::new(layout)
46            .with_buffer(&matrices)
47            .with_buffer(&info)
48            .build(backend);
49        Self { matrices, _info: info, bind_group, joint_count, capacity }
50    }
51}
52
53// ── public ────────────────────────────────────────────────────────────────────
54
55/// Holds one GPU buffer pair per unique `(material, mesh)` batch — one per
56/// skeleton type in practice. Retrieve the bind group for a batch from your
57/// render system via [`bind_group`](Self::bind_group).
58///
59/// WGSL layout for `GroupEntry::Global("pebble_skinning")`:
60/// ```wgsl
61/// struct SkinningInfo { joint_count: u32 }
62/// @group(N) @binding(0) var<storage, read> joint_matrices: array<mat4x4<f32>>;
63/// @group(N) @binding(1) var<uniform>        skin_info:     SkinningInfo;
64/// // in vs_main: let base = instance_index * skin_info.joint_count;
65/// ```
66pub struct SkinnedBatchRenderer {
67    layout:  BindGroupLayout,
68    batches: HashMap<(RawAssetHandle, RawAssetHandle), SkinningBatch>,
69}
70
71impl SkinnedBatchRenderer {
72    fn new(backend: &WGPUBackend) -> Self {
73        let layout = BindGroupLayoutBuilder::new()
74            .with_label("pebble_skinning")
75            .with_entry("joint_matrices", 0, BindingKind::storage_buffer_read_only(ShaderStages::VERTEX))
76            .with_entry("skin_info",      1, BindingKind::uniform_buffer(ShaderStages::VERTEX))
77            .build(backend);
78        Self { layout, batches: HashMap::new() }
79    }
80
81    /// The bind group for the `(material, mesh)` batch — pass to
82    /// `set_bind_group` immediately before `draw_indexed(0..index_count, 0, 0..instance_count)`.
83    pub fn bind_group(&self, material: RawAssetHandle, mesh: RawAssetHandle) -> Option<&BindGroup> {
84        self.batches.get(&(material, mesh)).map(|b| &b.bind_group)
85    }
86
87    fn prepare(&mut self, key: (RawAssetHandle, RawAssetHandle), joint_count: u32, needed: u32, backend: &WGPUBackend) {
88        let layout = self.layout.clone();
89        let batch = self.batches.entry(key).or_insert_with(|| {
90            SkinningBatch::new(backend, &layout, joint_count, needed.max(256))
91        });
92        if needed > batch.capacity {
93            *batch = SkinningBatch::new(backend, &layout, batch.joint_count, (batch.capacity * 2).max(needed));
94        }
95    }
96}
97
98/// One entry per unique `(material, mesh)` pair — produced by
99/// [`batch_skinned_entities`] each `PreRender` tick. Iterate in your render
100/// system alongside [`SkinnedBatchRenderer`] to drive draw calls.
101pub struct SkinnedBatchUnit {
102    pub material:       RawAssetHandle,
103    pub mesh:           RawAssetHandle,
104    pub instance_count: u32,
105}
106
107/// The frame output of the skinning batch system.
108///
109/// ```ignore
110/// for batch in storage.batches.iter() {
111///     let Some(bind_group) = renderer.bind_group(batch.material, batch.mesh) else { continue };
112///     pass.set_bind_group(0, bind_group, &[]);
113///     pass.draw_indexed(0..mesh.index_count, 0, 0..batch.instance_count);
114/// }
115/// ```
116#[derive(Default)]
117pub struct SkinnedBatchStorage {
118    pub batches: Vec<SkinnedBatchUnit>,
119}
120
121// ── systems ───────────────────────────────────────────────────────────────────
122
123fn init_skinned_batching(
124    mut commands: Commands,
125    backend:      Res<WGPUBackend>,
126    mut pool:     ResMut<GlobalLayoutPool>,
127) -> Option<()> {
128    let renderer = SkinnedBatchRenderer::new(&backend);
129    pool.register("pebble_skinning", renderer.layout.clone());
130    commands.insert_resource(renderer);
131    commands.insert_resource(SkinnedBatchStorage::default());
132    Some(())
133}
134
135struct GroupData {
136    joint_count: u32,
137    matrices:    Vec<glam::Mat4>, // flat: entity0 joints, entity1 joints, ...
138    count:       u32,
139}
140
141fn batch_skinned_entities(
142    backend:      Res<WGPUBackend>,
143    mut renderer: ResMut<SkinnedBatchRenderer>,
144    mut storage:  ResMut<SkinnedBatchStorage>,
145    mut query:    Query<(&Handle<Material>, &Handle<SkinnedMesh>, &AnimationPlayer)>,
146    mut groups:   Local<HashMap<(RawAssetHandle, RawAssetHandle), GroupData>>,
147) {
148    storage.batches.clear();
149    groups.clear();
150
151    for (mat, mesh, player) in query.iter() {
152        let entry = groups.entry((mat.id, mesh.id)).or_insert_with(|| GroupData {
153            joint_count: player.joint_count() as u32,
154            matrices:    Vec::new(),
155            count:       0,
156        });
157        entry.matrices.extend(player.compute_matrices());
158        entry.count += 1;
159    }
160
161    for ((mat_id, mesh_id), group) in groups.iter() {
162        renderer.prepare((*mat_id, *mesh_id), group.joint_count, group.count, &backend);
163        let batch = renderer.batches.get(&(*mat_id, *mesh_id)).unwrap();
164        batch.matrices.write(bytemuck::cast_slice(&group.matrices));
165        storage.batches.push(SkinnedBatchUnit {
166            material:       *mat_id,
167            mesh:           *mesh_id,
168            instance_count: group.count,
169        });
170    }
171}
172
173/// Registers the `"pebble_skinning"` bind group layout, creates
174/// [`SkinnedBatchRenderer`] and [`SkinnedBatchStorage`] resources, and runs
175/// [`batch_skinned_entities`] in [`SystemStage::PreRender`].
176///
177/// The plugin does **not** advance animation time — add your own system that
178/// calls [`AnimationPlayer::advance`](super::player::AnimationPlayer::advance).
179pub struct SkinnedBatchingPlugin;
180
181impl Plugin for SkinnedBatchingPlugin {
182    fn build(&self, app: &mut App) {
183        app.add_system(
184            SystemStage::Startup,
185            init_skinned_batching.run_if::<ResourceExists<WGPUBackend>>(),
186        );
187        // Gating on WGPUBackend alone is enough, even though this system also
188        // needs SkinnedBatchRenderer/SkinnedBatchStorage: every tick runs
189        // Startup to completion before PreRender starts (see ALL_STAGES in
190        // app.rs), so by the first tick the backend exists, the line above
191        // has already inserted both — no separate check needed for them.
192        app.add_system(
193            SystemStage::PreRender,
194            batch_skinned_entities.run_if::<ResourceExists<WGPUBackend>>(),
195        );
196    }
197}