mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
# 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:
parent
54d2e86afc
commit
31bd4ecbbc
19 changed files with 779 additions and 55 deletions
|
@ -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"
|
||||
|
|
1
assets/models/SimpleSkin/SimpleSkin.gltf
Normal file
1
assets/models/SimpleSkin/SimpleSkin.gltf
Normal 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"}}
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
66
crates/bevy_pbr/src/render/skinning.wgsl
Normal file
66
crates/bevy_pbr/src/render/skinning.wgsl
Normal 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
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
44
crates/bevy_render/src/mesh/mesh/skinning.rs
Normal file
44
crates/bevy_render/src/mesh/mesh/skinning.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
171
examples/animation/custom_skinned_mesh.rs
Normal file
171
examples/animation/custom_skinned_mesh.rs
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
70
examples/animation/gltf_skinned_mesh.rs
Normal file
70
examples/animation/gltf_skinned_mesh.rs
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue