Skip to content

Commit 28c6b74

Browse files
committed
Implement mesh skinning
1 parent 00d8d5d commit 28c6b74

File tree

16 files changed

+668
-6
lines changed

16 files changed

+668
-6
lines changed

Cargo.toml

+11
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"
+1
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

+24
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

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

+1
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)