diff --git a/Cargo.toml b/Cargo.toml index 9e8551ffc3daf..e0f365fa065de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = ["crates/*", "examples/ios", "tools/ci"] [features] default = [ + "bevy_animation_rig", "bevy_audio", "bevy_dynamic_plugin", "bevy_gilrs", @@ -41,6 +42,7 @@ dynamic = ["bevy_dylib"] render = ["bevy_internal/bevy_pbr", "bevy_internal/bevy_render", "bevy_internal/bevy_sprite", "bevy_internal/bevy_text", "bevy_internal/bevy_ui"] # Optional bevy crates +bevy_animation_rig = ["bevy_internal/bevy_animation_rig"] bevy_audio = ["bevy_internal/bevy_audio"] bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"] bevy_gilrs = ["bevy_internal/bevy_gilrs"] @@ -176,6 +178,19 @@ path = "examples/3d/wireframe.rs" name = "z_sort_debug" path = "examples/3d/z_sort_debug.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" + +[[example]] +name = "gltf_skinned_mesh_using_animation_data" +path = "examples/animation/gltf_skinned_mesh_using_animation_data.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_animation_rig/Cargo.toml b/crates/bevy_animation_rig/Cargo.toml new file mode 100644 index 0000000000000..fd543b25232ad --- /dev/null +++ b/crates/bevy_animation_rig/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "bevy_animation_rig" +version = "0.5.0" +edition = "2018" +authors = [ + "Bevy Contributors ", + "Carter Anderson ", +] +description = "Bevy Engine Animation Rigging System" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT" +keywords = ["bevy", "animation", "rig", "skeleton"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.5.0" } +bevy_asset = { path = "../bevy_asset", version = "0.5.0" } +bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" } +bevy_math = { path = "../bevy_math", version = "0.5.0" } +bevy_pbr = { path = "../bevy_pbr", version = "0.5.0" } +bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] } +bevy_render = { path = "../bevy_render", version = "0.5.0" } +bevy_transform = { path = "../bevy_transform", version = "0.5.0" } diff --git a/crates/bevy_animation_rig/src/lib.rs b/crates/bevy_animation_rig/src/lib.rs new file mode 100644 index 0000000000000..a4d4c9e576b5d --- /dev/null +++ b/crates/bevy_animation_rig/src/lib.rs @@ -0,0 +1,39 @@ +use bevy_app::{AppBuilder, CoreStage, Plugin, StartupStage}; +use bevy_asset::AddAsset; +use bevy_ecs::{ + schedule::{ParallelSystemDescriptorCoercion, SystemLabel}, + system::IntoSystem, +}; +use bevy_transform::TransformSystem; + +mod skinned_mesh; +pub use skinned_mesh::*; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum AnimationRigSystem { + SkinnedMeshSetup, + SkinnedMeshUpdate, +} + +#[derive(Default)] +pub struct AnimationRigPlugin; + +impl Plugin for AnimationRigPlugin { + fn build(&self, app: &mut AppBuilder) { + app.register_type::() + .add_asset::() + .add_startup_system_to_stage( + StartupStage::PreStartup, + skinned_mesh_setup + .system() + .label(AnimationRigSystem::SkinnedMeshSetup), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + skinned_mesh_update + .system() + .label(AnimationRigSystem::SkinnedMeshUpdate) + .after(TransformSystem::TransformPropagate), + ); + } +} diff --git a/crates/bevy_animation_rig/src/skinned_mesh.rs b/crates/bevy_animation_rig/src/skinned_mesh.rs new file mode 100644 index 0000000000000..0cff954492368 --- /dev/null +++ b/crates/bevy_animation_rig/src/skinned_mesh.rs @@ -0,0 +1,372 @@ +use bevy_asset::{Assets, Handle, HandleUntyped}; +use bevy_ecs::{ + entity::{Entity, EntityMap, MapEntities, MapEntitiesError}, + reflect::{ReflectComponent, ReflectMapEntities}, + system::{Query, Res, ResMut}, +}; +use bevy_math::Mat4; +use bevy_pbr::render_graph; +use bevy_reflect::{ + serde, DynamicStruct, FieldIter, Reflect, ReflectMut, ReflectRef, Struct, TypeUuid, +}; +use bevy_render::{ + pipeline::PipelineDescriptor, + render_graph::{RenderGraph, RenderResourcesNode}, + renderer::{ + RenderResource, RenderResourceHints, RenderResourceIterator, RenderResourceType, + RenderResources, + }, + shader::{Shader, ShaderStage}, + texture::Texture, +}; +use bevy_transform::components::GlobalTransform; + +/// The name of skinned mesh node +pub mod node { + pub const SKINNED_MESH: &str = "skinned_mesh"; +} + +/// The name of skinned mesh buffer +pub mod buffer { + pub const JOINT_TRANSFORMS: &str = "JointTransforms"; +} + +/// Specify RenderPipelines with this handle to render the skinned mesh. +pub const SKINNED_MESH_PIPELINE_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(PipelineDescriptor::TYPE_UUID, 0x14db1922328e7fcc); + +/// Used to update and bind joint transforms to the skinned mesh render pipeline specified with [`SKINNED_MESH_PIPELINE_HANDLE`]. +/// +/// The length of entities vector passed to [`SkinnedMesh::new()`] should equal to the number of matrices inside [`SkinnedMeshInverseBindposes`]. +/// +/// The content of `joint_transforms` can be modified manually if [`skinned_mesh_update`] system is disabled. +/// +/// # Example +/// ``` +/// use bevy_animation_rig::{SkinnedMesh, SKINNED_MESH_PIPELINE_HANDLE}; +/// use bevy_ecs::{entity::Entity, system::Commands}; +/// use bevy_pbr::prelude::PbrBundle; +/// use bevy_render::pipeline::{RenderPipeline, RenderPipelines}; +/// +/// fn example_system(mut commands: Commands) { +/// commands.spawn_bundle(PbrBundle { +/// render_pipelines: RenderPipelines::from_pipelines( +/// vec![RenderPipeline::new(SKINNED_MESH_PIPELINE_HANDLE.typed())] +/// ), +/// ..Default::default() +/// }).insert(SkinnedMesh::new( +/// // Refer to [`SkinnedMeshInverseBindposes`] example on how to create inverse bindposes data. +/// Default::default(), +/// // Specify joint entities here. +/// vec![Entity::new(0)] +/// )); +/// } +/// ``` +#[derive(Debug, Default, Clone, Reflect)] +#[reflect(Component, MapEntities)] +pub struct SkinnedMesh { + pub inverse_bindposes: Handle, + pub joints: Vec, +} + +impl SkinnedMesh { + pub fn new( + inverse_bindposes: Handle, + joint_entities: impl IntoIterator, + ) -> Self { + Self { + inverse_bindposes, + joints: joint_entities + .into_iter() + .map(|entity| SkinnedMeshJoint { + entity, + transform: Mat4::IDENTITY, + }) + .collect(), + } + } + + pub fn update_joint_transforms( + &mut self, + inverse_bindposes_assets: &Res>, + global_transform_query: &Query<&GlobalTransform>, + ) { + let inverse_bindposes = inverse_bindposes_assets + .get(self.inverse_bindposes.clone()) + .unwrap(); + + for (joint, &inverse_bindpose) in self.joints.iter_mut().zip(inverse_bindposes.0.iter()) { + let global_transform = global_transform_query.get(joint.entity).unwrap(); + joint.transform = global_transform.compute_matrix() * inverse_bindpose; + } + } +} + +impl MapEntities for SkinnedMesh { + fn map_entities(&mut self, entity_map: &EntityMap) -> Result<(), MapEntitiesError> { + for joint in &mut self.joints { + joint.entity = entity_map.get(joint.entity)?; + } + + Ok(()) + } +} + +impl RenderResource for SkinnedMesh { + fn resource_type(&self) -> Option { + Some(RenderResourceType::Buffer) + } + + fn write_buffer_bytes(&self, buffer: &mut [u8]) { + let transform_size = std::mem::size_of::<[f32; 16]>(); + + for (index, joint) in self.joints.iter().enumerate() { + joint.transform.write_buffer_bytes( + &mut buffer[index * transform_size..(index + 1) * transform_size], + ); + } + } + + fn buffer_byte_len(&self) -> Option { + Some(self.joints.len() * std::mem::size_of::<[f32; 16]>()) + } + + fn texture(&self) -> Option<&Handle> { + None + } +} + +impl RenderResources for SkinnedMesh { + fn render_resources_len(&self) -> usize { + 1 + } + + fn get_render_resource(&self, index: usize) -> Option<&dyn RenderResource> { + (index == 0).then(|| self as &dyn RenderResource) + } + + fn get_render_resource_name(&self, index: usize) -> Option<&str> { + (index == 0).then(|| buffer::JOINT_TRANSFORMS) + } + + // Used to tell GLSL to use storage buffer instead of uniform buffer + fn get_render_resource_hints(&self, index: usize) -> Option { + (index == 0).then(|| RenderResourceHints::BUFFER) + } + + fn iter(&self) -> RenderResourceIterator { + RenderResourceIterator::new(self) + } +} + +/// Store data for each joint belongs to the [`SkinnedMesh`] +#[derive(Debug, Clone)] +pub struct SkinnedMeshJoint { + pub entity: Entity, + pub transform: Mat4, +} + +/// Manually implement [`bevy_reflect::Reflect`] for [`SkinnedMeshJoint`] to work around an issue, +/// where spawning a scene with a component containings a vector of structs would result in runtime panic. +unsafe impl Reflect for SkinnedMeshJoint { + #[inline] + fn type_name(&self) -> &str { + std::any::type_name::() + } + + #[inline] + fn any(&self) -> &dyn std::any::Any { + self + } + + #[inline] + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + /// Workaround + #[inline] + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + #[inline] + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } + + #[inline] + fn apply(&mut self, value: &dyn Reflect) { + if let ReflectRef::Struct(struct_value) = value.reflect_ref() { + for (i, value) in struct_value.iter_fields().enumerate() { + let name = struct_value.name_at(i).unwrap(); + + if let Some(v) = self.field_mut(name) { + v.apply(value) + } + } + } else { + panic!("Attempted to apply non-struct type to struct type."); + } + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Struct(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Struct(self) + } + + fn serializable(&self) -> Option { + None + } + + fn reflect_hash(&self) -> Option { + None + } + + fn reflect_partial_eq(&self, _value: &dyn Reflect) -> Option { + None + } +} + +/// Manually implement [`bevy_reflect::Struct`] for [`SkinnedMeshJoint`] because it is required by [`bevy_reflect::Reflect`] trait. +impl Struct for SkinnedMeshJoint { + fn field(&self, name: &str) -> Option<&dyn Reflect> { + match name { + "entity" => Some(&self.entity), + "transform" => Some(&self.transform), + _ => None, + } + } + + fn field_mut(&mut self, name: &str) -> Option<&mut dyn Reflect> { + match name { + "entity" => Some(&mut self.entity), + "transform" => Some(&mut self.transform), + _ => None, + } + } + + fn field_at(&self, index: usize) -> Option<&dyn Reflect> { + match index { + 0 => Some(&self.entity), + 1 => Some(&self.transform), + _ => None, + } + } + + fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + match index { + 0 => Some(&mut self.entity), + 1 => Some(&mut self.transform), + _ => None, + } + } + + fn name_at(&self, index: usize) -> Option<&str> { + match index { + 0 => Some("entity"), + 1 => Some("transform"), + _ => None, + } + } + + fn field_len(&self) -> usize { + 2 + } + + fn iter_fields(&self) -> FieldIter { + FieldIter::new(self) + } + + fn clone_dynamic(&self) -> DynamicStruct { + let mut dynamic = DynamicStruct::default(); + dynamic.set_name(self.type_name().to_string()); + dynamic.insert_boxed("entity", self.entity.clone_value()); + dynamic.insert_boxed("transform", self.transform.clone_value()); + dynamic + } +} + +/// Store joint inverse bindpose matrices. It can be shared between SkinnedMesh instances using assets. +/// +/// The matrices can be loaded automatically from glTF or can be defined manually. +/// +/// # Example +/// ``` +/// use bevy_asset::Assets; +/// use bevy_animation_rig::{SkinnedMesh, SkinnedMeshInverseBindposes, SKINNED_MESH_PIPELINE_HANDLE}; +/// use bevy_ecs::{entity::Entity, system::{Commands, ResMut}}; +/// use bevy_math::Mat4; +/// use bevy_pbr::prelude::PbrBundle; +/// use bevy_render::pipeline::{RenderPipeline, RenderPipelines}; +/// +/// fn example_system(mut commands: Commands, mut skinned_mesh_inverse_bindposes_assets: ResMut>) { +/// // A skeleton with only 2 joints +/// let skinned_mesh_inverse_bindposes = skinned_mesh_inverse_bindposes_assets.add(SkinnedMeshInverseBindposes(vec![ +/// Mat4::IDENTITY, +/// Mat4::IDENTITY, +/// ])); +/// +/// // The inverse bindposes then can be shared between multiple skinned mesh instances +/// for _ in 0..3 { +/// commands.spawn_bundle(PbrBundle { +/// render_pipelines: RenderPipelines::from_pipelines( +/// vec![RenderPipeline::new(SKINNED_MESH_PIPELINE_HANDLE.typed())] +/// ), +/// ..Default::default() +/// }).insert(SkinnedMesh::new( +/// skinned_mesh_inverse_bindposes.clone(), +/// // Remember to assign joint entity here! +/// vec![Entity::new(0); 2], +/// )); +/// } +/// } +/// ``` +#[derive(Debug, TypeUuid)] +#[uuid = "b9f155a9-54ec-4026-988f-e0a03e99a76f"] +pub struct SkinnedMeshInverseBindposes(pub Vec); + +pub fn skinned_mesh_setup( + mut pipelines: ResMut>, + mut shaders: ResMut>, + mut render_graph: ResMut, +) { + let mut skinned_mesh_pipeline = pipelines + .get(render_graph::PBR_PIPELINE_HANDLE) + .unwrap() + .clone(); + skinned_mesh_pipeline.name = Some("Skinned Mesh Pipeline".into()); + skinned_mesh_pipeline.shader_stages.vertex = shaders.add(Shader::from_glsl( + ShaderStage::Vertex, + include_str!("skinned_mesh.vert"), + )); + pipelines.set_untracked(SKINNED_MESH_PIPELINE_HANDLE, skinned_mesh_pipeline); + + render_graph.add_system_node( + node::SKINNED_MESH, + RenderResourcesNode::::new(false), + ); + render_graph + .add_node_edge( + node::SKINNED_MESH, + bevy_render::render_graph::base::node::MAIN_PASS, + ) + .unwrap(); +} + +pub fn skinned_mesh_update( + skinned_mesh_inverse_bindposes_assets: Res>, + global_transform_query: Query<&GlobalTransform>, + mut skinned_mesh_query: Query<&mut SkinnedMesh>, +) { + skinned_mesh_query.for_each_mut(|mut skinned_mesh| { + skinned_mesh.update_joint_transforms( + &skinned_mesh_inverse_bindposes_assets, + &global_transform_query, + ); + }); +} diff --git a/crates/bevy_animation_rig/src/skinned_mesh.vert b/crates/bevy_animation_rig/src/skinned_mesh.vert new file mode 100644 index 0000000000000..a899804396a03 --- /dev/null +++ b/crates/bevy_animation_rig/src/skinned_mesh.vert @@ -0,0 +1,44 @@ +#version 450 + +layout(location = 0) in vec3 Vertex_Position; +layout(location = 1) in vec3 Vertex_Normal; +layout(location = 2) in vec2 Vertex_Uv; +layout(location = 3) in vec4 Vertex_JointWeight; +layout(location = 4) in uvec4 Vertex_JointIndex; + +#ifdef STANDARDMATERIAL_NORMAL_MAP +layout(location = 5) in vec4 Vertex_Tangent; +#endif + +layout(location = 0) out vec3 v_WorldPosition; +layout(location = 1) out vec3 v_WorldNormal; +layout(location = 2) out vec2 v_Uv; + +layout(set = 0, binding = 0) uniform CameraViewProj { + mat4 ViewProj; +}; + +#ifdef STANDARDMATERIAL_NORMAL_MAP +layout(location = 3) out vec4 v_WorldTangent; +#endif + +layout(set = 2, binding = 0) buffer JointTransforms { + mat4[] Joints; +}; + +void main() { + mat4 Model = + Vertex_JointWeight.x * Joints[Vertex_JointIndex.x] + + Vertex_JointWeight.y * Joints[Vertex_JointIndex.y] + + Vertex_JointWeight.z * Joints[Vertex_JointIndex.z] + + Vertex_JointWeight.w * Joints[Vertex_JointIndex.w]; + + vec4 world_position = Model * vec4(Vertex_Position, 1.0); + v_WorldPosition = world_position.xyz; + v_WorldNormal = mat3(Model) * Vertex_Normal; + v_Uv = Vertex_Uv; +#ifdef STANDARDMATERIAL_NORMAL_MAP + v_WorldTangent = vec4(mat3(Model) * Vertex_Tangent.xyz, Vertex_Tangent.w); +#endif + gl_Position = ViewProj * world_position; +} diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 52a0510805714..de67850886601 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -33,7 +33,7 @@ crossbeam-channel = "0.5.0" anyhow = "1.0" thiserror = "1.0" downcast-rs = "1.2.0" -notify = { version = "5.0.0-pre.2", optional = true } +notify = { version = "5.0.0-pre.11", optional = true } parking_lot = "0.11.0" rand = "0.8.0" diff --git a/crates/bevy_asset/src/filesystem_watcher.rs b/crates/bevy_asset/src/filesystem_watcher.rs index 004a2b24bf1ca..19b131780a078 100644 --- a/crates/bevy_asset/src/filesystem_watcher.rs +++ b/crates/bevy_asset/src/filesystem_watcher.rs @@ -12,7 +12,7 @@ pub struct FilesystemWatcher { impl Default for FilesystemWatcher { fn default() -> Self { let (sender, receiver) = crossbeam_channel::unbounded(); - let watcher: RecommendedWatcher = Watcher::new_immediate(move |res| { + let watcher: RecommendedWatcher = RecommendedWatcher::new(move |res| { sender.send(res).expect("Watch event send failure."); }) .expect("Failed to create filesystem watcher."); @@ -22,6 +22,6 @@ impl Default for FilesystemWatcher { impl FilesystemWatcher { pub fn watch>(&mut self, path: P) -> Result<()> { - self.watcher.watch(path, RecursiveMode::Recursive) + self.watcher.watch(path.as_ref(), RecursiveMode::Recursive) } } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 825eab38a459e..4e5d9ee84bfdd 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -119,6 +119,15 @@ impl<'a> LoadContext<'a> { self.get_handle(AssetPath::new_ref(self.path(), Some(label))) } + pub fn get_mut_labeled_asset(&mut self, label: &str) -> Option<&mut T> { + assert!(!label.is_empty()); + + self.labeled_assets + .get_mut(&Some(label.to_string())) + .and_then(|loaded_asset| loaded_asset.value.as_mut()) + .and_then(|dyn_asset| dyn_asset.downcast_mut::()) + } + pub fn get_handle, T: Asset>(&self, id: I) -> Handle { Handle::strong(id.into(), self.ref_change_channel.sender.clone()) } diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 1fa7db10f97bf..00788cb58c09f 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["bevy"] [dependencies] # bevy +bevy_animation_rig = { path = "../bevy_animation_rig", version = "0.5.0" } bevy_app = { path = "../bevy_app", version = "0.5.0" } bevy_asset = { path = "../bevy_asset", version = "0.5.0" } bevy_core = { path = "../bevy_core", version = "0.5.0" } diff --git a/crates/bevy_gltf/src/animation.rs b/crates/bevy_gltf/src/animation.rs new file mode 100644 index 0000000000000..18a3e0537f928 --- /dev/null +++ b/crates/bevy_gltf/src/animation.rs @@ -0,0 +1,212 @@ + +use bevy_asset::{Handle}; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::{Quat, Vec3}; +use gltf::animation::{Interpolation, util::{MorphTargetWeights, ReadOutputs, Rotations}}; + +use super::{Gltf, GltfNode}; +use bevy_reflect::{Reflect, TypeUuid}; + +/// A component for transform nodes loaded by the Gltf loader that indicates this entity is the target of at least one animation in the Gltf asset. +/// +/// One or more animations may target the same node at different property paths. Each unique animation/channel pair that targets this entity receives an entry in the `animations` and `channel_indices` vectors. For example, the animation index at animations\[`1`\] targets a property of this entity through the channel index identified by the value at channel_indices\[`1`\]. +#[derive(Debug, Default, Clone, Reflect)] +#[reflect(Component)] +pub struct GltfAnimTargetInfo { + pub gltf: Handle, + pub animation_indices: Vec, + pub channel_indices: Vec, +} + +/// Contains a collection of animation channels, each of which targets a single glTF-animatable property (position, rotation, scale, morph target weight) and sampling data. +#[derive(Debug, Clone, TypeUuid)] +#[uuid = "cc71ba69-fc20-4665-b399-27da45618653"] +pub struct GltfAnimation { + /// Animations target node properties through channels, each of which contains a target node, property, keyframe times, and per-keyframe values. + pub channels: Vec, + /// The index of this animation in the Gltf. + pub index: usize, + /// The name of this animation. Animations are not guaranteed to have names. + pub name: Option, + pub(crate) start_time: f32, + pub(crate) end_time: f32, +} +impl GltfAnimation { + /// The time in seconds of the earliest keyframe among all channels in this animation. + pub fn start_time(&self) -> f32 { self.start_time } + /// The time in seconds of the latest keyframe among all channels in this animation. + pub fn end_time(&self) -> f32 { self.end_time } + /// The duration in seconds between the earliest and latest keyframes among all channels in this animation. + pub fn duration(&self) -> f32 { self.end_time - self.start_time } +} + +/// Targets a single glTF-animatable property of a glTF node (position, rotation, scale, or morph target weight) and sampling data for converting animation time in seconds to the animated property value. +#[derive(Debug, Clone)] +pub struct GltfAnimChannel { + pub target: GltfAnimTarget, + pub sampler: GltfAnimSampler, + pub index: usize, + pub(crate) start_time: f32, + pub(crate) end_time: f32, +} +impl GltfAnimChannel { + /// The time in seconds of first keyframe in this channel. + pub fn start_time(&self) -> f32 { self.start_time } + /// The time in seconds of last keyframe in this channel. + pub fn end_time(&self) -> f32 { self.end_time } + /// The duration in seconds between the first and last keyframes in this channel. + pub fn duration(&self) -> f32 { self.end_time - self.start_time } +} + +/// Contains a handle to the target GltfNode for animation and the animation path for the node (translation, rotation, scale, or morph target weight). +#[derive(Debug, Clone)] +pub struct GltfAnimTarget { + pub node: Handle, + pub path: GltfAnimTargetProperty +} + +/// As gltf_json::Property. Specifies a property to animate. Valid target properties are position, rotation, scale, or morph target weights. +#[derive(Debug, Clone)] +pub enum GltfAnimTargetProperty { + /// XYZ translation vector. + Position, + + /// XYZW rotation quaternion. + Rotation, + + /// XYZ scale vector. + Scale, + + /// Weights of morph targets. + MorphTargetWeights +} + +impl From for GltfAnimTargetProperty { + fn from(property: gltf::animation::Property) -> Self { + match property { + gltf::animation::Property::Translation => GltfAnimTargetProperty::Position, + gltf::animation::Property::Rotation => GltfAnimTargetProperty::Rotation, + gltf::animation::Property::Scale => GltfAnimTargetProperty::Scale, + gltf::animation::Property::MorphTargetWeights => GltfAnimTargetProperty::MorphTargetWeights, + } + } +} + +#[derive(Debug, Clone)] +pub struct GltfAnimSampler { + pub input: GltfAnimKeyframeTimes, + pub interpolation: GltfAnimInterpolation, + pub output: GltfAnimOutputValues +} + +#[derive(Debug, Clone)] +pub struct GltfAnimKeyframeTimes(pub Vec); + +#[derive(Debug, Clone)] +pub enum GltfAnimInterpolation { + Linear, + Step, + CubicSpline +} + +impl From for GltfAnimInterpolation { + fn from(interp: gltf::animation::Interpolation) -> Self { + match interp { + Interpolation::Linear => GltfAnimInterpolation::Linear, + Interpolation::Step => GltfAnimInterpolation::Step, + Interpolation::CubicSpline => GltfAnimInterpolation::CubicSpline, + } + } +} + +#[derive(Debug, Clone)] +pub enum GltfAnimOutputValues { + Translations(Vec), + Rotations(Vec), + Scales(Vec), + MorphTargetWeights(Vec) +} + +impl GltfAnimOutputValues { + pub fn len(&self) -> usize { + match self { + GltfAnimOutputValues::Translations(vec) => vec.len(), + GltfAnimOutputValues::Rotations(vec) => vec.len(), + GltfAnimOutputValues::Scales(vec) => vec.len(), + GltfAnimOutputValues::MorphTargetWeights(vec) => vec.len(), + } + } +} + +impl From> for GltfAnimOutputValues { + fn from(outputs: ReadOutputs<'_>) -> Self { + match outputs { + ReadOutputs::Translations(translations) => GltfAnimOutputValues::Translations( + translations.map(|xyz| xyz.into()).collect() + ), + ReadOutputs::Rotations(rotations) => GltfAnimOutputValues::Rotations( + match rotations { + // glTF reference for converting non-float encoded types in quaternions to float and back: + // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#animations + // + // The encoding is always XYZW. + Rotations::I8(i8x4s) => i8x4s + .map(|i8x4| i8x4.map(|c| gltf_i8_to_f32(c))) + .map(|xyzw| Quat::from_xyzw(xyzw[0], xyzw[1], xyzw[2], xyzw[3])) + .collect(), + Rotations::U8(u8x4s) => u8x4s + .map(|u8x4| u8x4.map(|c| gltf_u8_to_f32(c))) + .map(|xyzw| Quat::from_xyzw(xyzw[0], xyzw[1], xyzw[2], xyzw[3])) + .collect(), + Rotations::I16(i16x4s) => i16x4s + .map(|i16x4| i16x4.map(|c| gltf_i16_to_f32(c))) + .map(|xyzw| Quat::from_xyzw(xyzw[0], xyzw[1], xyzw[2], xyzw[3])) + .collect(), + Rotations::U16(u16x4s) => u16x4s + .map(|u16x4| u16x4.map(|c| gltf_u16_to_f32(c))) + .map(|xyzw| Quat::from_xyzw(xyzw[0], xyzw[1], xyzw[2], xyzw[3])) + .collect(), + Rotations::F32(f32x4s) => f32x4s + .map(|xyzw| Quat::from_xyzw(xyzw[0], xyzw[1], xyzw[2], xyzw[3])) + .collect(), + } + ), + ReadOutputs::Scales(scales) => GltfAnimOutputValues::Scales( + scales.map(|xyz| xyz.into()).collect() + ), + ReadOutputs::MorphTargetWeights(weights) => GltfAnimOutputValues::MorphTargetWeights( + match weights { + MorphTargetWeights::I8(i8s) => i8s + .map(|i8| gltf_i8_to_f32(i8)) + .collect(), + MorphTargetWeights::U8(u8s) => u8s + .map(|u8| gltf_u8_to_f32(u8)) + .collect(), + MorphTargetWeights::I16(i16s) => i16s + .map(|i16| gltf_i16_to_f32(i16)) + .collect(), + MorphTargetWeights::U16(u16s) => u16s + .map(|u16| gltf_u16_to_f32(u16)) + .collect(), + MorphTargetWeights::F32(f32s) => f32s + .collect(), + } + ), + } + } +} + +/// glTF reference for converting non-float encoded types in quaternions to float and back: +// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#animations + +/// Converts from i8 to f32 according to the glTF specification for animation data values. +pub fn gltf_i8_to_f32(i: i8) -> f32 { (i as f32 / i8::MAX as f32).max(-1.0) } + +/// Converts from u8 to f32 according to the glTF specification for animation data values. +pub fn gltf_u8_to_f32(u: u8) -> f32 { u as f32 / u8::MAX as f32 } + +/// Converts from i16 to f32 according to the glTF specification for animation data values. +pub fn gltf_i16_to_f32(i: i16) -> f32 { (i as f32 / i16::MAX as f32).max(-1.0) } + +/// Converts from u16 to f32 according to the glTF specification for animation data values. +pub fn gltf_u16_to_f32(u: u16) -> f32 { u as f32 / u16::MAX as f32 } diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 591d6c0701356..3d4f0627c75a2 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -3,6 +3,9 @@ use std::collections::HashMap; mod loader; pub use loader::*; +mod animation; +pub use animation::*; + use bevy_app::prelude::*; use bevy_asset::{AddAsset, Handle}; use bevy_pbr::prelude::StandardMaterial; @@ -20,7 +23,9 @@ impl Plugin for GltfPlugin { .add_asset::() .add_asset::() .add_asset::() - .add_asset::(); + .add_asset::() + .add_asset::() + .register_type::(); } } @@ -35,6 +40,8 @@ pub struct Gltf { pub named_materials: HashMap>, pub nodes: Vec>, pub named_nodes: HashMap>, + pub animations: Vec>, + pub named_animations: HashMap>, pub default_scene: Option>, } diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index efc41b6378333..854783b7e6542 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -1,18 +1,22 @@ use anyhow::Result; +use bevy_animation_rig::{SkinnedMesh, SkinnedMeshInverseBindposes, SKINNED_MESH_PIPELINE_HANDLE}; use bevy_asset::{ AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset, }; use bevy_core::Name; -use bevy_ecs::world::World; +use bevy_ecs::{entity::Entity, world::World}; use bevy_log::warn; use bevy_math::Mat4; -use bevy_pbr::prelude::{PbrBundle, StandardMaterial}; +use bevy_pbr::{ + prelude::{PbrBundle, StandardMaterial}, + render_graph::PBR_PIPELINE_HANDLE, +}; use bevy_render::{ camera::{ Camera, CameraProjection, OrthographicProjection, PerspectiveProjection, VisibleEntities, }, mesh::{Indices, Mesh, VertexAttributeValues}, - pipeline::PrimitiveTopology, + pipeline::{PrimitiveTopology, RenderPipeline, RenderPipelines}, prelude::{Color, Texture}, render_graph::base, texture::{AddressMode, FilterMode, ImageType, SamplerDescriptor, TextureError, TextureFormat}, @@ -33,7 +37,7 @@ use std::{ }; use thiserror::Error; -use crate::{Gltf, GltfNode}; +use crate::{Gltf, animation::*, GltfNode}; /// An error that occurs when loading a GLTF file #[derive(Error, Debug)] @@ -155,6 +159,20 @@ async fn load_gltf<'a, 'b>( mesh.set_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute); } + if let Some(vertex_attribute) = reader + .read_joints(0) + .map(|v| VertexAttributeValues::Uint16x4(v.into_u16().collect())) + { + mesh.set_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.set_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, vertex_attribute); + } + if let Some(indices) = reader.read_indices() { mesh.set_indices(Some(Indices::U32(indices.into_u32().collect()))); }; @@ -228,6 +246,11 @@ async fn load_gltf<'a, 'b>( named_nodes_intermediate.insert(name, node.index()); } } + // Referenced for animation importer. + let nodes_raw = resolve_node_hierarchy(nodes_intermediate.clone()) + .into_iter() + .map(|(_, node)| node.clone()) + .collect::>(); let nodes = resolve_node_hierarchy(nodes_intermediate) .into_iter() .map(|(label, node)| load_context.set_labeled_asset(&label, LoadedAsset::new(node))) @@ -273,17 +296,147 @@ async fn load_gltf<'a, 'b>( load_context.set_labeled_asset(&label, LoadedAsset::new(texture)); }); + 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 = 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(inverse_bindposes)), + ) + }) + .collect(); + + // Load GltfAnimations and track targeted nodes. + // + // Later, when scenes are spawned, node entities targeted by animations will receive GltfAnimTargetInfo components. + let mut animations = vec![]; + let mut named_animations = HashMap::new(); + let mut animation_channel_targets = HashMap::new(); + let mut anim_target_info_map = HashMap::new(); + let mut anim_target_entity_map = HashMap::new(); + let mut rest_poses = HashMap::new(); + for (anim_idx, gltf_animation) in gltf.animations().enumerate() { + let mut anim_channels = vec![]; + let mut earliest_keyframe_time = None; + let mut latest_keyframe_time = None; + for (chan_idx, gltf_channel) in gltf_animation.channels().enumerate() { + let target_gltf_node = gltf_channel.target().node(); + + // Make sure we assign a rest pose for this node. This is identical no matter the animation/channel, so assigning this here is a little wasteful. + rest_poses.insert(target_gltf_node.index(), + nodes_raw[target_gltf_node.index()].transform + ); + + let channel_target = GltfAnimTarget { + node: nodes[target_gltf_node.index()].clone(), + path: gltf_channel.target().property().into() + }; + + // Track animation targets for use later when spawning nodes. + animation_channel_targets.insert((anim_idx, chan_idx), target_gltf_node); + + let (sampler, start_time, end_time) = { + let reader = gltf_channel + .reader(|buffer| Some(&buffer_data[buffer.index()])); + let input_keyframe_times: Vec = reader + .read_inputs() + .unwrap() + .collect(); + let (start_time, end_time) = ( + *input_keyframe_times.first().unwrap(), + *input_keyframe_times.last().unwrap() + ); + let output_values: GltfAnimOutputValues = reader + .read_outputs() + .unwrap() + .into(); + + ( + GltfAnimSampler { + input: GltfAnimKeyframeTimes(input_keyframe_times), + interpolation: gltf_channel.sampler().interpolation().into(), + output: output_values, + }, + start_time, + end_time + ) + }; + + earliest_keyframe_time = Some(earliest_keyframe_time.unwrap_or(start_time).min(start_time)); + latest_keyframe_time = Some(latest_keyframe_time.unwrap_or(end_time).max(end_time)); + + anim_channels.push(GltfAnimChannel { + target: channel_target, + sampler: sampler, + index: chan_idx, + start_time: start_time, + end_time: end_time + }); + } + let animation = GltfAnimation { + channels: anim_channels, + index: anim_idx, + name: gltf_animation.name().and_then(|s| Some(s.to_string())), + start_time: earliest_keyframe_time.unwrap(), + end_time: latest_keyframe_time.unwrap(), + }; + + let handle = load_context.set_labeled_asset( + &animation_label(&gltf_animation), + LoadedAsset::new(animation) + ); + if let Some(animation_name) = gltf_animation.name() { + named_animations.insert(animation_name.to_string(), handle.clone()); + } + animations.push(handle); + } + + // For animation, invert the channel targets map to map from each Gltf target node index to every animation-channel pair that targets that node. + // TODO: Isn't this just two steps that should be one step + for ((anim_idx, channel_idx), target_gltf_node) in &animation_channel_targets { + // GltfAnimTargetInfo wants a Gltf, but it doesn't exist yet. + // Instead we just track the animation and channel indices and we'll + // spawn GltfAnimTargetInfo after the Gltf handle + let mut target_infos = anim_target_info_map + .remove(&target_gltf_node.index()) + .unwrap_or(vec![]); + target_infos.push((*anim_idx, *channel_idx)); + anim_target_info_map.insert(target_gltf_node.index(), target_infos); + } + let mut scenes = vec![]; let mut named_scenes = HashMap::new(); + let mut loaded_scene_labels = vec![]; 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(); + let scene_idx = scene.index(); + world .spawn() .insert_bundle((Transform::identity(), GlobalTransform::identity())) .with_children(|parent| { for node in scene.nodes() { - let result = load_node(&node, parent, load_context, &buffer_data); + let result = load_node( + &node, + parent, + load_context, + &buffer_data, + &mut node_index_to_entity_map, + &mut entity_to_skin_index_map, + &mut anim_target_info_map, + scene_idx, + &mut anim_target_entity_map, + ); if result.is_err() { err = Some(result); return; @@ -293,13 +446,33 @@ 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::new( + skinned_mesh_inverse_bindposes[skin_index].clone(), + joint_entities, + )); + } + + let loaded_scene_label = scene_label(&scene); + let loaded_scene = Scene::new(world); let scene_handle = load_context - .set_labeled_asset(&scene_label(&scene), LoadedAsset::new(Scene::new(world))); + .set_labeled_asset(&loaded_scene_label, LoadedAsset::new(loaded_scene)); if let Some(name) = scene.name() { named_scenes.insert(name.to_string(), scene_handle.clone()); } scenes.push(scene_handle); + + // Store scene references for animation support after gltf_handle is created. + loaded_scene_labels.push(loaded_scene_label.clone()); } load_context.set_default_asset(LoadedAsset::new(Gltf { @@ -315,7 +488,33 @@ async fn load_gltf<'a, 'b>( named_materials, nodes, named_nodes, + animations, + named_animations })); + let gltf_handle = load_context.get_handle(AssetPath::new_ref(load_context.path(), None)); + + // Animation hack: Now that the gltf_handle exists, use the channel and index data to modify the spawned scenes' animation targets with GltfAnimTargetInfo components. + for (node_entity_id, (scene_idx, target_details)) in anim_target_entity_map { + for (anim_idx, chan_idx) in target_details { + let scene_label = &loaded_scene_labels[scene_idx]; + // TODO: get_mut_labeled_asset is another change in the asset loader that wigs me out a little bit. Is there a better option? + let mut_scene: Option<&mut Scene> = load_context.get_mut_labeled_asset(scene_label.as_str()); + let mut_scene = mut_scene.unwrap(); + let mut target_entity = mut_scene.world.entity_mut(node_entity_id); + + let mut target_info = target_entity.get_mut::(); + if target_info.is_none() { + target_entity.insert(GltfAnimTargetInfo { + gltf: gltf_handle.clone(), + animation_indices: vec![anim_idx], + channel_indices: vec![chan_idx], + }); + } else { + target_info.as_mut().unwrap().animation_indices.push(anim_idx); + target_info.as_mut().unwrap().channel_indices.push(chan_idx); + } + } + } Ok(()) } @@ -447,6 +646,11 @@ fn load_node( world_builder: &mut WorldChildBuilder, load_context: &mut LoadContext, buffer_data: &[Vec], + node_index_to_entity_map: &mut HashMap, + entity_to_skin_index_map: &mut HashMap, + anim_target_map: &mut HashMap>, + scene_idx: usize, + spawned_scene_anim_channel_map: &mut HashMap)>, ) -> Result<(), GltfError> { let transform = gltf_node.transform(); let mut gltf_error = None; @@ -508,6 +712,21 @@ fn load_node( } } + // Map node index to entity + node_index_to_entity_map.insert(gltf_node.index(), node.id()); + + // Queue adding animation target info to entity + // let foo = anim_target_map.get(&gltf_node.index()); + if let Some(target_details) = anim_target_map.get(&gltf_node.index()) { + for anim_channel in target_details { + // let mut info = anim_target_info.clone(); + let (scene_idx, mut target_details) = spawned_scene_anim_channel_map.remove(&node.id()) + .unwrap_or((scene_idx, Vec::new())); + target_details.push(*anim_channel); + spawned_scene_anim_channel_map.insert(node.id(), (scene_idx, target_details)); + } + } + node.with_children(|parent| { if let Some(mesh) = gltf_node.mesh() { // append primitives @@ -522,23 +741,47 @@ fn load_node( load_material(&material, load_context); } + let mut node = parent.spawn(); + + let mut pipeline = PBR_PIPELINE_HANDLE.typed(); + + // Mark for adding skinned mesh + if let Some(skin) = gltf_node.skin() { + entity_to_skin_index_map.insert(node.id(), skin.index()); + pipeline = SKINNED_MESH_PIPELINE_HANDLE.typed(); + } + let primitive_label = primitive_label(&mesh, &primitive); 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)); - parent.spawn_bundle(PbrBundle { + node.insert_bundle(PbrBundle { mesh: load_context.get_handle(mesh_asset_path), material: load_context.get_handle(material_asset_path), + render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::new( + pipeline, + )]), ..Default::default() }); + node.insert(Name::new("PBR Renderer")); } } // append other nodes for child in gltf_node.children() { - if let Err(err) = load_node(&child, parent, load_context, buffer_data) { + if let Err(err) = load_node( + &child, + parent, + load_context, + buffer_data, + node_index_to_entity_map, + entity_to_skin_index_map, + anim_target_map, + scene_idx, + spawned_scene_anim_channel_map, + ) { gltf_error = Some(err); return; } @@ -579,6 +822,14 @@ fn scene_label(scene: &gltf::Scene) -> String { format!("Scene{}", scene.index()) } +fn skin_label(skin: &gltf::Skin) -> String { + format!("Skin{}", skin.index()) +} + +fn animation_label(animation: &gltf::Animation) -> String { + format!("Animation{}", animation.index()) +} + fn texture_sampler(texture: &gltf::Texture) -> SamplerDescriptor { let gltf_sampler = texture.sampler(); @@ -647,6 +898,7 @@ async fn load_buffers( load_context: &LoadContext<'_>, asset_path: &Path, ) -> Result>, GltfError> { + const GLTF_BUFFER_URI: &str = "application/gltf-buffer"; const OCTET_STREAM_URI: &str = "application/octet-stream"; let mut buffer_data = Vec::new(); @@ -658,6 +910,7 @@ async fn load_buffers( .unwrap(); let uri = uri.as_ref(); let buffer_bytes = match DataUri::parse(uri) { + Ok(data_uri) if data_uri.mime_type == GLTF_BUFFER_URI => data_uri.decode()?, Ok(data_uri) if data_uri.mime_type == OCTET_STREAM_URI => data_uri.decode()?, Ok(_) => return Err(GltfError::BufferFormatUnsupported), Err(()) => { diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9d557aff5d167..295a42c6c4e0c 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -62,6 +62,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.5.0" } bevy_window = { path = "../bevy_window", version = "0.5.0" } bevy_tasks = { path = "../bevy_tasks", version = "0.5.0" } # bevy (optional) +bevy_animation_rig = { path = "../bevy_animation_rig", optional = true, version = "0.5.0" } bevy_audio = { path = "../bevy_audio", optional = true, version = "0.5.0" } bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.5.0" } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.5.0" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index f42f45c477a14..543a377784151 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -62,6 +62,9 @@ impl PluginGroup for DefaultPlugins { group.add(AssetPlugin::default()); group.add(ScenePlugin::default()); + #[cfg(feature = "bevy_animation_rig")] + group.add(bevy_animation_rig::AnimationRigPlugin::default()); + #[cfg(feature = "bevy_render")] group.add(RenderPlugin::default()); diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 62fb7cf270b56..9b082f148831e 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -76,6 +76,12 @@ pub mod window { pub use bevy_window::*; } +#[cfg(feature = "bevy_animation_rig")] +pub mod animation_rig { + //! Skinned mesh rendering. + pub use bevy_animation_rig::*; +} + #[cfg(feature = "bevy_audio")] pub mod audio { //! Provides types and plugins for audio playback. diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 9362aea02f108..77ae3664893dd 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -7,6 +7,10 @@ pub use crate::{ pub use bevy_derive::bevy_main; +#[doc(hidden)] +#[cfg(feature = "bevy_animation_rig")] +pub use crate::animation_rig::*; + #[doc(hidden)] #[cfg(feature = "bevy_audio")] pub use crate::audio::prelude::*; diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 7dc65b9576167..2eeb887239901 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -5,6 +5,7 @@ |feature name|description| |-|-| |bevy_audio|Audio support. Support for all audio formats depends on this.| +|bevy_animation_rig|Skinned mesh support.| |bevy_dynamic_plugins|Plugins for dynamic loading (libloading).| |bevy_gilrs|Adds gamepad support.| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support.| diff --git a/examples/README.md b/examples/README.md index e3b8878c136f0..47cda66967e22 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) @@ -108,6 +109,13 @@ Example | File | Description `wireframe` | [`3d/wireframe.rs`](./3d/wireframe.rs) | Showcases wireframe rendering `z_sort_debug` | [`3d/z_sort_debug.rs`](./3d/z_sort_debug.rs) | Visualizes camera Z-ordering +## 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..3621a27c07449 --- /dev/null +++ b/examples/animation/custom_skinned_mesh.rs @@ -0,0 +1,161 @@ +use std::f32::consts::PI; + +use bevy::{ + pbr::AmbientLight, + prelude::*, + render::{ + mesh::Indices, + pipeline::{PrimitiveTopology, RenderPipeline}, + }, +}; + +/// Skinned mesh example with mesh and joints data defined in code. +/// Example taken from https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_019_SimpleSkin.md +fn main() { + App::build() + .add_plugins(DefaultPlugins) + .insert_resource(AmbientLight { + brightness: 1.0, + ..Default::default() + }) + .add_startup_system(setup.system()) + .add_system(joint_animation.system()) + .run(); +} + +/// Used to mark a joint to be animated in the [`joint_animation`] system. +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 + let mut camera = OrthographicCameraBundle::new_2d(); + camera.orthographic_projection.near = -1.0; + camera.orthographic_projection.far = 1.0; + camera.orthographic_projection.scale = 0.005; + camera.transform = Transform::from_xyz(0.0, 1.0, 0.0); + commands.spawn_bundle(camera); + + // Create inverse bindpose matrices for a skeleton consists of 2 joints + let inverse_bindposes = + skinned_mesh_inverse_bindposes_assets.add(SkinnedMeshInverseBindposes(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.set_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.set_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.set_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.set_attribute( + Mesh::ATTRIBUTE_JOINT_INDEX, + vec![ + [0, 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.set_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, + ]))); + + // Create joint entities + let joint_0 = commands + .spawn_bundle(( + Transform::from_xyz(0.0, 1.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: meshes.add(mesh), + material: materials.add(Color::WHITE.into()), + render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::new( + SKINNED_MESH_PIPELINE_HANDLE.typed(), + )]), + ..Default::default() + }) + .insert(SkinnedMesh::new(inverse_bindposes, joint_entities)); +} + +/// Animate the joint marked with [`AnimatedJoint`] component. +fn joint_animation(time: Res