1use crate::{
2 assets::{handle::Handle, storage::Assets, upload::Asset},
3 wgpu::{
4 backend::WGPUBackend,
5 buffer::Buffer,
6 buffers::BufferBuilder,
7 flags::BufferUsages,
8 vertex_format::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode},
9 },
10};
11
12#[repr(C)]
21#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
22pub struct SkinnedVertex {
23 pub position: glam::Vec3,
24 pub tex_coords: glam::Vec2,
25 pub normal: glam::Vec3,
26 pub tangent: glam::Vec4,
27 pub joint_indices: [u16; 4],
35 pub joint_weights: [f32; 4],
36 _pad: [u32; 2],
37}
38
39impl SkinnedVertex {
40 pub fn new(
41 position: glam::Vec3,
42 tex_coords: glam::Vec2,
43 normal: glam::Vec3,
44 tangent: glam::Vec4,
45 joint_indices: [u16; 4],
46 joint_weights: [f32; 4],
47 ) -> Self {
48 Self {
49 position,
50 tex_coords,
51 normal,
52 tangent,
53 joint_indices,
54 joint_weights,
55 _pad: [0; 2],
56 }
57 }
58
59 pub fn layout() -> VertexBufferLayout {
68 VertexBufferLayout {
69 array_stride: std::mem::size_of::<SkinnedVertex>() as u64,
70 step_mode: VertexStepMode::Vertex,
71 attributes: vec![
72 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 }, ],
79 }
80 }
81}
82
83pub struct SkinnedMesh {
89 vertices: Vec<SkinnedVertex>,
90 indices: Vec<u32>,
91}
92
93pub struct SkinnedMeshBuilder {
97 vertices: Vec<SkinnedVertex>,
98 indices: Vec<u32>,
99}
100
101impl SkinnedMeshBuilder {
102 pub fn new(vertices: Vec<SkinnedVertex>, indices: Vec<u32>) -> Self {
103 Self { vertices, indices }
104 }
105
106 fn validate(&self) {
112 if self.vertices.is_empty() {
113 tracing::warn!("SkinnedMeshBuilder::new(): no vertices — did you forget to pass them?");
114 }
115 if self.indices.is_empty() {
116 tracing::warn!("SkinnedMeshBuilder::new(): no indices — did you forget to pass them?");
117 }
118 for (i, vertex) in self.vertices.iter().enumerate() {
119 let sum: f32 = vertex.joint_weights.iter().sum();
120 if (sum - 1.0).abs() > 0.01 {
121 tracing::warn!(
122 "SkinnedMeshBuilder: vertex {i}'s joint_weights sum to {sum}, not ~1.0 — did \
123 you forget to normalize them?"
124 );
125 }
126 }
127 }
128
129 pub fn build(self) -> SkinnedMesh {
131 self.validate();
132 SkinnedMesh { vertices: self.vertices, indices: self.indices }
133 }
134
135 pub fn build_asset(self, name: &str, assets: &mut Assets<SkinnedMesh>) -> Handle<SkinnedMesh> {
138 let mesh = self.build();
139 assets.insert(name, mesh)
140 }
141}
142
143pub struct GPUSkinnedMesh {
147 pub vertex_buffer: Buffer,
148 pub index_buffer: Buffer,
149 pub index_count: u32,
150}
151
152impl Asset<WGPUBackend> for GPUSkinnedMesh {
153 type Source = SkinnedMesh;
154 type Deps<'a> = ();
155
156 fn upload<'a>(source: &SkinnedMesh, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
157 let vertex_buffer = BufferBuilder::with_data(bytemuck::cast_slice(source.vertices.as_slice()))
158 .label("SkinnedMesh Vertex Buffer")
159 .usage(BufferUsages::VERTEX)
160 .build(backend);
161 let index_buffer = BufferBuilder::with_data(bytemuck::cast_slice(&source.indices))
162 .label("SkinnedMesh Index Buffer")
163 .usage(BufferUsages::INDEX)
164 .build(backend);
165 Some(Self {
166 vertex_buffer,
167 index_buffer,
168 index_count: source.indices.len() as u32,
169 })
170 }
171}
172
173crate::wgpu::plugin_macros::asset_plugin! {
174 SkinnedMeshPlugin, GPUSkinnedMesh
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn vertex(joint_weights: [f32; 4]) -> SkinnedVertex {
186 SkinnedVertex::new(
187 glam::Vec3::ZERO,
188 glam::Vec2::ZERO,
189 glam::Vec3::Z,
190 glam::Vec4::new(1.0, 0.0, 0.0, 1.0),
191 [0, 0, 0, 0],
192 joint_weights,
193 )
194 }
195
196 #[test]
197 fn layout_matches_the_verified_byte_offsets() {
198 assert_eq!(std::mem::size_of::<SkinnedVertex>(), 80);
199 let layout = SkinnedVertex::layout();
200 assert_eq!(layout.array_stride, 80);
201 assert_eq!(layout.attributes.len(), 6);
202 assert_eq!(layout.attributes[4].offset, 48);
203 assert_eq!(layout.attributes[4].shader_location, 8);
204 assert_eq!(layout.attributes[5].offset, 56);
205 assert_eq!(layout.attributes[5].shader_location, 9);
206 }
207
208 #[test]
209 fn build_does_not_panic_regardless_of_weight_sum() {
210 let mesh = SkinnedMeshBuilder::new(vec![vertex([0.5, 0.0, 0.0, 0.0])], vec![0]).build();
213 assert_eq!(mesh.vertices.len(), 1);
214 }
215
216 #[test]
217 fn build_with_normalized_weights_does_not_panic() {
218 let mesh = SkinnedMeshBuilder::new(vec![vertex([1.0, 0.0, 0.0, 0.0])], vec![0]).build();
219 assert_eq!(mesh.indices.len(), 1);
220 }
221}