Skip to content

Commit 3cd5ad1

Browse files
committed
Implement mesh skinning
1 parent 00d8d5d commit 3cd5ad1

File tree

16 files changed

+679
-6
lines changed

16 files changed

+679
-6
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ members = ["crates/*", "examples/ios", "tools/ci"]
2121

2222
[features]
2323
default = [
24+
"bevy_animation_rig",
2425
"bevy_audio",
2526
"bevy_dynamic_plugin",
2627
"bevy_gilrs",
@@ -41,6 +42,7 @@ dynamic = ["bevy_dylib"]
4142
render = ["bevy_internal/bevy_pbr", "bevy_internal/bevy_render", "bevy_internal/bevy_sprite", "bevy_internal/bevy_text", "bevy_internal/bevy_ui"]
4243

4344
# Optional bevy crates
45+
bevy_animation_rig = ["bevy_internal/bevy_animation_rig"]
4446
bevy_audio = ["bevy_internal/bevy_audio"]
4547
bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"]
4648
bevy_gilrs = ["bevy_internal/bevy_gilrs"]
@@ -176,6 +178,15 @@ path = "examples/3d/wireframe.rs"
176178
name = "z_sort_debug"
177179
path = "examples/3d/z_sort_debug.rs"
178180

181+
# Animation
182+
[[example]]
183+
name = "custom_skinned_mesh"
184+
path = "examples/animation/custom_skinned_mesh.rs"
185+
186+
[[example]]
187+
name = "gltf_skinned_mesh"
188+
path = "examples/animation/gltf_skinned_mesh.rs"
189+
179190
# Application
180191
[[example]]
181192
name = "custom_loop"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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"}}

crates/bevy_animation_rig/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "bevy_animation_rig"
3+
version = "0.5.0"
4+
edition = "2018"
5+
authors = [
6+
"Bevy Contributors <[email protected]>",
7+
"Carter Anderson <[email protected]>",
8+
]
9+
description = "Bevy Engine Animation Rigging System"
10+
homepage = "https://bevyengine.org"
11+
repository = "https://github.com/bevyengine/bevy"
12+
license = "MIT"
13+
keywords = ["bevy", "animation", "rig", "skeleton"]
14+
15+
[dependencies]
16+
# bevy
17+
bevy_app = { path = "../bevy_app", version = "0.5.0" }
18+
bevy_asset = { path = "../bevy_asset", version = "0.5.0" }
19+
bevy_ecs = { path = "../bevy_ecs", version = "0.5.0" }
20+
bevy_math = { path = "../bevy_math", version = "0.5.0" }
21+
bevy_pbr = { path = "../bevy_pbr", version = "0.5.0" }
22+
bevy_reflect = { path = "../bevy_reflect", version = "0.5.0", features = ["bevy"] }
23+
bevy_render = { path = "../bevy_render", version = "0.5.0" }
24+
bevy_transform = { path = "../bevy_transform", version = "0.5.0" }

crates/bevy_animation_rig/src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use bevy_app::{AppBuilder, CoreStage, Plugin, StartupStage};
2+
use bevy_asset::AddAsset;
3+
use bevy_ecs::{
4+
schedule::{ParallelSystemDescriptorCoercion, SystemLabel},
5+
system::IntoSystem,
6+
};
7+
use bevy_transform::TransformSystem;
8+
9+
mod skinned_mesh;
10+
pub use skinned_mesh::*;
11+
12+
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
13+
pub enum AnimationRigSystem {
14+
SkinnedMeshSetup,
15+
SkinnedMeshUpdate,
16+
}
17+
18+
#[derive(Default)]
19+
pub struct AnimationRigPlugin;
20+
21+
impl Plugin for AnimationRigPlugin {
22+
fn build(&self, app: &mut AppBuilder) {
23+
app.register_type::<SkinnedMesh>()
24+
.add_asset::<SkinnedMeshInverseBindposes>()
25+
.add_startup_system_to_stage(
26+
StartupStage::PreStartup,
27+
skinned_mesh_setup
28+
.system()
29+
.label(AnimationRigSystem::SkinnedMeshSetup),
30+
)
31+
.add_system_to_stage(
32+
CoreStage::PostUpdate,
33+
skinned_mesh_update
34+
.system()
35+
.label(AnimationRigSystem::SkinnedMeshUpdate)
36+
.after(TransformSystem::TransformPropagate),
37+
);
38+
}
39+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
use bevy_asset::{Assets, Handle, HandleUntyped};
2+
use bevy_ecs::{
3+
entity::{Entity, EntityMap, MapEntities, MapEntitiesError},
4+
reflect::{ReflectComponent, ReflectMapEntities},
5+
system::{Query, Res, ResMut},
6+
};
7+
use bevy_math::Mat4;
8+
use bevy_pbr::render_graph;
9+
use bevy_reflect::{Reflect, TypeUuid};
10+
use bevy_render::{
11+
pipeline::PipelineDescriptor,
12+
render_graph::{RenderGraph, RenderResourcesNode},
13+
renderer::{
14+
RenderResource, RenderResourceHints, RenderResourceIterator, RenderResourceType,
15+
RenderResources,
16+
},
17+
shader::{Shader, ShaderStage},
18+
texture::Texture,
19+
};
20+
use bevy_transform::components::GlobalTransform;
21+
22+
/// The name of skinned mesh node
23+
pub mod node {
24+
pub const SKINNED_MESH: &str = "skinned_mesh";
25+
}
26+
27+
/// The name of skinned mesh buffer
28+
pub mod buffer {
29+
pub const JOINT_TRANSFORMS: &str = "JointTransforms";
30+
}
31+
32+
/// Specify RenderPipelines with this handle to render the skinned mesh.
33+
pub const SKINNED_MESH_PIPELINE_HANDLE: HandleUntyped =
34+
HandleUntyped::weak_from_u64(PipelineDescriptor::TYPE_UUID, 0x14db1922328e7fcc);
35+
36+
/// Used to update and bind joint transforms to the skinned mesh render pipeline specified with [`SKINNED_MESH_PIPELINE_HANDLE`].
37+
///
38+
/// The length of `joint_entities` and `joint_transforms` should equal to the number of matrices inside [`SkinnedMeshInverseBindposes`].
39+
///
40+
/// The content of `joint_transforms` can be modified manually if [`skinned_mesh_update`] system is disabled.
41+
///
42+
/// # Example
43+
/// ```
44+
/// use bevy_animation_rig::{SkinnedMesh, SKINNED_MESH_PIPELINE_HANDLE};
45+
/// use bevy_ecs::{entity::Entity, system::Commands};
46+
/// use bevy_pbr::prelude::PbrBundle;
47+
/// use bevy_render::pipeline::{RenderPipeline, RenderPipelines};
48+
///
49+
/// fn example_system(mut commands: Commands) {
50+
/// commands.spawn_bundle(PbrBundle {
51+
/// render_pipelines: RenderPipelines::from_pipelines(
52+
/// vec![RenderPipeline::new(SKINNED_MESH_PIPELINE_HANDLE.typed())]
53+
/// ),
54+
/// ..Default::default()
55+
/// }).insert(SkinnedMesh::new(
56+
/// // Refer to [`SkinnedMeshInverseBindposes`] example on how to create inverse bindposes data.
57+
/// Default::default(),
58+
/// // Specify joint entities here.
59+
/// vec![Entity::new(0)]
60+
/// ));
61+
/// }
62+
/// ```
63+
#[derive(Debug, Default, Clone, Reflect)]
64+
#[reflect(Component, MapEntities)]
65+
pub struct SkinnedMesh {
66+
pub inverse_bindposes: Handle<SkinnedMeshInverseBindposes>,
67+
pub joint_entities: Vec<Entity>,
68+
pub joint_transforms: Vec<Mat4>,
69+
}
70+
71+
impl SkinnedMesh {
72+
pub fn new(
73+
inverse_bindposes: Handle<SkinnedMeshInverseBindposes>,
74+
joint_entities: Vec<Entity>,
75+
) -> Self {
76+
let entities_count = joint_entities.len();
77+
78+
Self {
79+
inverse_bindposes,
80+
joint_entities,
81+
joint_transforms: vec![Mat4::IDENTITY; entities_count],
82+
}
83+
}
84+
85+
pub fn update_joint_transforms(
86+
&mut self,
87+
inverse_bindposes_assets: &Res<Assets<SkinnedMeshInverseBindposes>>,
88+
global_transform_query: &Query<&GlobalTransform>,
89+
) {
90+
let inverse_bindposes = inverse_bindposes_assets
91+
.get(self.inverse_bindposes.clone())
92+
.unwrap();
93+
94+
for (joint_transform, (&joint_entity, &inverse_bindpose)) in self
95+
.joint_transforms
96+
.iter_mut()
97+
.zip(self.joint_entities.iter().zip(inverse_bindposes.0.iter()))
98+
{
99+
let global_transform = global_transform_query.get(joint_entity).unwrap();
100+
*joint_transform = global_transform.compute_matrix() * inverse_bindpose;
101+
}
102+
}
103+
}
104+
105+
impl MapEntities for SkinnedMesh {
106+
fn map_entities(&mut self, entity_map: &EntityMap) -> Result<(), MapEntitiesError> {
107+
for entity in &mut self.joint_entities {
108+
*entity = entity_map.get(*entity)?;
109+
}
110+
111+
Ok(())
112+
}
113+
}
114+
115+
impl RenderResource for SkinnedMesh {
116+
fn resource_type(&self) -> Option<RenderResourceType> {
117+
Some(RenderResourceType::Buffer)
118+
}
119+
120+
fn write_buffer_bytes(&self, buffer: &mut [u8]) {
121+
let transform_size = std::mem::size_of::<[f32; 16]>();
122+
123+
for (index, transform) in self.joint_transforms.iter().enumerate() {
124+
transform.write_buffer_bytes(
125+
&mut buffer[index * transform_size..(index + 1) * transform_size],
126+
);
127+
}
128+
}
129+
130+
fn buffer_byte_len(&self) -> Option<usize> {
131+
Some(self.joint_transforms.len() * std::mem::size_of::<[f32; 16]>())
132+
}
133+
134+
fn texture(&self) -> Option<&Handle<Texture>> {
135+
None
136+
}
137+
}
138+
139+
impl RenderResources for SkinnedMesh {
140+
fn render_resources_len(&self) -> usize {
141+
1
142+
}
143+
144+
fn get_render_resource(&self, index: usize) -> Option<&dyn RenderResource> {
145+
(index == 0).then(|| self as &dyn RenderResource)
146+
}
147+
148+
fn get_render_resource_name(&self, index: usize) -> Option<&str> {
149+
(index == 0).then(|| buffer::JOINT_TRANSFORMS)
150+
}
151+
152+
// Used to tell GLSL to use storage buffer instead of uniform buffer
153+
fn get_render_resource_hints(&self, index: usize) -> Option<RenderResourceHints> {
154+
(index == 0).then(|| RenderResourceHints::BUFFER)
155+
}
156+
157+
fn iter(&self) -> RenderResourceIterator {
158+
RenderResourceIterator::new(self)
159+
}
160+
}
161+
162+
/// Store joint inverse bindpose matrices. It can be shared between SkinnedMesh instances using assets.
163+
///
164+
/// The matrices can be loaded automatically from glTF or can be defined manually.
165+
///
166+
/// # Example
167+
/// ```
168+
/// use bevy_asset::Assets;
169+
/// use bevy_animation_rig::{SkinnedMesh, SkinnedMeshInverseBindposes, SKINNED_MESH_PIPELINE_HANDLE};
170+
/// use bevy_ecs::{entity::Entity, system::{Commands, ResMut}};
171+
/// use bevy_math::Mat4;
172+
/// use bevy_pbr::prelude::PbrBundle;
173+
/// use bevy_render::pipeline::{RenderPipeline, RenderPipelines};
174+
///
175+
/// fn example_system(mut commands: Commands, mut skinned_mesh_inverse_bindposes_assets: ResMut<Assets<SkinnedMeshInverseBindposes>>) {
176+
/// // A skeleton with only 2 joints
177+
/// let skinned_mesh_inverse_bindposes = skinned_mesh_inverse_bindposes_assets.add(SkinnedMeshInverseBindposes(vec![
178+
/// Mat4::IDENTITY,
179+
/// Mat4::IDENTITY,
180+
/// ]));
181+
///
182+
/// // The inverse bindposes then can be shared between multiple skinned mesh instances
183+
/// for _ in 0..3 {
184+
/// commands.spawn_bundle(PbrBundle {
185+
/// render_pipelines: RenderPipelines::from_pipelines(
186+
/// vec![RenderPipeline::new(SKINNED_MESH_PIPELINE_HANDLE.typed())]
187+
/// ),
188+
/// ..Default::default()
189+
/// }).insert(SkinnedMesh::new(
190+
/// skinned_mesh_inverse_bindposes.clone(),
191+
/// // Remember to assign joint entity here!
192+
/// vec![Entity::new(0); 2],
193+
/// ));
194+
/// }
195+
/// }
196+
/// ```
197+
#[derive(Debug, TypeUuid)]
198+
#[uuid = "b9f155a9-54ec-4026-988f-e0a03e99a76f"]
199+
pub struct SkinnedMeshInverseBindposes(pub Vec<Mat4>);
200+
201+
pub fn skinned_mesh_setup(
202+
mut pipelines: ResMut<Assets<PipelineDescriptor>>,
203+
mut shaders: ResMut<Assets<Shader>>,
204+
mut render_graph: ResMut<RenderGraph>,
205+
) {
206+
let mut skinned_mesh_pipeline = pipelines
207+
.get(render_graph::PBR_PIPELINE_HANDLE)
208+
.unwrap()
209+
.clone();
210+
skinned_mesh_pipeline.name = Some("Skinned Mesh Pipeline".into());
211+
skinned_mesh_pipeline.shader_stages.vertex = shaders.add(Shader::from_glsl(
212+
ShaderStage::Vertex,
213+
include_str!("skinned_mesh.vert"),
214+
));
215+
pipelines.set_untracked(SKINNED_MESH_PIPELINE_HANDLE, skinned_mesh_pipeline);
216+
217+
render_graph.add_system_node(
218+
node::SKINNED_MESH,
219+
RenderResourcesNode::<SkinnedMesh>::new(false),
220+
);
221+
render_graph
222+
.add_node_edge(node::SKINNED_MESH, bevy_render::render_graph::base::node::MAIN_PASS)
223+
.unwrap();
224+
}
225+
226+
pub fn skinned_mesh_update(
227+
skinned_mesh_inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
228+
global_transform_query: Query<&GlobalTransform>,
229+
mut skinned_mesh_query: Query<&mut SkinnedMesh>,
230+
) {
231+
skinned_mesh_query.for_each_mut(|mut skinned_mesh| {
232+
skinned_mesh.update_joint_transforms(
233+
&skinned_mesh_inverse_bindposes_assets,
234+
&global_transform_query,
235+
);
236+
});
237+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#version 450
2+
3+
layout(location = 0) in vec3 Vertex_Position;
4+
layout(location = 1) in vec3 Vertex_Normal;
5+
layout(location = 2) in vec2 Vertex_Uv;
6+
layout(location = 3) in vec4 Vertex_JointWeight;
7+
layout(location = 4) in uvec4 Vertex_JointIndex;
8+
9+
#ifdef STANDARDMATERIAL_NORMAL_MAP
10+
layout(location = 5) in vec4 Vertex_Tangent;
11+
#endif
12+
13+
layout(location = 0) out vec3 v_WorldPosition;
14+
layout(location = 1) out vec3 v_WorldNormal;
15+
layout(location = 2) out vec2 v_Uv;
16+
17+
layout(set = 0, binding = 0) uniform CameraViewProj {
18+
mat4 ViewProj;
19+
};
20+
21+
#ifdef STANDARDMATERIAL_NORMAL_MAP
22+
layout(location = 3) out vec4 v_WorldTangent;
23+
#endif
24+
25+
layout(set = 2, binding = 0) buffer JointTransforms {
26+
mat4[] Joints;
27+
};
28+
29+
void main() {
30+
mat4 Model =
31+
Vertex_JointWeight.x * Joints[Vertex_JointIndex.x] +
32+
Vertex_JointWeight.y * Joints[Vertex_JointIndex.y] +
33+
Vertex_JointWeight.z * Joints[Vertex_JointIndex.z] +
34+
Vertex_JointWeight.w * Joints[Vertex_JointIndex.w];
35+
36+
vec4 world_position = Model * vec4(Vertex_Position, 1.0);
37+
v_WorldPosition = world_position.xyz;
38+
v_WorldNormal = mat3(Model) * Vertex_Normal;
39+
v_Uv = Vertex_Uv;
40+
#ifdef STANDARDMATERIAL_NORMAL_MAP
41+
v_WorldTangent = vec4(mat3(Model) * Vertex_Tangent.xyz, Vertex_Tangent.w);
42+
#endif
43+
gl_Position = ViewProj * world_position;
44+
}

crates/bevy_gltf/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ keywords = ["bevy"]
1414

1515
[dependencies]
1616
# bevy
17+
bevy_animation_rig = { path = "../bevy_animation_rig", version = "0.5.0" }
1718
bevy_app = { path = "../bevy_app", version = "0.5.0" }
1819
bevy_asset = { path = "../bevy_asset", version = "0.5.0" }
1920
bevy_core = { path = "../bevy_core", version = "0.5.0" }

0 commit comments

Comments
 (0)