Mesh Skinning. Attempt #3 (#4238)

# Objective
Load skeletal weights and indices from GLTF files. Animate meshes.

## Solution
 - Load skeletal weights and indices from GLTF files.
 - Added `SkinnedMesh` component and ` SkinnedMeshInverseBindPose` asset
 - Added `extract_skinned_meshes` to extract joint matrices.
 - Added queue phase systems for enqueuing the buffer writes.

Some notes:

 -  This ports part of # #2359 to the current main.
 -  This generates new `BufferVec`s and bind groups every frame. The expectation here is that the number of `Query::get` calls during extract is probably going to be the stronger bottleneck, with up to 256 calls per skinned mesh. Until that is optimized, caching buffers and bind groups is probably a non-concern.
 - Unfortunately, due to the uniform size requirements, this means a 16KB buffer is allocated for every skinned mesh every frame. There's probably a few ways to get around this, but most of them require either compute shaders or storage buffers, which are both incompatible with WebGL2.

Co-authored-by: james7132 <contact@jamessliu.com>
Co-authored-by: François <mockersf@gmail.com>
Co-authored-by: James Liu <contact@jamessliu.com>
This commit is contained in:
James Liu 2022-03-29 18:31:13 +00:00
parent 54d2e86afc
commit 31bd4ecbbc
19 changed files with 779 additions and 55 deletions

View file

@ -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"

View file

@ -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"}}

View file

@ -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<Mat4> = 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<u8>],
animated_nodes: &HashSet<usize>,
node_index_to_entity_map: &mut HashMap<usize, Entity>,
entity_to_skin_index_map: &mut HashMap<Entity, usize>,
) -> 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();

View file

@ -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"

View file

@ -245,11 +245,11 @@ impl<M: SpecializedMaterial> SpecializedMeshPipeline for MaterialPipeline<M> {
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)

View file

@ -12,8 +12,18 @@ var<uniform> view: View;
[[group(1), binding(0)]]
var<uniform> mesh: Mesh;
#ifdef SKINNED
[[group(1), binding(1)]]
var<uniform> joint_matrices: SkinnedMesh;
#import bevy_pbr::skinning
#endif
struct Vertex {
[[location(0)]] position: vec3<f32>;
#ifdef SKINNED
[[location(4)]] joint_indices: vec4<u32>;
[[location(5)]] joint_weights: vec4<f32>;
#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<f32>(vertex.position, 1.0);
out.clip_position = view.view_proj * model * vec4<f32>(vertex.position, 1.0);
return out;
}

View file

@ -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::<MeshPipeline>().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<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
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::<Shader>(),
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,

View file

