use std::{collections::HashMap, sync::Arc};
use crate::{
assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
wgpu::{
backend::WGPUBackend,
buffer::Buffer,
buffers::BufferBuilder,
flags::BufferUsages,
vertex_format::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode},
},
};
#[repr(C)]
#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SkinnedVertex {
pub position: glam::Vec3,
pub tex_coords: glam::Vec2,
pub normal: glam::Vec3,
pub tangent: glam::Vec4,
pub joint_indices: [u16; 4],
pub joint_weights: [f32; 4],
_pad: [u32; 2],
}
impl SkinnedVertex {
pub fn new(
position: glam::Vec3,
tex_coords: glam::Vec2,
normal: glam::Vec3,
tangent: glam::Vec4,
joint_indices: [u16; 4],
joint_weights: [f32; 4],
) -> Self {
Self {
position,
tex_coords,
normal,
tangent,
joint_indices,
joint_weights,
_pad: [0; 2],
}
}
pub fn layout() -> VertexBufferLayout {
VertexBufferLayout {
array_stride: std::mem::size_of::<SkinnedVertex>() as u64,
step_mode: VertexStepMode::Vertex,
attributes: vec![
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 }, ],
}
}
}
pub struct SkinnedMesh {
vertices: Vec<SkinnedVertex>,
indices: Vec<u32>,
}
pub struct SkinnedMeshBuilder {
vertices: Vec<SkinnedVertex>,
indices: Vec<u32>,
}
impl SkinnedMeshBuilder {
pub fn new(vertices: Vec<SkinnedVertex>, indices: Vec<u32>) -> Self {
Self { vertices, indices }
}
fn validate(&self) {
if self.vertices.is_empty() {
tracing::warn!("SkinnedMeshBuilder::new(): no vertices — did you forget to pass them?");
}
if self.indices.is_empty() {
tracing::warn!("SkinnedMeshBuilder::new(): no indices — did you forget to pass them?");
}
for (i, vertex) in self.vertices.iter().enumerate() {
let sum: f32 = vertex.joint_weights.iter().sum();
if (sum - 1.0).abs() > 0.01 {
tracing::warn!(
"SkinnedMeshBuilder: vertex {i}'s joint_weights sum to {sum}, not ~1.0 — did \
you forget to normalize them?"
);
}
}
}
pub fn build(self) -> SkinnedMesh {
self.validate();
SkinnedMesh { vertices: self.vertices, indices: self.indices }
}
pub fn build_asset(self, name: &str, assets: &mut Assets<SkinnedMesh>) -> Handle<SkinnedMesh> {
let mesh = self.build();
assets.insert(name, mesh)
}
}
pub struct GPUSkinnedMesh {
pub vertex_buffer: Buffer,
pub index_buffer: Buffer,
pub index_count: u32,
}
impl AssetSource for SkinnedMesh {
type Processed = GPUSkinnedMesh;
}
impl Asset<WGPUBackend> for SkinnedMesh {
type Deps<'a> = ();
fn upload<'a>(&self, backend: &WGPUBackend, _deps: &()) -> Option<GPUSkinnedMesh> {
let vertex_buffer = BufferBuilder::with_data(bytemuck::cast_slice(self.vertices.as_slice()))
.with_label("SkinnedMesh Vertex Buffer")
.with_usage(BufferUsages::VERTEX)
.build(backend);
let index_buffer = BufferBuilder::with_data(bytemuck::cast_slice(&self.indices))
.with_label("SkinnedMesh Index Buffer")
.with_usage(BufferUsages::INDEX)
.build(backend);
Some(GPUSkinnedMesh {
vertex_buffer,
index_buffer,
index_count: self.indices.len() as u32,
})
}
}
crate::wgpu::plugin_macros::asset_plugin! {
SkinnedMeshPlugin, SkinnedMesh
}
pub struct SkinnedModelBuilder {
primary: String,
extra_clips: Vec<(String, String)>,
}
impl SkinnedModelBuilder {
pub fn with_animation(mut self, name: impl Into<String>, path: impl Into<String>) -> Self {
self.extra_clips.push((name.into(), path.into()));
self
}
pub fn build(self, assets: &mut Assets<SkinnedMesh>) -> Result<LoadedSkinnedMesh, super::gltf_loader::ModelLoadError> {
let model = super::gltf_loader::load_gltf(&self.primary)?;
let skeleton = model.skeleton
.ok_or_else(|| super::gltf_loader::ModelLoadError::MissingData("no skeleton in model".to_string()))?;
let skeleton = Arc::new(skeleton);
let meshes: Vec<(String, Handle<SkinnedMesh>)> = model.skinned_meshes
.into_iter()
.map(|(name, mesh)| {
let handle = assets.insert(&name, mesh);
(name, handle)
})
.collect();
let mut clips: HashMap<String, super::animation::AnimationClip> = model.animations
.into_iter()
.map(|clip| (clip.name.clone(), clip))
.collect();
for (clip_name, path) in self.extra_clips {
let extra = super::gltf_loader::load_gltf(&path)?;
for clip in extra.animations {
clips.insert(clip_name.clone(), clip);
}
}
let player = super::player::AnimationPlayer::new(Arc::clone(&skeleton), Arc::new(clips));
Ok(LoadedSkinnedMesh { meshes, player })
}
}
pub struct LoadedSkinnedMesh {
pub meshes: Vec<(String, Handle<SkinnedMesh>)>,
pub player: super::player::AnimationPlayer,
}
impl LoadedSkinnedMesh {
pub fn mesh(&self) -> Option<Handle<SkinnedMesh>> {
self.meshes.first().map(|(_, h)| *h)
}
}
impl SkinnedMeshBuilder {
pub fn from_file(path: &str) -> SkinnedModelBuilder {
SkinnedModelBuilder { primary: path.to_string(), extra_clips: Vec::new() }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vertex(joint_weights: [f32; 4]) -> SkinnedVertex {
SkinnedVertex::new(
glam::Vec3::ZERO,
glam::Vec2::ZERO,
glam::Vec3::Z,
glam::Vec4::new(1.0, 0.0, 0.0, 1.0),
[0, 0, 0, 0],
joint_weights,
)
}
#[test]
fn layout_matches_the_verified_byte_offsets() {
assert_eq!(std::mem::size_of::<SkinnedVertex>(), 80);
let layout = SkinnedVertex::layout();
assert_eq!(layout.array_stride, 80);
assert_eq!(layout.attributes.len(), 6);
assert_eq!(layout.attributes[4].offset, 48);
assert_eq!(layout.attributes[4].shader_location, 8);
assert_eq!(layout.attributes[5].offset, 56);
assert_eq!(layout.attributes[5].shader_location, 9);
}
#[test]
fn build_does_not_panic_regardless_of_weight_sum() {
let mesh = SkinnedMeshBuilder::new(vec![vertex([0.5, 0.0, 0.0, 0.0])], vec![0]).build();
assert_eq!(mesh.vertices.len(), 1);
}
#[test]
fn build_with_normalized_weights_does_not_panic() {
let mesh = SkinnedMeshBuilder::new(vec![vertex([1.0, 0.0, 0.0, 0.0])], vec![0]).build();
assert_eq!(mesh.indices.len(), 1);
}
}