1use std::{collections::HashMap, sync::Arc};
2
3use crate::{
4 assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
5 wgpu::{
6 backend::WGPUBackend,
7 buffer::Buffer,
8 buffers::BufferBuilder,
9 flags::BufferUsages,
10 vertex_format::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode},
11 },
12};
13
14#[repr(C)]
23#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
24pub struct SkinnedVertex {
25 pub position: glam::Vec3,
26 pub tex_coords: glam::Vec2,
27 pub normal: glam::Vec3,
28 pub tangent: glam::Vec4,
29 pub joint_indices: [u16; 4],
37 pub joint_weights: [f32; 4],
38 _pad: [u32; 2],
39}
40
41impl SkinnedVertex {
42 pub fn new(
43 position: glam::Vec3,
44 tex_coords: glam::Vec2,
45 normal: glam::Vec3,
46 tangent: glam::Vec4,
47 joint_indices: [u16; 4],
48 joint_weights: [f32; 4],
49 ) -> Self {
50 Self {
51 position,
52 tex_coords,
53 normal,
54 tangent,
55 joint_indices,
56 joint_weights,
57 _pad: [0; 2],
58 }
59 }
60
61 pub fn layout() -> VertexBufferLayout {
70 VertexBufferLayout {
71 array_stride: std::mem::size_of::<SkinnedVertex>() as u64,
72 step_mode: VertexStepMode::Vertex,
73 attributes: vec![
74 VertexAttribute { format: VertexFormat::Float32x3, offset: 0, shader_location: 0 }, VertexAttribute { format: VertexFormat::Float32x2, offset: 12, shader_location: 1 }, VertexAttribute { format: VertexFormat::Float32x3, offset: 20, shader_location: 2 }, VertexAttribute { format: VertexFormat::Float32x4, offset: 32, shader_location: 3 }, VertexAttribute { format: VertexFormat::Uint16x4, offset: 48, shader_location: 8 }, VertexAttribute { format: VertexFormat::Float32x4, offset: 56, shader_location: 9 }, ],
81 }
82 }
83}
84
85pub struct SkinnedMesh {
91 vertices: Vec<SkinnedVertex>,
92 indices: Vec<u32>,
93}
94
95pub struct SkinnedMeshBuilder {
99 vertices: Vec<SkinnedVertex>,
100 indices: Vec<u32>,
101}
102
103impl SkinnedMeshBuilder {
104 pub fn new(vertices: Vec<SkinnedVertex>, indices: Vec<u32>) -> Self {
105 Self { vertices, indices }
106 }
107
108 fn validate(&self) {
114 if self.vertices.is_empty() {
115 tracing::warn!("SkinnedMeshBuilder::new(): no vertices — did you forget to pass them?");
116 }
117 if self.indices.is_empty() {
118 tracing::warn!("SkinnedMeshBuilder::new(): no indices — did you forget to pass them?");
119 }
120 for (i, vertex) in self.vertices.iter().enumerate() {
121 let sum: f32 = vertex.joint_weights.iter().sum();
122 if (sum - 1.0).abs() > 0.01 {
123 tracing::warn!(
124 "SkinnedMeshBuilder: vertex {i}'s joint_weights sum to {sum}, not ~1.0 — did \
125 you forget to normalize them?"
126 );
127 }
128 }
129 }
130
131 pub fn build(self) -> SkinnedMesh {
133 self.validate();
134 SkinnedMesh { vertices: self.vertices, indices: self.indices }
135 }
136
137 pub fn build_asset(self, name: &str, assets: &mut Assets<SkinnedMesh>) -> Handle<SkinnedMesh> {
140 let mesh = self.build();
141 assets.insert(name, mesh)
142 }
143}
144
145pub struct GPUSkinnedMesh {
149 pub vertex_buffer: Buffer,
150 pub index_buffer: Buffer,
151 pub index_count: u32,
152}
153
154impl AssetSource for SkinnedMesh {
155 type Processed = GPUSkinnedMesh;
156}
157
158impl Asset<WGPUBackend> for SkinnedMesh {
159 type Deps<'a> = ();
160
161 fn upload<'a>(&self, backend: &WGPUBackend, _deps: &()) -> Option<GPUSkinnedMesh> {
162 let vertex_buffer = BufferBuilder::with_data(bytemuck::cast_slice(self.vertices.as_slice()))
163 .with_label("SkinnedMesh Vertex Buffer")
164 .with_usage(BufferUsages::VERTEX)
165 .build(backend);
166 let index_buffer = BufferBuilder::with_data(bytemuck::cast_slice(&self.indices))
167 .with_label("SkinnedMesh Index Buffer")
168 .with_usage(BufferUsages::INDEX)
169 .build(backend);
170 Some(GPUSkinnedMesh {
171 vertex_buffer,
172 index_buffer,
173 index_count: self.indices.len() as u32,
174 })
175 }
176}
177
178crate::wgpu::plugin_macros::asset_plugin! {
179 SkinnedMeshPlugin, SkinnedMesh
183}
184
185pub struct SkinnedModelBuilder {
191 primary: String,
192 extra_clips: Vec<(String, String)>,
193}
194
195impl SkinnedModelBuilder {
196 pub fn with_animation(mut self, name: impl Into<String>, path: impl Into<String>) -> Self {
198 self.extra_clips.push((name.into(), path.into()));
199 self
200 }
201
202 pub fn build(self, assets: &mut Assets<SkinnedMesh>) -> Result<LoadedSkinnedMesh, super::gltf_loader::ModelLoadError> {
205 let model = super::gltf_loader::load_gltf(&self.primary)?;
206
207 let skeleton = model.skeleton
208 .ok_or_else(|| super::gltf_loader::ModelLoadError::MissingData("no skeleton in model".to_string()))?;
209 let skeleton = Arc::new(skeleton);
210
211 let meshes: Vec<(String, Handle<SkinnedMesh>)> = model.skinned_meshes
212 .into_iter()
213 .map(|(name, mesh)| {
214 let handle = assets.insert(&name, mesh);
215 (name, handle)
216 })
217 .collect();
218
219 let mut clips: HashMap<String, super::animation::AnimationClip> = model.animations
220 .into_iter()
221 .map(|clip| (clip.name.clone(), clip))
222 .collect();
223
224 for (clip_name, path) in self.extra_clips {
225 let extra = super::gltf_loader::load_gltf(&path)?;
226 for clip in extra.animations {
227 clips.insert(clip_name.clone(), clip);
228 }
229 }
230
231 let player = super::player::AnimationPlayer::new(Arc::clone(&skeleton), Arc::new(clips));
232 Ok(LoadedSkinnedMesh { meshes, player })
233 }
234}
235
236pub struct LoadedSkinnedMesh {
239 pub meshes: Vec<(String, Handle<SkinnedMesh>)>,
240 pub player: super::player::AnimationPlayer,
241}
242
243impl LoadedSkinnedMesh {
244 pub fn mesh(&self) -> Option<Handle<SkinnedMesh>> {
247 self.meshes.first().map(|(_, h)| *h)
248 }
249}
250
251impl SkinnedMeshBuilder {
252 pub fn from_file(path: &str) -> SkinnedModelBuilder {
254 SkinnedModelBuilder { primary: path.to_string(), extra_clips: Vec::new() }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn vertex(joint_weights: [f32; 4]) -> SkinnedVertex {
263 SkinnedVertex::new(
264 glam::Vec3::ZERO,
265 glam::Vec2::ZERO,
266 glam::Vec3::Z,
267 glam::Vec4::new(1.0, 0.0, 0.0, 1.0),
268 [0, 0, 0, 0],
269 joint_weights,
270 )
271 }
272
273 #[test]
274 fn layout_matches_the_verified_byte_offsets() {
275 assert_eq!(std::mem::size_of::<SkinnedVertex>(), 80);
276 let layout = SkinnedVertex::layout();
277 assert_eq!(layout.array_stride, 80);
278 assert_eq!(layout.attributes.len(), 6);
279 assert_eq!(layout.attributes[4].offset, 48);
280 assert_eq!(layout.attributes[4].shader_location, 8);
281 assert_eq!(layout.attributes[5].offset, 56);
282 assert_eq!(layout.attributes[5].shader_location, 9);
283 }
284
285 #[test]
286 fn build_does_not_panic_regardless_of_weight_sum() {
287 let mesh = SkinnedMeshBuilder::new(vec![vertex([0.5, 0.0, 0.0, 0.0])], vec![0]).build();
290 assert_eq!(mesh.vertices.len(), 1);
291 }
292
293 #[test]
294 fn build_with_normalized_weights_does_not_panic() {
295 let mesh = SkinnedMeshBuilder::new(vec![vertex([1.0, 0.0, 0.0, 0.0])], vec![0]).build();
296 assert_eq!(mesh.indices.len(), 1);
297 }
298}