@ -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::<Mat4>();
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::<MeshUniform>::default());
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<MeshPipeline>()
.init_resource::<SkinnedMeshUniform>()
.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<Mat4>,
}
#[derive(Component)]
pub struct SkinnedMeshJoints {
pub index: u32,
}
impl SkinnedMeshJoints {
#[inline]
pub fn build(
skin: &SkinnedMesh,
inverse_bindposes: &Assets<SkinnedMeshInverseBindposes>,
joints: &Query<&GlobalTransform>,
buffer: &mut Vec<Mat4>,
) -> Option<Self> {
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::<Mat4>() as u32;
self
}
}
pub fn extract_skinned_meshes(
query: Query<(Entity, &ComputedVisibility, &SkinnedMesh)>,
inverse_bindposes: Res<Assets<SkinnedMeshInverseBindposes>>,
joint_query: Query<&GlobalTransform>,
mut commands: Commands,
mut previous_len: Local<usize>,
mut previous_joint_len: Local<usize>,
) {
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,8 +373,7 @@ impl FromWorld for MeshPipeline {
label: Some("mesh_view_layout"),
});
let mesh_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
let mesh_binding = BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
@ -286,9 +382,31 @@ impl FromWorld for MeshPipeline {
min_binding_size: BufferSize::new(MeshUniform::std140_size_static() as u64),
},
count: None,
}],
};
let mesh_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
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<BindGroup>,
}
pub fn queue_mesh_bind_group(
@ -512,19 +644,70 @@ pub fn queue_mesh_bind_group(
mesh_pipeline: Res<MeshPipeline>,
render_device: Res<RenderDevice>,
mesh_uniforms: Res<ComponentUniforms<MeshUniform>>,
skinned_mesh_uniform: Res<SkinnedMeshUniform>,
) {
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<Mat4>,
}
pub fn prepare_skinned_meshes(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
extracted_joints: Res<ExtractedJoints>,
mut skinned_mesh_uniform: ResMut<SkinnedMeshUniform>,
) {
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<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetMeshBindGroup<I> {
type Param = (
SRes<MeshBindGroup>,
SQuery<Read<DynamicUniformIndex<MeshUniform>>>,
SQuery<(
Read<DynamicUniformIndex<MeshUniform>>,
Option<Read<SkinnedMeshJoints>>,
)>,
);
#[inline]
fn render<'w>(
@ -649,12 +835,20 @@ impl<const I: usize> EntityRenderCommand for SetMeshBindGroup<I> {
(mesh_bind_group, mesh_query): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let mesh_index = mesh_query.get(item).unwrap();
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().value,
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
}
}

View file

@ -8,6 +8,10 @@ struct Vertex {
#ifdef VERTEX_TANGENTS
[[location(3)]] tangent: vec4<f32>;
#endif
#ifdef SKINNED
[[location(4)]] joint_indices: vec4<u32>;
[[location(5)]] joint_weights: vec4<f32>;
#endif
};
struct VertexOutput {
@ -22,15 +26,24 @@ struct VertexOutput {
[[group(2), binding(0)]]
var<uniform> mesh: Mesh;
#ifdef SKINNED
[[group(2), binding(1)]]
var<uniform> joint_matrices: SkinnedMesh;
#import bevy_pbr::skinning
#endif
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
let world_position = mesh.model * vec4<f32>(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<f32>(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<f32>(vertex.position, 1.0);
out.world_normal = mat3x3<f32>(
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;
}

View file

@ -7,4 +7,10 @@ struct Mesh {
flags: u32;
};
#ifdef SKINNED
struct SkinnedMesh {
data: array<mat4x4<f32>, 256u>;
};
#endif
let MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u;

View file

@ -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<f32>,
b: mat4x4<f32>,
) -> mat4x4<f32> {
return mat4x4<f32>(
a.x + b.x,
a.y + b.y,
a.z + b.z,
a.w + b.w,
);
}
fn skin_model(
indexes: vec4<u32>,
weights: vec4<f32>,
) -> mat4x4<f32> {
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<f32>) -> mat3x3<f32> {
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<f32>(
x / det,
y / det,
z / det
);
}
fn skin_normals(
model: mat4x4<f32>,
normal: vec3<f32>,
) -> vec3<f32> {
return inverse_transpose_3x3(mat3x3<f32>(
model[0].xyz,
model[1].xyz,
model[2].xyz
)) * normal;
}
fn skin_tangents(
model: mat4x4<f32>,
tangent: vec4<f32>,
) -> vec4<f32> {
return vec4<f32>(
mat3x3<f32>(
model[0].xyz,
model[1].xyz,
model[2].xyz
) * tangent.xyz,
tangent.w
);
}

View file

@ -3,6 +3,10 @@
struct Vertex {
[[location(0)]] position: vec3<f32>;
#ifdef SKINNED
[[location(4)]] joint_indexes: vec4<u32>;
[[location(5)]] joint_weights: vec4<f32>;
#endif
};
[[group(1), binding(0)]]
@ -12,10 +16,21 @@ struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
};
#ifdef SKINNED
[[group(2), binding(0)]]
var<uniform> joint_matrices: SkinnedMesh;
#import bevy_pbr::skinning
#endif
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
let world_position = mesh.model * vec4<f32>(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<f32>(vertex.position, 1.0);
var out: VertexOutput;
out.clip_position = view.view_proj * world_position;

View file

@ -118,6 +118,12 @@ impl From<Vec<[u32; 4]>> for VertexAttributeValues {
}
}
impl From<Vec<[u16; 4]>> for VertexAttributeValues {
fn from(vec: Vec<[u16; 4]>) -> Self {
VertexAttributeValues::Uint16x4(vec)
}
}
impl From<Vec<[u8; 4]>> for VertexAttributeValues {
fn from(vec: Vec<[u8; 4]>) -> Self {
VertexAttributeValues::Unorm8x4(vec)

View file

@ -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

View file

@ -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<SkinnedMeshInverseBindposes>,
pub joints: Vec<Entity>,
}
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<Vec<Mat4>> for SkinnedMeshInverseBindposes {
fn from(value: Vec<Mat4>) -> Self {
Self(value.into_boxed_slice())
}
}
impl Deref for SkinnedMeshInverseBindposes {
type Target = [Mat4];
fn deref(&self) -> &Self::Target {
&*self.0
}
}

View file

@ -15,6 +15,8 @@ pub struct MeshPlugin;
impl Plugin for MeshPlugin {
fn build(&self, app: &mut App) {
app.add_asset::<Mesh>()
.add_asset::<skinning::SkinnedMeshInverseBindposes>()
.register_type::<skinning::SkinnedMesh>()
.add_plugin(RenderAssetPlugin::<Mesh>::default());
}
}

View file

@ -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

View file

@ -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 <https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_019_SimpleSkin.md>
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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut skinned_mesh_inverse_bindposes_assets: ResMut<Assets<SkinnedMeshInverseBindposes>>,
) {
// 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<Time>, mut query: Query<&mut Transform, With<AnimatedJoint>>) {
for mut transform in query.iter_mut() {
transform.rotation = Quat::from_axis_angle(
Vec3::Z,
0.5 * PI * time.time_since_startup().as_secs_f32().sin(),
);
}
}

View file

@ -0,0 +1,70 @@
use std::f32::consts::PI;
use bevy::{pbr::AmbientLight, prelude::*, render::mesh::skinning::SkinnedMesh};
/// Skinned mesh example with mesh and joints data loaded from a glTF file.
/// Example taken from <https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_019_SimpleSkin.md>
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(AmbientLight {
brightness: 1.0,
..Default::default()
})
.add_startup_system(setup)
.add_system(joint_animation)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Create a camera
commands.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
// Spawn the first scene in `models/SimpleSkin/SimpleSkin.gltf`
commands.spawn_scene(asset_server.load::<Scene, _>("models/SimpleSkin/SimpleSkin.gltf#Scene0"));
}
/// The scene hierachy currently looks somewhat like this:
///
/// ```ignore
/// <Parent entity>
/// + Mesh node (without `PbrBundle` or `SkinnedMesh` component)
/// + Skinned mesh entity (with `PbrBundle` and `SkinnedMesh` component, created by glTF loader)
/// + First joint
/// + Second joint
/// ```
///
/// In this example, we want to get and animate the second joint.
/// It is similar to the animation defined in `models/SimpleSkin/SimpleSkin.gltf`.
fn joint_animation(
time: Res<Time>,
parent_query: Query<&Parent, With<SkinnedMesh>>,
children_query: Query<&Children>,
mut transform_query: Query<&mut Transform>,
) {
// Iter skinned mesh entity
for skinned_mesh_parent in parent_query.iter() {
// Mesh node is the parent of the skinned mesh entity.
let mesh_node_entity = skinned_mesh_parent.0;
// Get `Children` in the mesh node.
let mesh_node_children = children_query.get(mesh_node_entity).unwrap();
// First joint is the second child of the mesh node.
let first_joint_entity = mesh_node_children[1];
// Get `Children` in the first joint.
let first_joint_children = children_query.get(first_joint_entity).unwrap();
// Second joint is the first child of the first joint.
let second_joint_entity = first_joint_children[0];
// Get `Transform` in the second joint.
let mut second_joint_transform = transform_query.get_mut(second_joint_entity).unwrap();
second_joint_transform.rotation = Quat::from_axis_angle(
Vec3::Z,
0.5 * PI * time.time_since_startup().as_secs_f32().sin(),
);
}
}