diff --git a/Cargo.toml b/Cargo.toml index 6577c9317ea95..3e63ca08a7e7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -235,6 +235,15 @@ path = "examples/3d/update_gltf_scene.rs" name = "wireframe" path = "examples/3d/wireframe.rs" +# Animation +[[example]] +name = "custom_skinned_mesh" +path = "examples/animation/custom_skinned_mesh.rs" + +[[example]] +name = "gltf_skinned_mesh" +path = "examples/animation/gltf_skinned_mesh.rs" + # Application [[example]] name = "custom_loop" diff --git a/assets/models/SimpleSkin/SimpleSkin.gltf b/assets/models/SimpleSkin/SimpleSkin.gltf new file mode 100644 index 0000000000000..6e68616c72762 --- /dev/null +++ b/assets/models/SimpleSkin/SimpleSkin.gltf @@ -0,0 +1 @@ +{"scenes":[{"nodes":[0]}],"nodes":[{"skin":0,"mesh":0,"children":[1]},{"children":[2],"translation":[0,1,0]},{"rotation":[0,0,0,1]}],"meshes":[{"primitives":[{"attributes":{"POSITION":1,"JOINTS_0":2,"WEIGHTS_0":3},"indices":0}]}],"skins":[{"inverseBindMatrices":4,"joints":[1,2]}],"animations":[{"channels":[{"sampler":0,"target":{"node":2,"path":"rotation"}}],"samplers":[{"input":5,"interpolation":"LINEAR","output":6}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAD8AAAAAAACAPwAAAD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAwD8AAAAAAACAPwAAwD8AAAAAAAAAAAAAAEAAAAAAAACAPwAAAEAAAAAA","byteLength":168},{"uri":"data:application/gltf-buffer;base64,AAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=","byteLength":320},{"uri":"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAvwAAgL8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAL8AAIC/AAAAAAAAgD8=","byteLength":128},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/","byteLength":240}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":48,"target":34963},{"buffer":0,"byteOffset":48,"byteLength":120,"target":34962},{"buffer":1,"byteOffset":0,"byteLength":320,"byteStride":16},{"buffer":2,"byteOffset":0,"byteLength":128},{"buffer":3,"byteOffset":0,"byteLength":240}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5123,"count":24,"type":"SCALAR","max":[9],"min":[0]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":10,"type":"VEC3","max":[1,2,0],"min":[0,0,0]},{"bufferView":2,"byteOffset":0,"componentType":5123,"count":10,"type":"VEC4","max":[0,1,0,0],"min":[0,1,0,0]},{"bufferView":2,"byteOffset":160,"componentType":5126,"count":10,"type":"VEC4","max":[1,1,0,0],"min":[0,0,0,0]},{"bufferView":3,"byteOffset":0,"componentType":5126,"count":2,"type":"MAT4","max":[1,0,0,0,0,1,0,0,0,0,1,0,-0.5,-1,0,1],"min":[1,0,0,0,0,1,0,0,0,0,1,0,-0.5,-1,0,1]},{"bufferView":4,"byteOffset":0,"componentType":5126,"count":12,"type":"SCALAR","max":[5.5],"min":[0]},{"bufferView":4,"byteOffset":48,"componentType":5126,"count":12,"type":"VEC4","max":[0,0,0.707,1],"min":[0,0,-0.707,0.707]}],"asset":{"version":"2.0"}} \ No newline at end of file diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 3a8bcda3a7d97..2f9066069daa4 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -3,7 +3,7 @@ use bevy_asset::{ AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset, }; use bevy_core::Name; -use bevy_ecs::{prelude::FromWorld, world::World}; +use bevy_ecs::{entity::Entity, prelude::FromWorld, world::World}; use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder}; use bevy_log::warn; use bevy_math::{Mat4, Quat, Vec3}; @@ -16,7 +16,10 @@ use bevy_render::{ Camera, Camera2d, Camera3d, CameraProjection, OrthographicProjection, PerspectiveProjection, }, color::Color, - mesh::{Indices, Mesh, VertexAttributeValues}, + mesh::{ + skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, + Indices, Mesh, VertexAttributeValues, + }, primitives::{Aabb, Frustum}, render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor}, renderer::RenderDevice, @@ -249,6 +252,18 @@ async fn load_gltf<'a, 'b>( // mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute); // } + if let Some(iter) = reader.read_joints(0) { + let vertex_attribute = VertexAttributeValues::Uint16x4(iter.into_u16().collect()); + mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, vertex_attribute); + } + + if let Some(vertex_attribute) = reader + .read_weights(0) + .map(|v| VertexAttributeValues::Float32x4(v.into_f32().collect())) + { + mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, vertex_attribute); + } + if let Some(indices) = reader.read_indices() { mesh.set_indices(Some(Indices::U32(indices.into_u32().collect()))); }; @@ -384,18 +399,45 @@ async fn load_gltf<'a, 'b>( }); } + let skinned_mesh_inverse_bindposes: Vec<_> = gltf + .skins() + .map(|gltf_skin| { + let reader = gltf_skin.reader(|buffer| Some(&buffer_data[buffer.index()])); + let inverse_bindposes: Vec = reader + .read_inverse_bind_matrices() + .unwrap() + .map(|mat| Mat4::from_cols_array_2d(&mat)) + .collect(); + + load_context.set_labeled_asset( + &skin_label(&gltf_skin), + LoadedAsset::new(SkinnedMeshInverseBindposes::from(inverse_bindposes)), + ) + }) + .collect(); + let mut scenes = vec![]; let mut named_scenes = HashMap::default(); for scene in gltf.scenes() { let mut err = None; let mut world = World::default(); + let mut node_index_to_entity_map = HashMap::new(); + let mut entity_to_skin_index_map = HashMap::new(); + world .spawn() .insert_bundle(TransformBundle::identity()) .with_children(|parent| { for node in scene.nodes() { - let result = - load_node(&node, parent, load_context, &buffer_data, &animated_nodes); + let result = load_node( + &node, + parent, + load_context, + &buffer_data, + &animated_nodes, + &mut node_index_to_entity_map, + &mut entity_to_skin_index_map, + ); if result.is_err() { err = Some(result); return; @@ -405,6 +447,21 @@ async fn load_gltf<'a, 'b>( if let Some(Err(err)) = err { return Err(err); } + + for (&entity, &skin_index) in &entity_to_skin_index_map { + let mut entity = world.entity_mut(entity); + let skin = gltf.skins().nth(skin_index).unwrap(); + let joint_entities: Vec<_> = skin + .joints() + .map(|node| node_index_to_entity_map[&node.index()]) + .collect(); + + entity.insert(SkinnedMesh { + inverse_bindposes: skinned_mesh_inverse_bindposes[skin_index].clone(), + joints: joint_entities, + }); + } + let scene_handle = load_context .set_labeled_asset(&scene_label(&scene), LoadedAsset::new(Scene::new(world))); @@ -575,6 +632,8 @@ fn load_node( load_context: &mut LoadContext, buffer_data: &[Vec], animated_nodes: &HashSet, + node_index_to_entity_map: &mut HashMap, + entity_to_skin_index_map: &mut HashMap, ) -> Result<(), GltfError> { let transform = gltf_node.transform(); let mut gltf_error = None; @@ -645,6 +704,9 @@ fn load_node( } } + // Map node index to entity + node_index_to_entity_map.insert(gltf_node.index(), node.id()); + node.with_children(|parent| { if let Some(mesh) = gltf_node.mesh() { // append primitives @@ -660,13 +722,13 @@ fn load_node( } let primitive_label = primitive_label(&mesh, &primitive); + let bounds = primitive.bounding_box(); let mesh_asset_path = AssetPath::new_ref(load_context.path(), Some(&primitive_label)); let material_asset_path = AssetPath::new_ref(load_context.path(), Some(&material_label)); - let bounds = primitive.bounding_box(); - parent + let node = parent .spawn_bundle(PbrBundle { mesh: load_context.get_handle(mesh_asset_path), material: load_context.get_handle(material_asset_path), @@ -675,7 +737,13 @@ fn load_node( .insert(Aabb::from_min_max( Vec3::from_slice(&bounds.min), Vec3::from_slice(&bounds.max), - )); + )) + .id(); + + // Mark for adding skinned mesh + if let Some(skin) = gltf_node.skin() { + entity_to_skin_index_map.insert(node, skin.index()); + } } } @@ -723,7 +791,15 @@ fn load_node( // append other nodes for child in gltf_node.children() { - if let Err(err) = load_node(&child, parent, load_context, buffer_data, animated_nodes) { + if let Err(err) = load_node( + &child, + parent, + load_context, + buffer_data, + animated_nodes, + node_index_to_entity_map, + entity_to_skin_index_map, + ) { gltf_error = Some(err); return; } @@ -770,6 +846,10 @@ fn scene_label(scene: &gltf::Scene) -> String { format!("Scene{}", scene.index()) } +fn skin_label(skin: &gltf::Skin) -> String { + format!("Skin{}", skin.index()) +} + /// Extracts the texture sampler data from the glTF texture. fn texture_sampler<'a>(texture: &gltf::Texture) -> SamplerDescriptor<'a> { let gltf_sampler = texture.sampler(); diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 04f01ab963f6e..e1496ac67c0a3 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -29,3 +29,4 @@ bevy_window = { path = "../bevy_window", version = "0.7.0-dev" } bitflags = "1.2" # direct dependency required for derive macro bytemuck = { version = "1", features = ["derive"] } +smallvec = "1.0" diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index c5bafd333e2a6..5e0648adcd43a 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -245,11 +245,11 @@ impl SpecializedMeshPipeline for MaterialPipeline { if let Some(fragment_shader) = &self.fragment_shader { descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); } - descriptor.layout = Some(vec![ - self.mesh_pipeline.view_layout.clone(), - self.material_layout.clone(), - self.mesh_pipeline.mesh_layout.clone(), - ]); + + // MeshPipeline::specialize's current implementation guarantees that the returned + // specialized descriptor has a populated layout + let descriptor_layout = descriptor.layout.as_mut().unwrap(); + descriptor_layout.insert(1, self.material_layout.clone()); M::specialize(&mut descriptor, key.material_key, layout)?; Ok(descriptor) diff --git a/crates/bevy_pbr/src/render/depth.wgsl b/crates/bevy_pbr/src/render/depth.wgsl index 857ece24728b0..9f5e17fca3433 100644 --- a/crates/bevy_pbr/src/render/depth.wgsl +++ b/crates/bevy_pbr/src/render/depth.wgsl @@ -12,8 +12,18 @@ var view: View; [[group(1), binding(0)]] var mesh: Mesh; +#ifdef SKINNED +[[group(1), binding(1)]] +var joint_matrices: SkinnedMesh; +#import bevy_pbr::skinning +#endif + struct Vertex { [[location(0)]] position: vec3; +#ifdef SKINNED + [[location(4)]] joint_indices: vec4; + [[location(5)]] joint_weights: vec4; +#endif }; struct VertexOutput { @@ -22,7 +32,13 @@ struct VertexOutput { [[stage(vertex)]] fn vertex(vertex: Vertex) -> VertexOutput { +#ifdef SKINNED + let model = skin_model(vertex.joint_indices, vertex.joint_weights); +#else + let model = mesh.model; +#endif + var out: VertexOutput; - out.clip_position = view.view_proj * mesh.model * vec4(vertex.position, 1.0); + out.clip_position = view.view_proj * model * vec4(vertex.position, 1.0); return out; } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 21297fac70b99..002d5d6afa936 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -159,6 +159,7 @@ pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; pub struct ShadowPipeline { pub view_layout: BindGroupLayout, pub mesh_layout: BindGroupLayout, + pub skinned_mesh_layout: BindGroupLayout, pub point_light_sampler: Sampler, pub directional_light_sampler: Sampler, } @@ -187,10 +188,12 @@ impl FromWorld for ShadowPipeline { }); let mesh_pipeline = world.get_resource::().unwrap(); + let skinned_mesh_layout = mesh_pipeline.skinned_mesh_layout.clone(); ShadowPipeline { view_layout, mesh_layout: mesh_pipeline.mesh_layout.clone(), + skinned_mesh_layout, point_light_sampler: render_device.create_sampler(&SamplerDescriptor { address_mode_u: AddressMode::ClampToEdge, address_mode_v: AddressMode::ClampToEdge, @@ -256,18 +259,31 @@ impl SpecializedMeshPipeline for ShadowPipeline { key: Self::Key, layout: &MeshVertexBufferLayout, ) -> Result { - let vertex_buffer_layout = - layout.get_layout(&[Mesh::ATTRIBUTE_POSITION.at_shader_location(0)])?; + let mut vertex_attributes = vec![Mesh::ATTRIBUTE_POSITION.at_shader_location(0)]; + + let mut bind_group_layout = vec![self.view_layout.clone(), self.mesh_layout.clone()]; + let mut shader_defs = Vec::new(); + + if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) + && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) + { + shader_defs.push(String::from("SKINNED")); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(4)); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(5)); + bind_group_layout.push(self.skinned_mesh_layout.clone()); + } + + let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?; Ok(RenderPipelineDescriptor { vertex: VertexState { shader: SHADOW_SHADER_HANDLE.typed::(), entry_point: "vertex".into(), - shader_defs: vec![], + shader_defs, buffers: vec![vertex_buffer_layout], }, fragment: None, - layout: Some(vec![self.view_layout.clone(), self.mesh_layout.clone()]), + layout: Some(bind_group_layout), primitive: PrimitiveState { topology: key.primitive_topology(), strip_index_format: None, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index d7d01bad5a997..08ac42b83865d 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -3,7 +3,7 @@ use crate::{ ViewClusterBindings, ViewLightsUniformOffset, ViewShadowBindings, }; use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, Handle, HandleUntyped}; +use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, @@ -11,7 +11,10 @@ use bevy_ecs::{ use bevy_math::{Mat4, Size}; use bevy_reflect::TypeUuid; use bevy_render::{ - mesh::{GpuBufferInfo, Mesh, MeshVertexBufferLayout}, + mesh::{ + skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, + GpuBufferInfo, Mesh, MeshVertexBufferLayout, + }, render_asset::RenderAssets, render_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, render_phase::{EntityRenderCommand, RenderCommandResult, TrackedRenderPass}, @@ -22,16 +25,24 @@ use bevy_render::{ RenderApp, RenderStage, }; use bevy_transform::components::GlobalTransform; +use smallvec::SmallVec; +use std::num::NonZeroU64; #[derive(Default)] pub struct MeshRenderPlugin; +const MAX_JOINTS: usize = 256; +const JOINT_SIZE: usize = std::mem::size_of::(); +pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; + pub const MESH_VIEW_BIND_GROUP_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9076678235888822571); pub const MESH_STRUCT_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2506024101911992377); pub const MESH_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 3252377289100772450); +pub const SKINNING_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 13215291596265391738); impl Plugin for MeshRenderPlugin { fn build(&self, app: &mut bevy_app::App) { @@ -48,13 +59,17 @@ impl Plugin for MeshRenderPlugin { "mesh_view_bind_group.wgsl", Shader::from_wgsl ); + load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl); app.add_plugin(UniformComponentPlugin::::default()); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() + .init_resource::() .add_system_to_stage(RenderStage::Extract, extract_meshes) + .add_system_to_stage(RenderStage::Extract, extract_skinned_meshes) + .add_system_to_stage(RenderStage::Prepare, prepare_skinned_meshes) .add_system_to_stage(RenderStage::Queue, queue_mesh_bind_group) .add_system_to_stage(RenderStage::Queue, queue_mesh_view_bind_groups); } @@ -129,7 +144,7 @@ pub fn extract_meshes( commands.insert_or_spawn_batch(caster_values); let mut not_caster_values = Vec::with_capacity(*previous_not_caster_len); - for (entity, computed_visibility, transform, handle, not_receiver) in not_caster_query.iter() { + for (entity, computed_visibility, transform, mesh, not_receiver) in not_caster_query.iter() { if !computed_visibility.is_visible { continue; } @@ -137,7 +152,7 @@ pub fn extract_meshes( not_caster_values.push(( entity, ( - handle.clone_weak(), + mesh.clone_weak(), MeshUniform { flags: if not_receiver.is_some() { MeshFlags::empty().bits @@ -155,10 +170,92 @@ pub fn extract_meshes( commands.insert_or_spawn_batch(not_caster_values); } +#[derive(Debug, Default)] +pub struct ExtractedJoints { + pub buffer: Vec, +} + +#[derive(Component)] +pub struct SkinnedMeshJoints { + pub index: u32, +} + +impl SkinnedMeshJoints { + #[inline] + pub fn build( + skin: &SkinnedMesh, + inverse_bindposes: &Assets, + joints: &Query<&GlobalTransform>, + buffer: &mut Vec, + ) -> Option { + let inverse_bindposes = inverse_bindposes.get(&skin.inverse_bindposes)?; + let bindposes = inverse_bindposes.iter(); + let skin_joints = skin.joints.iter(); + let mut temp = + SmallVec::<[Mat4; MAX_JOINTS]>::with_capacity(bindposes.len().min(MAX_JOINTS)); + for (inverse_bindpose, joint) in bindposes.zip(skin_joints).take(MAX_JOINTS) { + let joint_matrix = joints.get(*joint).ok()?.compute_matrix(); + temp.push(joint_matrix * *inverse_bindpose); + } + + let start = buffer.len(); + buffer.extend(temp); + // Pad to 256 byte alignment + while buffer.len() % 4 != 0 { + buffer.push(Mat4::ZERO); + } + Some(Self { + index: start as u32, + }) + } + + pub fn to_buffer_index(mut self) -> Self { + self.index *= std::mem::size_of::() as u32; + self + } +} + +pub fn extract_skinned_meshes( + query: Query<(Entity, &ComputedVisibility, &SkinnedMesh)>, + inverse_bindposes: Res>, + joint_query: Query<&GlobalTransform>, + mut commands: Commands, + mut previous_len: Local, + mut previous_joint_len: Local, +) { + let mut values = Vec::with_capacity(*previous_len); + let mut joints = Vec::with_capacity(*previous_joint_len); + let mut last_start = 0; + + for (entity, computed_visibility, skin) in query.iter() { + if !computed_visibility.is_visible { + continue; + } + // PERF: This can be expensive, can we move this to prepare? + if let Some(skinned_joints) = + SkinnedMeshJoints::build(skin, &inverse_bindposes, &joint_query, &mut joints) + { + last_start = last_start.max(skinned_joints.index as usize); + values.push((entity, (skinned_joints.to_buffer_index(),))); + } + } + + // Pad out the buffer to ensure that there's enough space for bindings + while joints.len() - last_start < MAX_JOINTS { + joints.push(Mat4::ZERO); + } + + *previous_len = values.len(); + *previous_joint_len = joints.len(); + commands.insert_resource(ExtractedJoints { buffer: joints }); + commands.insert_or_spawn_batch(values); +} + #[derive(Clone)] pub struct MeshPipeline { pub view_layout: BindGroupLayout, pub mesh_layout: BindGroupLayout, + pub skinned_mesh_layout: BindGroupLayout, // This dummy white texture is to be used in place of optional StandardMaterial textures pub dummy_white_gpu_image: GpuImage, } @@ -276,19 +373,40 @@ impl FromWorld for MeshPipeline { label: Some("mesh_view_layout"), }); + let mesh_binding = BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(MeshUniform::std140_size_static() as u64), + }, + count: None, + }; + let mesh_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: BufferSize::new(MeshUniform::std140_size_static() as u64), - }, - count: None, - }], + entries: &[mesh_binding], label: Some("mesh_layout"), }); + + let skinned_mesh_layout = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + mesh_binding, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(JOINT_BUFFER_SIZE as u64), + }, + count: None, + }, + ], + label: Some("skinned_mesh_layout"), + }); + // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures let dummy_white_gpu_image = { let image = Image::new_fill( @@ -338,6 +456,7 @@ impl FromWorld for MeshPipeline { MeshPipeline { view_layout, mesh_layout, + skinned_mesh_layout, dummy_white_gpu_image, } } @@ -429,6 +548,18 @@ impl SpecializedMeshPipeline for MeshPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); } + let mut bind_group_layout = vec![self.view_layout.clone()]; + if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) + && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) + { + shader_defs.push(String::from("SKINNED")); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(4)); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(5)); + bind_group_layout.push(self.skinned_mesh_layout.clone()); + } else { + bind_group_layout.push(self.mesh_layout.clone()); + }; + let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?; let (label, blend, depth_write_enabled); @@ -467,7 +598,7 @@ impl SpecializedMeshPipeline for MeshPipeline { write_mask: ColorWrites::ALL, }], }), - layout: Some(vec![self.view_layout.clone(), self.mesh_layout.clone()]), + layout: Some(bind_group_layout), primitive: PrimitiveState { front_face: FrontFace::Ccw, cull_mode: Some(Face::Back), @@ -504,7 +635,8 @@ impl SpecializedMeshPipeline for MeshPipeline { } pub struct MeshBindGroup { - pub value: BindGroup, + pub normal: BindGroup, + pub skinned: Option, } pub fn queue_mesh_bind_group( @@ -512,19 +644,70 @@ pub fn queue_mesh_bind_group( mesh_pipeline: Res, render_device: Res, mesh_uniforms: Res>, + skinned_mesh_uniform: Res, ) { - if let Some(binding) = mesh_uniforms.uniforms().binding() { - commands.insert_resource(MeshBindGroup { - value: render_device.create_bind_group(&BindGroupDescriptor { + if let Some(mesh_binding) = mesh_uniforms.uniforms().binding() { + let mut mesh_bind_group = MeshBindGroup { + normal: render_device.create_bind_group(&BindGroupDescriptor { entries: &[BindGroupEntry { binding: 0, - resource: binding, + resource: mesh_binding.clone(), }], label: Some("mesh_bind_group"), layout: &mesh_pipeline.mesh_layout, }), - }); + skinned: None, + }; + + if let Some(skinned_joints_buffer) = skinned_mesh_uniform.buffer.uniform_buffer() { + mesh_bind_group.skinned = Some(render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: mesh_binding, + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Buffer(BufferBinding { + buffer: skinned_joints_buffer, + offset: 0, + size: Some(NonZeroU64::new(JOINT_BUFFER_SIZE as u64).unwrap()), + }), + }, + ], + label: Some("skinned_mesh_bind_group"), + layout: &mesh_pipeline.skinned_mesh_layout, + })); + } + commands.insert_resource(mesh_bind_group); + } +} + +#[derive(Default)] +pub struct SkinnedMeshUniform { + pub buffer: UniformVec, +} + +pub fn prepare_skinned_meshes( + render_device: Res, + render_queue: Res, + extracted_joints: Res, + mut skinned_mesh_uniform: ResMut, +) { + if extracted_joints.buffer.is_empty() { + return; } + + skinned_mesh_uniform.buffer.clear(); + skinned_mesh_uniform + .buffer + .reserve(extracted_joints.buffer.len(), &render_device); + for joint in extracted_joints.buffer.iter() { + skinned_mesh_uniform.buffer.push(*joint); + } + skinned_mesh_uniform + .buffer + .write_buffer(&render_device, &render_queue); } #[derive(Component)] @@ -640,7 +823,10 @@ pub struct SetMeshBindGroup; impl EntityRenderCommand for SetMeshBindGroup { type Param = ( SRes, - SQuery>>, + SQuery<( + Read>, + Option>, + )>, ); #[inline] fn render<'w>( @@ -649,12 +835,20 @@ impl EntityRenderCommand for SetMeshBindGroup { (mesh_bind_group, mesh_query): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { - let mesh_index = mesh_query.get(item).unwrap(); - pass.set_bind_group( - I, - &mesh_bind_group.into_inner().value, - &[mesh_index.index()], - ); + let (mesh_index, skinned_mesh_joints) = mesh_query.get(item).unwrap(); + if let Some(joints) = skinned_mesh_joints { + pass.set_bind_group( + I, + mesh_bind_group.into_inner().skinned.as_ref().unwrap(), + &[mesh_index.index(), joints.index], + ); + } else { + pass.set_bind_group( + I, + &mesh_bind_group.into_inner().normal, + &[mesh_index.index()], + ); + } RenderCommandResult::Success } } diff --git a/crates/bevy_pbr/src/render/mesh.wgsl b/crates/bevy_pbr/src/render/mesh.wgsl index 0bb20aeea0e62..ffa26f4b94769 100644 --- a/crates/bevy_pbr/src/render/mesh.wgsl +++ b/crates/bevy_pbr/src/render/mesh.wgsl @@ -8,6 +8,10 @@ struct Vertex { #ifdef VERTEX_TANGENTS [[location(3)]] tangent: vec4; #endif +#ifdef SKINNED + [[location(4)]] joint_indices: vec4; + [[location(5)]] joint_weights: vec4; +#endif }; struct VertexOutput { @@ -22,15 +26,24 @@ struct VertexOutput { [[group(2), binding(0)]] var mesh: Mesh; +#ifdef SKINNED +[[group(2), binding(1)]] +var joint_matrices: SkinnedMesh; +#import bevy_pbr::skinning +#endif [[stage(vertex)]] fn vertex(vertex: Vertex) -> VertexOutput { - let world_position = mesh.model * vec4(vertex.position, 1.0); - var out: VertexOutput; - out.uv = vertex.uv; - out.world_position = world_position; - out.clip_position = view.view_proj * world_position; +#ifdef SKINNED + var model = skin_model(vertex.joint_indices, vertex.joint_weights); + out.world_position = model * vec4(vertex.position, 1.0); + out.world_normal = skin_normals(model, vertex.normal); +#ifdef VERTEX_TANGENTS + out.world_tangent = skin_tangents(model, vertex.tangent); +#endif +#else + out.world_position = mesh.model * vec4(vertex.position, 1.0); out.world_normal = mat3x3( mesh.inverse_transpose_model[0].xyz, mesh.inverse_transpose_model[1].xyz, @@ -46,6 +59,10 @@ fn vertex(vertex: Vertex) -> VertexOutput { vertex.tangent.w ); #endif +#endif + + out.uv = vertex.uv; + out.clip_position = view.view_proj * out.world_position; return out; } diff --git a/crates/bevy_pbr/src/render/mesh_struct.wgsl b/crates/bevy_pbr/src/render/mesh_struct.wgsl index 46eafa7213f92..de29921f46f7b 100644 --- a/crates/bevy_pbr/src/render/mesh_struct.wgsl +++ b/crates/bevy_pbr/src/render/mesh_struct.wgsl @@ -7,4 +7,10 @@ struct Mesh { flags: u32; }; +#ifdef SKINNED +struct SkinnedMesh { + data: array, 256u>; +}; +#endif + let MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u; diff --git a/crates/bevy_pbr/src/render/skinning.wgsl b/crates/bevy_pbr/src/render/skinning.wgsl new file mode 100644 index 0000000000000..56b35634a09cd --- /dev/null +++ b/crates/bevy_pbr/src/render/skinning.wgsl @@ -0,0 +1,66 @@ +// If using this WGSL snippet as an #import, a dedicated +// "joint_matricies" uniform of type SkinnedMesh must be added in the +// main shader. + +#define_import_path bevy_pbr::skinning + +/// HACK: This works around naga not supporting matrix addition in SPIR-V +// translations. See https://github.com/gfx-rs/naga/issues/1527 +fn add_matrix( + a: mat4x4, + b: mat4x4, +) -> mat4x4 { + return mat4x4( + a.x + b.x, + a.y + b.y, + a.z + b.z, + a.w + b.w, + ); +} + +fn skin_model( + indexes: vec4, + weights: vec4, +) -> mat4x4 { + var matrix = weights.x * joint_matrices.data[indexes.x]; + matrix = add_matrix(matrix, weights.y * joint_matrices.data[indexes.y]); + matrix = add_matrix(matrix, weights.z * joint_matrices.data[indexes.z]); + return add_matrix(matrix, weights.w * joint_matrices.data[indexes.w]); +} + +fn inverse_transpose_3x3(in: mat3x3) -> mat3x3 { + let x = cross(in.y, in.z); + let y = cross(in.z, in.x); + let z = cross(in.x, in.y); + let det = dot(in.z, z); + return mat3x3( + x / det, + y / det, + z / det + ); +} + +fn skin_normals( + model: mat4x4, + normal: vec3, +) -> vec3 { + return inverse_transpose_3x3(mat3x3( + model[0].xyz, + model[1].xyz, + model[2].xyz + )) * normal; +} + +fn skin_tangents( + model: mat4x4, + tangent: vec4, +) -> vec4 { + return vec4( + mat3x3( + model[0].xyz, + model[1].xyz, + model[2].xyz + ) * tangent.xyz, + tangent.w + ); +} \ No newline at end of file diff --git a/crates/bevy_pbr/src/render/wireframe.wgsl b/crates/bevy_pbr/src/render/wireframe.wgsl index b76c39e695582..66d91653f73cc 100644 --- a/crates/bevy_pbr/src/render/wireframe.wgsl +++ b/crates/bevy_pbr/src/render/wireframe.wgsl @@ -3,6 +3,10 @@ struct Vertex { [[location(0)]] position: vec3; +#ifdef SKINNED + [[location(4)]] joint_indexes: vec4; + [[location(5)]] joint_weights: vec4; +#endif }; [[group(1), binding(0)]] @@ -12,10 +16,21 @@ struct VertexOutput { [[builtin(position)]] clip_position: vec4; }; +#ifdef SKINNED +[[group(2), binding(0)]] +var joint_matrices: SkinnedMesh; +#import bevy_pbr::skinning +#endif + [[stage(vertex)]] fn vertex(vertex: Vertex) -> VertexOutput { - let world_position = mesh.model * vec4(vertex.position, 1.0); +#ifdef SKINNED + let model = skin_model(vertex.joint_indexes, vertex.joint_weights); +#else + let model = mesh.model; +#endif + let world_position = model * vec4(vertex.position, 1.0); var out: VertexOutput; out.clip_position = view.view_proj * world_position; diff --git a/crates/bevy_render/src/mesh/mesh/conversions.rs b/crates/bevy_render/src/mesh/mesh/conversions.rs index 9bf4a55f76488..a49edbe1ce5a4 100644 --- a/crates/bevy_render/src/mesh/mesh/conversions.rs +++ b/crates/bevy_render/src/mesh/mesh/conversions.rs @@ -118,6 +118,12 @@ impl From> for VertexAttributeValues { } } +impl From> for VertexAttributeValues { + fn from(vec: Vec<[u16; 4]>) -> Self { + VertexAttributeValues::Uint16x4(vec) + } +} + impl From> for VertexAttributeValues { fn from(vec: Vec<[u8; 4]>) -> Self { VertexAttributeValues::Unorm8x4(vec) diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index 9c7488bdbff5b..babf3c2530837 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -1,4 +1,6 @@ mod conversions; +pub mod skinning; +pub use wgpu::PrimitiveTopology; use crate::{ primitives::Aabb, @@ -14,8 +16,8 @@ use bevy_utils::{EnumVariantMeta, Hashed}; use std::{collections::BTreeMap, hash::Hash}; use thiserror::Error; use wgpu::{ - util::BufferInitDescriptor, BufferUsages, IndexFormat, PrimitiveTopology, VertexAttribute, - VertexFormat, VertexStepMode, + util::BufferInitDescriptor, BufferUsages, IndexFormat, VertexAttribute, VertexFormat, + VertexStepMode, }; pub const INDEX_BUFFER_ASSET_INDEX: u64 = 0; @@ -78,7 +80,7 @@ impl Mesh { MeshVertexAttribute::new("Vertex_JointWeight", 5, VertexFormat::Float32x4); /// Per vertex joint transform matrix index. Use in conjunction with [`Mesh::insert_attribute`] pub const ATTRIBUTE_JOINT_INDEX: MeshVertexAttribute = - MeshVertexAttribute::new("Vertex_JointIndex", 6, VertexFormat::Uint32); + MeshVertexAttribute::new("Vertex_JointIndex", 6, VertexFormat::Uint16x4); /// Construct a new mesh. You need to provide a [`PrimitiveTopology`] so that the /// renderer knows how to treat the vertex data. Most of the time this will be diff --git a/crates/bevy_render/src/mesh/mesh/skinning.rs b/crates/bevy_render/src/mesh/mesh/skinning.rs new file mode 100644 index 0000000000000..6d152689069ea --- /dev/null +++ b/crates/bevy_render/src/mesh/mesh/skinning.rs @@ -0,0 +1,44 @@ +use bevy_asset::Handle; +use bevy_ecs::{ + component::Component, + entity::{Entity, EntityMap, MapEntities, MapEntitiesError}, + prelude::ReflectComponent, + reflect::ReflectMapEntities, +}; +use bevy_math::Mat4; +use bevy_reflect::{Reflect, TypeUuid}; +use std::ops::Deref; + +#[derive(Component, Debug, Default, Clone, Reflect)] +#[reflect(Component, MapEntities)] +pub struct SkinnedMesh { + pub inverse_bindposes: Handle, + pub joints: Vec, +} + +impl MapEntities for SkinnedMesh { + fn map_entities(&mut self, entity_map: &EntityMap) -> Result<(), MapEntitiesError> { + for joint in &mut self.joints { + *joint = entity_map.get(*joint)?; + } + + Ok(()) + } +} + +#[derive(Debug, TypeUuid)] +#[uuid = "b9f155a9-54ec-4026-988f-e0a03e99a76f"] +pub struct SkinnedMeshInverseBindposes(Box<[Mat4]>); + +impl From> for SkinnedMeshInverseBindposes { + fn from(value: Vec) -> Self { + Self(value.into_boxed_slice()) + } +} + +impl Deref for SkinnedMeshInverseBindposes { + type Target = [Mat4]; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 6740bd02ef44d..156e44de8da64 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -15,6 +15,8 @@ pub struct MeshPlugin; impl Plugin for MeshPlugin { fn build(&self, app: &mut App) { app.add_asset::() + .add_asset::() + .register_type::() .add_plugin(RenderAssetPlugin::::default()); } } diff --git a/examples/README.md b/examples/README.md index 5342f9d4d288c..0a057e01b88f9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,6 +39,7 @@ git checkout v0.4.0 - [Cross-Platform Examples](#cross-platform-examples) - [2D Rendering](#2d-rendering) - [3D Rendering](#3d-rendering) + - [Animation](#animation) - [Application](#application) - [Assets](#assets) - [Async Tasks](#async-tasks) @@ -118,6 +119,13 @@ Example | File | Description `update_gltf_scene` | [`3d/update_gltf_scene.rs`](./3d/update_gltf_scene.rs) | Update a scene from a gltf file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene `wireframe` | [`3d/wireframe.rs`](./3d/wireframe.rs) | Showcases wireframe rendering +## Animation + +Example | File | Description +--- | --- | --- +`custom_skinned_mesh` | [`animation/custom_skinned_mesh.rs`](./animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code. +`gltf_skinned_mesh` | [`animation/gltf_skinned_mesh.rs`](./animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file. + ## Application Example | File | Description diff --git a/examples/animation/custom_skinned_mesh.rs b/examples/animation/custom_skinned_mesh.rs new file mode 100644 index 0000000000000..5216cd08a65de --- /dev/null +++ b/examples/animation/custom_skinned_mesh.rs @@ -0,0 +1,171 @@ +use std::f32::consts::PI; + +use bevy::{ + pbr::AmbientLight, + prelude::*, + render::mesh::{ + skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, + Indices, PrimitiveTopology, + }, +}; +use rand::Rng; + +/// Skinned mesh example with mesh and joints data defined in code. +/// Example taken from +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(AmbientLight { + brightness: 1.0, + ..Default::default() + }) + .add_startup_system(setup) + .add_system(joint_animation) + .run(); +} + +/// Used to mark a joint to be animated in the [`joint_animation`] system. +#[derive(Component)] +struct AnimatedJoint; + +/// Construct a mesh and a skeleton with 2 joints for that mesh, +/// and mark the second joint to be animated. +/// It is similar to the scene defined in `models/SimpleSkin/SimpleSkin.gltf` +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut skinned_mesh_inverse_bindposes_assets: ResMut>, +) { + // Create a camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // Create inverse bindpose matrices for a skeleton consists of 2 joints + let inverse_bindposes = + skinned_mesh_inverse_bindposes_assets.add(SkinnedMeshInverseBindposes::from(vec![ + Mat4::from_translation(Vec3::new(-0.5, -1.0, 0.0)), + Mat4::from_translation(Vec3::new(-0.5, -1.0, 0.0)), + ])); + + // Create a mesh + let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); + // Set mesh vertex positions + mesh.insert_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.5, 0.0], + [1.0, 0.5, 0.0], + [0.0, 1.0, 0.0], + [1.0, 1.0, 0.0], + [0.0, 1.5, 0.0], + [1.0, 1.5, 0.0], + [0.0, 2.0, 0.0], + [1.0, 2.0, 0.0], + ], + ); + // Set mesh vertex normals + mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 10]); + // Set mesh vertex UVs. Although the mesh doesn't have any texture applied, + // UVs are still required by the render pipeline. So these UVs are zeroed out. + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; 10]); + // Set mesh vertex joint indices for mesh skinning. + // Each vertex gets 4 indices used to address the `JointTransforms` array in the vertex shader + // as well as `SkinnedMeshJoint` array in the `SkinnedMesh` component. + // This means that a maximum of 4 joints can affect a single vertex. + mesh.insert_attribute( + Mesh::ATTRIBUTE_JOINT_INDEX, + vec![ + [0u16, 0, 0, 0], + [0, 0, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + ], + ); + // Set mesh vertex joint weights for mesh skinning. + // Each vertex gets 4 joint weights corresponding to the 4 joint indices assigned to it. + // The sum of these weights should equal to 1. + mesh.insert_attribute( + Mesh::ATTRIBUTE_JOINT_WEIGHT, + vec![ + [1.00, 0.00, 0.0, 0.0], + [1.00, 0.00, 0.0, 0.0], + [0.75, 0.25, 0.0, 0.0], + [0.75, 0.25, 0.0, 0.0], + [0.50, 0.50, 0.0, 0.0], + [0.50, 0.50, 0.0, 0.0], + [0.25, 0.75, 0.0, 0.0], + [0.25, 0.75, 0.0, 0.0], + [0.00, 1.00, 0.0, 0.0], + [0.00, 1.00, 0.0, 0.0], + ], + ); + // Tell bevy to construct triangles from a list of vertex indices, + // where each 3 vertex indices form an triangle. + mesh.set_indices(Some(Indices::U16(vec![ + 0, 1, 3, 0, 3, 2, 2, 3, 5, 2, 5, 4, 4, 5, 7, 4, 7, 6, 6, 7, 9, 6, 9, 8, + ]))); + + let mesh = meshes.add(mesh); + for i in -5..5 { + // Create joint entities + let joint_0 = commands + .spawn_bundle(( + Transform::from_xyz(i as f32 * 1.5, 0.0, 0.0), + GlobalTransform::identity(), + )) + .id(); + let joint_1 = commands + .spawn_bundle(( + AnimatedJoint, + Transform::identity(), + GlobalTransform::identity(), + )) + .id(); + + // Set joint_1 as a child of joint_0. + commands.entity(joint_0).push_children(&[joint_1]); + + // Each joint in this vector corresponds to each inverse bindpose matrix in `SkinnedMeshInverseBindposes`. + let joint_entities = vec![joint_0, joint_1]; + + // Create skinned mesh renderer. Note that its transform doesn't affect the position of the mesh. + commands + .spawn_bundle(PbrBundle { + mesh: mesh.clone(), + material: materials.add( + Color::rgb( + rand::thread_rng().gen_range(0.0..1.0), + rand::thread_rng().gen_range(0.0..1.0), + rand::thread_rng().gen_range(0.0..1.0), + ) + .into(), + ), + ..Default::default() + }) + .insert(SkinnedMesh { + inverse_bindposes: inverse_bindposes.clone(), + joints: joint_entities, + }); + } +} + +/// Animate the joint marked with [`AnimatedJoint`] component. +fn joint_animation(time: Res