Add morph targets (#8158)

# Objective

- Add morph targets to `bevy_pbr` (closes #5756) & load them from glTF
- Supersedes #3722
- Fixes #6814

[Morph targets][1] (also known as shape interpolation, shape keys, or
blend shapes) allow animating individual vertices with fine grained
controls. This is typically used for facial expressions. By specifying
multiple poses as vertex offset, and providing a set of weight of each
pose, it is possible to define surprisingly realistic transitions
between poses. Blending between multiple poses also allow composition.
Morph targets are part of the [gltf standard][2] and are a feature of
Unity and Unreal, and babylone.js, it is only natural to implement them
in bevy.

## Solution

This implementation of morph targets uses a 3d texture where each pixel
is a component of an animated attribute. Each layer is a different
target. We use a 2d texture for each target, because the number of
attribute×components×animated vertices is expected to always exceed the
maximum pixel row size limit of webGL2. It copies fairly closely the way
skinning is implemented on the CPU side, while on the GPU side, the
shader morph target implementation is a relatively trivial detail.

We add an optional `morph_texture` to the `Mesh` struct. The
`morph_texture` is built through a method that accepts an iterator over
attribute buffers.

The `MorphWeights` component, user-accessible, controls the blend of
poses used by mesh instances (so that multiple copy of the same mesh may
have different weights), all the weights are uploaded to a uniform
buffer of 256 `f32`. We limit to 16 poses per mesh, and a total of 256
poses.

More literature:
* Old babylone.js implementation (vertex attribute-based):
https://www.eternalcoding.com/dev-log-1-morph-targets/
* Babylone.js implementation (similar to ours):
https://www.youtube.com/watch?v=LBPRmGgU0PE
* GPU gems 3:
https://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-3-directx-10-blend-shapes-breaking-limits
* Development discord thread
https://discord.com/channels/691052431525675048/1083325980615114772


https://user-images.githubusercontent.com/26321040/231181046-3bca2ab2-d4d9-472e-8098-639f1871ce2e.mp4


https://github.com/bevyengine/bevy/assets/26321040/d2a0c544-0ef8-45cf-9f99-8c3792f5a258

## Acknowledgements

* Thanks to `storytold` for sponsoring the feature
* Thanks to `superdump` and `james7132` for guidance and help figuring
out stuff

## Future work

- Handling of less and more attributes (eg: animated uv, animated
arbitrary attributes)
- Dynamic pose allocation (so that zero-weighted poses aren't uploaded
to GPU for example, enables much more total poses)
- Better animation API, see #8357

----

## Changelog

- Add morph targets to bevy meshes
- Support up to 64 poses per mesh of individually up to 116508 vertices,
animation currently strictly limited to the position, normal and tangent
attributes.
	- Load a morph target using `Mesh::set_morph_targets` 
- Add `VisitMorphTargets` and `VisitMorphAttributes` traits to
`bevy_render`, this allows defining morph targets (a fairly complex and
nested data structure) through iterators (ie: single copy instead of
passing around buffers), see documentation of those traits for details
- Add `MorphWeights` component exported by `bevy_render`
- `MorphWeights` control mesh's morph target weights, blending between
various poses defined as morph targets.
- `MorphWeights` are directly inherited by direct children (single level
of hierarchy) of an entity. This allows controlling several mesh
primitives through a unique entity _as per GLTF spec_.
- Add `MorphTargetNames` component, naming each indices of loaded morph
targets.
- Load morph targets weights and buffers in `bevy_gltf` 
- handle morph targets animations in `bevy_animation` (previously, it
was a `warn!` log)
- Add the `MorphStressTest.gltf` asset for morph targets testing, taken
from the glTF samples repo, CC0.
- Add morph target manipulation to `scene_viewer`
- Separate the animation code in `scene_viewer` from the rest of the
code, reducing `#[cfg(feature)]` noise
- Add the `morph_targets.rs` example to show off how to manipulate morph
targets, loading `MorpStressTest.gltf`

## Migration Guide

- (very specialized, unlikely to be touched by 3rd parties)
- `MeshPipeline` now has a single `mesh_layouts` field rather than
separate `mesh_layout` and `skinned_mesh_layout` fields. You should
handle all possible mesh bind group layouts in your implementation
- You should also handle properly the new `MORPH_TARGETS` shader def and
mesh pipeline key. A new function is exposed to make this easier:
`setup_moprh_and_skinning_defs`
- The `MeshBindGroup` is now `MeshBindGroups`, cached bind groups are
now accessed through the `get` method.

[1]: https://en.wikipedia.org/wiki/Morph_target_animation
[2]:
https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets

---------

Co-authored-by: François <mockersf@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Nicola Papale 2023-06-22 22:00:01 +02:00 committed by GitHub
parent bb59509d44
commit c6170d48f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 2722 additions and 255 deletions

View file

@ -21,8 +21,15 @@
* Ground tile from [Kenney's Tower Defense Kit](https://www.kenney.nl/assets/tower-defense-kit) (CC0 1.0 Universal)
* Game icons from [Kenney's Game Icons](https://www.kenney.nl/assets/game-icons) (CC0 1.0 Universal)
* Space ships from [Kenny's Simple Space Kit](https://www.kenney.nl/assets/simple-space) (CC0 1.0 Universal)
* glTF animated fox from [glTF Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox)
* Low poly fox [by PixelMannen](https://opengameart.org/content/fox-and-shiba) (CC0 1.0 Universal)
* Rigging and animation [by @tomkranis on Sketchfab](https://sketchfab.com/models/371dea88d7e04a76af5763f2a36866bc) ([CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/))
* glTF animated fox from [glTF Sample Models][fox]
* Low poly fox [by PixelMannen] (CC0 1.0 Universal)
* Rigging and animation [by @tomkranis on Sketchfab] ([CC-BY 4.0])
* FiraMono by The Mozilla Foundation and Telefonica S.A (SIL Open Font License, Version 1.1: assets/fonts/FiraMono-LICENSE)
* Barycentric from [mk_bary_gltf](https://github.com/komadori/mk_bary_gltf) (MIT OR Apache-2.0)
* `MorphStressTest.gltf`, [MorphStressTest] ([CC-BY 4.0] by Analytical Graphics, Inc, Model and textures by Ed Mackey)
[MorphStressTest]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/MorphStressTest
[fox]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox
[by PixelMannen]: https://opengameart.org/content/fox-and-shiba
[by @tomkranis on Sketchfab]: https://sketchfab.com/models/371dea88d7e04a76af5763f2a36866bc
[CC-BY 4.0]: https://creativecommons.org/licenses/by/4.0/

View file

@ -746,6 +746,16 @@ description = "Plays an animation from a skinned glTF"
category = "Animation"
wasm = true
[[example]]
name = "morph_targets"
path = "examples/animation/morph_targets.rs"
[package.metadata.example.morph_targets]
name = "Morph Targets"
description = "Plays an animation from a glTF file with meshes with morph targets"
category = "Animation"
wasm = true
[[example]]
name = "animated_transform"
path = "examples/animation/animated_transform.rs"

File diff suppressed because one or more lines are too long

View file

@ -15,6 +15,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.11.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.11.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.11.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev", features = ["bevy"] }
bevy_render = { path = "../bevy_render", version = "0.11.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.11.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }

View file

@ -13,6 +13,7 @@ use bevy_ecs::prelude::*;
use bevy_hierarchy::{Children, Parent};
use bevy_math::{Quat, Vec3};
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
use bevy_render::mesh::morph::MorphWeights;
use bevy_time::Time;
use bevy_transform::{prelude::Transform, TransformSystem};
use bevy_utils::{tracing::warn, HashMap};
@ -34,9 +35,18 @@ pub enum Keyframes {
Translation(Vec<Vec3>),
/// Keyframes for scale.
Scale(Vec<Vec3>),
/// Keyframes for morph target weights.
///
/// Note that in `.0`, each contiguous `target_count` values is a single
/// keyframe representing the weight values at given keyframe.
///
/// This follows the [glTF design].
///
/// [glTF design]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations
Weights(Vec<f32>),
}
/// Describes how an attribute of a [`Transform`] should be animated.
/// Describes how an attribute of a [`Transform`] or [`MorphWeights`] should be animated.
///
/// `keyframe_timestamps` and `keyframes` should have the same length.
#[derive(Reflect, FromReflect, Clone, Debug)]
@ -106,6 +116,11 @@ impl AnimationClip {
self.paths.insert(path, idx);
}
}
/// Whether this animation clip can run on entity with given [`Name`].
pub fn compatible_with(&self, name: &Name) -> bool {
self.paths.keys().all(|path| &path.parts[0] == name)
}
}
#[derive(Reflect)]
@ -270,7 +285,7 @@ impl AnimationPlayer {
}
}
fn find_bone(
fn entity_from_path(
root: Entity,
path: &EntityPath,
children: &Query<&Children>,
@ -336,12 +351,14 @@ fn verify_no_ancestor_player(
/// System that will play all animations, using any entity with a [`AnimationPlayer`]
/// and a [`Handle<AnimationClip>`] as an animation root
#[allow(clippy::too_many_arguments)]
pub fn animation_player(
time: Res<Time>,
animations: Res<Assets<AnimationClip>>,
children: Query<&Children>,
names: Query<&Name>,
transforms: Query<&mut Transform>,
morphs: Query<&mut MorphWeights>,
parents: Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>,
) {
@ -356,6 +373,7 @@ pub fn animation_player(
&animations,
&names,
&transforms,
&morphs,
maybe_parent,
&parents,
&children,
@ -371,6 +389,7 @@ fn run_animation_player(
animations: &Assets<AnimationClip>,
names: &Query<&Name>,
transforms: &Query<&mut Transform>,
morphs: &Query<&mut MorphWeights>,
maybe_parent: Option<&Parent>,
parents: &Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
children: &Query<&Children>,
@ -392,6 +411,7 @@ fn run_animation_player(
animations,
names,
transforms,
morphs,
maybe_parent,
parents,
children,
@ -413,6 +433,7 @@ fn run_animation_player(
animations,
names,
transforms,
morphs,
maybe_parent,
parents,
children,
@ -420,6 +441,28 @@ fn run_animation_player(
}
}
/// Update `weights` based on weights in `keyframes` at index `key_index`
/// with a linear interpolation on `key_lerp`.
///
/// # Panics
///
/// When `key_index * target_count` is larger than `keyframes`
///
/// This happens when `keyframes` is not formatted as described in
/// [`Keyframes::Weights`]. A possible cause is [`AnimationClip`] not being
/// meant to be used for the [`MorphWeights`] of the entity it's being applied to.
fn lerp_morph_weights(weights: &mut [f32], key_lerp: f32, keyframes: &[f32], key_index: usize) {
let target_count = weights.len();
let start = target_count * key_index;
let end = target_count * (key_index + 1);
let zipped = weights.iter_mut().zip(&keyframes[start..end]);
for (morph_weight, keyframe) in zipped {
let minus_lerp = 1.0 - key_lerp;
*morph_weight = (*morph_weight * minus_lerp) + (keyframe * key_lerp);
}
}
#[allow(clippy::too_many_arguments)]
fn apply_animation(
weight: f32,
@ -430,6 +473,7 @@ fn apply_animation(
animations: &Assets<AnimationClip>,
names: &Query<&Name>,
transforms: &Query<&mut Transform>,
morphs: &Query<&mut MorphWeights>,
maybe_parent: Option<&Parent>,
parents: &Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
children: &Query<&Children>,
@ -456,7 +500,7 @@ fn apply_animation(
for (path, bone_id) in &animation_clip.paths {
let cached_path = &mut animation.path_cache[*bone_id];
let curves = animation_clip.get_curves(*bone_id).unwrap();
let Some(target) = find_bone(root, path, children, names, cached_path) else { continue };
let Some(target) = entity_from_path(root, path, children, names, cached_path) else { continue };
// SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias
// any of their descendant Transforms.
//
@ -470,6 +514,7 @@ fn apply_animation(
// to run their animation. Any players in the children or descendants will log a warning
// and do nothing.
let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else { continue };
let mut morphs = unsafe { morphs.get_unchecked(target) };
for curve in curves {
// Some curves have only one keyframe used to set a transform
if curve.keyframe_timestamps.len() == 1 {
@ -484,6 +529,11 @@ fn apply_animation(
Keyframes::Scale(keyframes) => {
transform.scale = transform.scale.lerp(keyframes[0], weight);
}
Keyframes::Weights(keyframes) => {
if let Ok(morphs) = &mut morphs {
lerp_morph_weights(morphs.weights_mut(), weight, keyframes, 0);
}
}
}
continue;
}
@ -529,6 +579,11 @@ fn apply_animation(
let result = scale_start.lerp(scale_end, lerp);
transform.scale = transform.scale.lerp(result, weight);
}
Keyframes::Weights(keyframes) => {
if let Ok(morphs) = &mut morphs {
lerp_morph_weights(morphs.weights_mut(), weight, keyframes, step_start);
}
}
}
}
}

View file

@ -131,7 +131,8 @@ pub struct TemporalAntiAliasBundle {
///
/// Cannot be used with [`bevy_render::camera::OrthographicProjection`].
///
/// Currently does not support skinned meshes. There will probably be ghosting artifacts if used with them.
/// Currently does not support skinned meshes and morph targets.
/// There will probably be ghosting artifacts if used with them.
/// Does not work well with alpha-blended meshes as it requires depth writing to determine motion.
///
/// It is very important that correct motion vectors are written for everything on screen.

View file

@ -39,3 +39,5 @@ thiserror = "1.0"
anyhow = "1.0.4"
base64 = "0.13.0"
percent-encoding = "2.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"

View file

@ -1,6 +1,7 @@
use crate::{vertex_attributes::*, Gltf, GltfExtras, GltfNode};
use anyhow::Result;
use bevy_asset::{
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, HandleId, LoadContext, LoadedAsset,
};
use bevy_core::Name;
use bevy_core_pipeline::prelude::Camera3dBundle;
@ -16,6 +17,7 @@ use bevy_render::{
camera::{Camera, OrthographicProjection, PerspectiveProjection, Projection, ScalingMode},
color::Color,
mesh::{
morph::{MeshMorphWeights, MorphAttributes, MorphTargetImage, MorphWeights},
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
Indices, Mesh, MeshVertexAttribute, VertexAttributeValues,
},
@ -28,19 +30,17 @@ use bevy_scene::Scene;
#[cfg(not(target_arch = "wasm32"))]
use bevy_tasks::IoTaskPool;
use bevy_transform::components::Transform;
use bevy_utils::{HashMap, HashSet};
use gltf::{
accessor::Iter,
mesh::{util::ReadIndices, Mode},
texture::{MagFilter, MinFilter, WrappingMode},
Material, Node, Primitive,
};
use serde::Deserialize;
use std::{collections::VecDeque, path::Path};
use thiserror::Error;
use crate::vertex_attributes::*;
use crate::{Gltf, GltfExtras, GltfNode};
/// An error that occurs when loading a glTF file.
#[derive(Error, Debug)]
pub enum GltfError {
@ -64,6 +64,8 @@ pub enum GltfError {
MissingAnimationSampler(usize),
#[error("failed to generate tangents: {0}")]
GenerateTangentsError(#[from] bevy_render::mesh::GenerateTangentsError),
#[error("failed to generate morph targets: {0}")]
MorphTarget(#[from] bevy_render::mesh::morph::MorphBuildError),
}
/// Loads glTF files with all of their data as their corresponding bevy representations.
@ -132,6 +134,8 @@ async fn load_gltf<'a, 'b>(
#[cfg(feature = "bevy_animation")]
let (animations, named_animations, animation_roots) = {
use bevy_animation::Keyframes;
use gltf::animation::util::ReadOutputs;
let mut animations = vec![];
let mut named_animations = HashMap::default();
let mut animation_roots = HashSet::default();
@ -162,20 +166,17 @@ async fn load_gltf<'a, 'b>(
let keyframes = if let Some(outputs) = reader.read_outputs() {
match outputs {
gltf::animation::util::ReadOutputs::Translations(tr) => {
bevy_animation::Keyframes::Translation(tr.map(Vec3::from).collect())
ReadOutputs::Translations(tr) => {
Keyframes::Translation(tr.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::Rotations(rots) => {
bevy_animation::Keyframes::Rotation(
ReadOutputs::Rotations(rots) => Keyframes::Rotation(
rots.into_f32().map(bevy_math::Quat::from_array).collect(),
)
),
ReadOutputs::Scales(scale) => {
Keyframes::Scale(scale.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::Scales(scale) => {
bevy_animation::Keyframes::Scale(scale.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
warn!("Morph animation property not yet supported");
continue;
ReadOutputs::MorphTargetWeights(weights) => {
Keyframes::Weights(weights.into_f32().collect())
}
}
} else {
@ -215,10 +216,10 @@ async fn load_gltf<'a, 'b>(
let mut meshes = vec![];
let mut named_meshes = HashMap::default();
for mesh in gltf.meshes() {
for gltf_mesh in gltf.meshes() {
let mut primitives = vec![];
for primitive in mesh.primitives() {
let primitive_label = primitive_label(&mesh, &primitive);
for primitive in gltf_mesh.primitives() {
let primitive_label = primitive_label(&gltf_mesh, &primitive);
let primitive_topology = get_primitive_topology(primitive.mode())?;
let mut mesh = Mesh::new(primitive_topology);
@ -246,6 +247,29 @@ async fn load_gltf<'a, 'b>(
}));
};
{
let morph_target_reader = reader.read_morph_targets();
if morph_target_reader.len() != 0 {
let morph_targets_label = morph_targets_label(&gltf_mesh, &primitive);
let morph_target_image = MorphTargetImage::new(
morph_target_reader.map(PrimitiveMorphAttributesIter),
mesh.count_vertices(),
)?;
let handle = load_context.set_labeled_asset(
&morph_targets_label,
LoadedAsset::new(morph_target_image.0),
);
mesh.set_morph_targets(handle);
let extras = gltf_mesh.extras().as_ref();
if let Option::<MorphTargetNames>::Some(names) =
extras.and_then(|extras| serde_json::from_str(extras.get()).ok())
{
mesh.set_morph_target_names(names.target_names);
}
}
}
if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none()
&& matches!(mesh.primitive_topology(), PrimitiveTopology::TriangleList)
{
@ -295,13 +319,13 @@ async fn load_gltf<'a, 'b>(
}
let handle = load_context.set_labeled_asset(
&mesh_label(&mesh),
&mesh_label(&gltf_mesh),
LoadedAsset::new(super::GltfMesh {
primitives,
extras: get_gltf_extras(mesh.extras()),
extras: get_gltf_extras(gltf_mesh.extras()),
}),
);
if let Some(name) = mesh.name() {
if let Some(name) = gltf_mesh.name() {
named_meshes.insert(name.to_string(), handle.clone());
}
meshes.push(handle);
@ -731,6 +755,19 @@ fn load_node(
// Map node index to entity
node_index_to_entity_map.insert(gltf_node.index(), node.id());
if let Some(mesh) = gltf_node.mesh() {
if let Some(weights) = mesh.weights() {
let first_mesh = if let Some(primitive) = mesh.primitives().next() {
let primitive_label = primitive_label(&mesh, &primitive);
let path = AssetPath::new_ref(load_context.path(), Some(&primitive_label));
Some(Handle::weak(HandleId::from(path)))
} else {
None
};
node.insert(MorphWeights::new(weights.to_vec(), first_mesh)?);
}
};
node.with_children(|parent| {
if let Some(mesh) = gltf_node.mesh() {
// append primitives
@ -752,27 +789,40 @@ fn load_node(
let material_asset_path =
AssetPath::new_ref(load_context.path(), Some(&material_label));
let mut mesh_entity = parent.spawn(PbrBundle {
let mut primitive_entity = parent.spawn(PbrBundle {
mesh: load_context.get_handle(mesh_asset_path),
material: load_context.get_handle(material_asset_path),
..Default::default()
});
mesh_entity.insert(Aabb::from_min_max(
let target_count = primitive.morph_targets().len();
if target_count != 0 {
let weights = match mesh.weights() {
Some(weights) => weights.to_vec(),
None => vec![0.0; target_count],
};
// unwrap: the parent's call to `MeshMorphWeights::new`
// means this code doesn't run if it returns an `Err`.
// According to https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets
// they should all have the same length.
// > All morph target accessors MUST have the same count as
// > the accessors of the original primitive.
primitive_entity.insert(MeshMorphWeights::new(weights).unwrap());
}
primitive_entity.insert(Aabb::from_min_max(
Vec3::from_slice(&bounds.min),
Vec3::from_slice(&bounds.max),
));
if let Some(extras) = primitive.extras() {
mesh_entity.insert(super::GltfExtras {
primitive_entity.insert(super::GltfExtras {
value: extras.get().to_string(),
});
}
if let Some(name) = mesh.name() {
mesh_entity.insert(Name::new(name.to_string()));
}
primitive_entity.insert(Name::new(primitive_name(&mesh, &primitive)));
// Mark for adding skinned mesh
if let Some(skin) = gltf_node.skin() {
entity_to_skin_index_map.insert(mesh_entity.id(), skin.index());
entity_to_skin_index_map.insert(primitive_entity.id(), skin.index());
}
}
}
@ -885,6 +935,24 @@ fn primitive_label(mesh: &gltf::Mesh, primitive: &Primitive) -> String {
format!("Mesh{}/Primitive{}", mesh.index(), primitive.index())
}
fn primitive_name(mesh: &gltf::Mesh, primitive: &Primitive) -> String {
let mesh_name = mesh.name().unwrap_or("Mesh");
if mesh.primitives().len() > 1 {
format!("{}.{}", mesh_name, primitive.index())
} else {
mesh_name.to_string()
}
}
/// Returns the label for the morph target of `primitive`.
fn morph_targets_label(mesh: &gltf::Mesh, primitive: &Primitive) -> String {
format!(
"Mesh{}/Primitive{}/MorphTargets",
mesh.index(),
primitive.index()
)
}
/// Returns the label for the `material`.
fn material_label(material: &gltf::Material) -> String {
if let Some(index) = material.index() {
@ -1121,6 +1189,38 @@ impl<'a> DataUri<'a> {
}
}
pub(super) struct PrimitiveMorphAttributesIter<'s>(
pub (
Option<Iter<'s, [f32; 3]>>,
Option<Iter<'s, [f32; 3]>>,
Option<Iter<'s, [f32; 3]>>,
),
);
impl<'s> Iterator for PrimitiveMorphAttributesIter<'s> {
type Item = MorphAttributes;
fn next(&mut self) -> Option<Self::Item> {
let position = self.0 .0.as_mut().and_then(|p| p.next());
let normal = self.0 .1.as_mut().and_then(|n| n.next());
let tangent = self.0 .2.as_mut().and_then(|t| t.next());
if position.is_none() && normal.is_none() && tangent.is_none() {
return None;
}
Some(MorphAttributes {
position: position.map(|p| p.into()).unwrap_or(Vec3::ZERO),
normal: normal.map(|n| n.into()).unwrap_or(Vec3::ZERO),
tangent: tangent.map(|t| t.into()).unwrap_or(Vec3::ZERO),
})
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct MorphTargetNames {
pub target_names: Vec<String>,
}
#[cfg(test)]
mod test {
use std::path::PathBuf;

View file

@ -473,6 +473,9 @@ pub fn queue_material_meshes<M: Material>(
let mut mesh_key =
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
| view_key;
if mesh.morph_targets.is_some() {
mesh_key |= MeshPipelineKey::MORPH_TARGETS;
}
match material.properties.alpha_mode {
AlphaMode::Blend => {
mesh_key |= MeshPipelineKey::BLEND_ALPHA;

View file

@ -45,9 +45,9 @@ use bevy_transform::prelude::GlobalTransform;
use bevy_utils::tracing::error;
use crate::{
prepare_lights, AlphaMode, DrawMesh, Material, MaterialPipeline, MaterialPipelineKey,
MeshPipeline, MeshPipelineKey, MeshUniform, RenderMaterials, SetMaterialBindGroup,
SetMeshBindGroup, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
prepare_lights, setup_morph_and_skinning_defs, AlphaMode, DrawMesh, Material, MaterialPipeline,
MaterialPipelineKey, MeshLayouts, MeshPipeline, MeshPipelineKey, MeshUniform, RenderMaterials,
SetMaterialBindGroup, SetMeshBindGroup, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
};
use std::{hash::Hash, marker::PhantomData};
@ -219,8 +219,7 @@ pub fn update_mesh_previous_global_transforms(
pub struct PrepassPipeline<M: Material> {
pub view_layout_motion_vectors: BindGroupLayout,
pub view_layout_no_motion_vectors: BindGroupLayout,
pub mesh_layout: BindGroupLayout,
pub skinned_mesh_layout: BindGroupLayout,
pub mesh_layouts: MeshLayouts,
pub material_layout: BindGroupLayout,
pub material_vertex_shader: Option<Handle<Shader>>,
pub material_fragment_shader: Option<Handle<Shader>>,
@ -307,8 +306,7 @@ impl<M: Material> FromWorld for PrepassPipeline<M> {
PrepassPipeline {
view_layout_motion_vectors,
view_layout_no_motion_vectors,
mesh_layout: mesh_pipeline.mesh_layout.clone(),
skinned_mesh_layout: mesh_pipeline.skinned_mesh_layout.clone(),
mesh_layouts: mesh_pipeline.mesh_layouts.clone(),
material_vertex_shader: match M::prepass_vertex_shader() {
ShaderRef::Default => None,
ShaderRef::Handle(handle) => Some(handle),
@ -423,16 +421,15 @@ where
shader_defs.push("PREPASS_FRAGMENT".into());
}
if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX)
&& layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT)
{
shader_defs.push("SKINNED".into());
vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(4));
vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(5));
bind_group_layouts.insert(2, self.skinned_mesh_layout.clone());
} else {
bind_group_layouts.insert(2, self.mesh_layout.clone());
}
let bind_group = setup_morph_and_skinning_defs(
&self.mesh_layouts,
layout,
4,
&key.mesh_key,
&mut shader_defs,
&mut vertex_attributes,
);
bind_group_layouts.insert(2, bind_group);
let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?;
@ -814,6 +811,9 @@ pub fn queue_prepass_material_meshes<M: Material>(
let mut mesh_key =
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology) | view_key;
if mesh.morph_targets.is_some() {
mesh_key |= MeshPipelineKey::MORPH_TARGETS;
}
let alpha_mode = material.properties.alpha_mode;
match alpha_mode {
AlphaMode::Opaque => {}

View file

@ -21,6 +21,10 @@ struct Vertex {
@location(4) joint_indices: vec4<u32>,
@location(5) joint_weights: vec4<f32>,
#endif // SKINNED
#ifdef MORPH_TARGETS
@builtin(vertex_index) index: u32,
#endif // MORPH_TARGETS
}
struct VertexOutput {
@ -47,10 +51,37 @@ struct VertexOutput {
#endif // DEPTH_CLAMP_ORTHO
}
#ifdef MORPH_TARGETS
fn morph_vertex(vertex_in: Vertex) -> Vertex {
var vertex = vertex_in;
let weight_count = layer_count();
for (var i: u32 = 0u; i < weight_count; i ++) {
let weight = weight_at(i);
if weight == 0.0 {
continue;
}
vertex.position += weight * morph(vertex.index, position_offset, i);
#ifdef VERTEX_NORMALS
vertex.normal += weight * morph(vertex.index, normal_offset, i);
#endif
#ifdef VERTEX_TANGENTS
vertex.tangent += vec4(weight * morph(vertex.index, tangent_offset, i), 0.0);
#endif
}
return vertex;
}
#endif
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex_no_morph);
#else
var vertex = vertex_no_morph;
#endif
#ifdef SKINNED
var model = skin_model(vertex.joint_indices, vertex.joint_weights);
#else // SKINNED

View file

@ -23,3 +23,11 @@ var<uniform> mesh: Mesh;
var<uniform> joint_matrices: SkinnedMesh;
#import bevy_pbr::skinning
#endif
#ifdef MORPH_TARGETS
@group(2) @binding(2)
var<uniform> morph_weights: MorphWeights;
@group(2) @binding(3)
var morph_targets: texture_3d<f32>;
#import bevy_pbr::morph
#endif

View file

@ -1603,6 +1603,9 @@ pub fn queue_shadows<M: Material>(
let mut mesh_key =
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
| MeshPipelineKey::DEPTH_PREPASS;
if mesh.morph_targets.is_some() {
mesh_key |= MeshPipelineKey::MORPH_TARGETS;
}
if is_directional_light {
mesh_key |= MeshPipelineKey::DEPTH_CLAMP_ORTHO;
}

View file

@ -6,7 +6,7 @@ use crate::{
MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
};
use bevy_app::Plugin;
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
use bevy_asset::{load_internal_asset, Assets, Handle, HandleId, HandleUntyped};
use bevy_core_pipeline::{
prepass::ViewPrepassTextures,
tonemapping::{
@ -25,7 +25,8 @@ use bevy_render::{
globals::{GlobalsBuffer, GlobalsUniform},
mesh::{
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
GpuBufferInfo, Mesh, MeshVertexBufferLayout,
GpuBufferInfo, InnerMeshVertexBufferLayout, Mesh, MeshVertexBufferLayout,
VertexAttributeDescriptor,
},
prelude::Msaa,
render_asset::RenderAssets,
@ -40,7 +41,12 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::GlobalTransform;
use std::num::NonZeroU64;
use bevy_utils::{tracing::error, HashMap, Hashed};
use crate::render::{
morph::{extract_morphs, prepare_morphs, MorphIndex, MorphUniform},
MeshLayouts,
};
#[derive(Default)]
pub struct MeshRenderPlugin;
@ -65,6 +71,8 @@ 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);
pub const MORPH_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 970982813587607345);
impl Plugin for MeshRenderPlugin {
fn build(&self, app: &mut bevy_app::App) {
@ -101,17 +109,24 @@ impl Plugin for MeshRenderPlugin {
);
load_internal_asset!(app, MESH_SHADER_HANDLE, "mesh.wgsl", Shader::from_wgsl);
load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl);
load_internal_asset!(app, MORPH_HANDLE, "morph.wgsl", Shader::from_wgsl);
app.add_plugins(UniformComponentPlugin::<MeshUniform>::default());
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<SkinnedMeshUniform>()
.add_systems(ExtractSchedule, (extract_meshes, extract_skinned_meshes))
.init_resource::<MeshBindGroups>()
.init_resource::<MorphUniform>()
.add_systems(
ExtractSchedule,
(extract_meshes, extract_skinned_meshes, extract_morphs),
)
.add_systems(
Render,
(
prepare_skinned_meshes.in_set(RenderSet::Prepare),
prepare_morphs.in_set(RenderSet::Prepare),
queue_mesh_bind_group.in_set(RenderSet::Queue),
queue_mesh_view_bind_groups.in_set(RenderSet::Queue),
),
@ -236,6 +251,7 @@ impl SkinnedMeshJoints {
})
}
/// Updated index to be in address space based on [`SkinnedMeshUniform`] size.
pub fn to_buffer_index(mut self) -> Self {
self.index *= std::mem::size_of::<Mat4>() as u32;
self
@ -280,11 +296,11 @@ pub fn extract_skinned_meshes(
pub struct MeshPipeline {
pub view_layout: BindGroupLayout,
pub view_layout_multisampled: 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,
pub clustered_forward_buffer_binding_type: BufferBindingType,
pub mesh_layouts: MeshLayouts,
}
impl FromWorld for MeshPipeline {
@ -478,40 +494,6 @@ impl FromWorld for MeshPipeline {
entries: &layout_entries(clustered_forward_buffer_binding_type, true),
});
let mesh_binding = BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(MeshUniform::min_size()),
},
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::default();
@ -555,10 +537,9 @@ impl FromWorld for MeshPipeline {
MeshPipeline {
view_layout,
view_layout_multisampled,
mesh_layout,
skinned_mesh_layout,
clustered_forward_buffer_binding_type,
dummy_white_gpu_image,
mesh_layouts: MeshLayouts::new(&render_device),
}
}
}
@ -600,6 +581,7 @@ bitflags::bitflags! {
const SCREEN_SPACE_AMBIENT_OCCLUSION = (1 << 8);
const DEPTH_CLAMP_ORTHO = (1 << 9);
const TAA = (1 << 10);
const MORPH_TARGETS = (1 << 11);
const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state
const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3
const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); //
@ -671,6 +653,41 @@ impl MeshPipelineKey {
}
}
fn is_skinned(layout: &Hashed<InnerMeshVertexBufferLayout>) -> bool {
layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT)
}
pub fn setup_morph_and_skinning_defs(
mesh_layouts: &MeshLayouts,
layout: &Hashed<InnerMeshVertexBufferLayout>,
offset: u32,
key: &MeshPipelineKey,
shader_defs: &mut Vec<ShaderDefVal>,
vertex_attributes: &mut Vec<VertexAttributeDescriptor>,
) -> BindGroupLayout {
let mut add_skin_data = || {
shader_defs.push("SKINNED".into());
vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(offset));
vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(offset + 1));
};
let is_morphed = key.intersects(MeshPipelineKey::MORPH_TARGETS);
match (is_skinned(layout), is_morphed) {
(true, false) => {
add_skin_data();
mesh_layouts.skinned.clone()
}
(true, true) => {
add_skin_data();
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed_skinned.clone()
}
(false, true) => {
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed.clone()
}
(false, false) => mesh_layouts.model_only.clone(),
}
}
impl SpecializedMeshPipeline for MeshPipeline {
type Key = MeshPipelineKey;
@ -724,16 +741,14 @@ impl SpecializedMeshPipeline for MeshPipeline {
}
};
if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX)
&& layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT)
{
shader_defs.push("SKINNED".into());
vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(5));
vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(6));
bind_group_layout.push(self.skinned_mesh_layout.clone());
} else {
bind_group_layout.push(self.mesh_layout.clone());
};
bind_group_layout.push(setup_morph_and_skinning_defs(
&self.mesh_layouts,
layout,
5,
&key,
&mut shader_defs,
&mut vertex_attributes,
));
if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) {
shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into());
@ -888,53 +903,61 @@ impl SpecializedMeshPipeline for MeshPipeline {
}
}
#[derive(Resource)]
pub struct MeshBindGroup {
pub normal: BindGroup,
pub skinned: Option<BindGroup>,
/// Bind groups for meshes currently loaded.
#[derive(Resource, Default)]
pub struct MeshBindGroups {
model_only: Option<BindGroup>,
skinned: Option<BindGroup>,
morph_targets: HashMap<HandleId, BindGroup>,
}
impl MeshBindGroups {
pub fn reset(&mut self) {
self.model_only = None;
self.skinned = None;
self.morph_targets.clear();
}
/// Get the `BindGroup` for `GpuMesh` with given `handle_id`.
pub fn get(&self, handle_id: HandleId, is_skinned: bool, morph: bool) -> Option<&BindGroup> {
match (is_skinned, morph) {
(_, true) => self.morph_targets.get(&handle_id),
(true, false) => self.skinned.as_ref(),
(false, false) => self.model_only.as_ref(),
}
}
}
pub fn queue_mesh_bind_group(
mut commands: Commands,
meshes: Res<RenderAssets<Mesh>>,
mut groups: ResMut<MeshBindGroups>,
mesh_pipeline: Res<MeshPipeline>,
render_device: Res<RenderDevice>,
mesh_uniforms: Res<ComponentUniforms<MeshUniform>>,
skinned_mesh_uniform: Res<SkinnedMeshUniform>,
weights_uniform: Res<MorphUniform>,
) {
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: mesh_binding.clone(),
}],
label: Some("mesh_bind_group"),
layout: &mesh_pipeline.mesh_layout,
}),
skinned: None,
groups.reset();
let layouts = &mesh_pipeline.mesh_layouts;
let Some(model) = mesh_uniforms.buffer() else {
return;
};
groups.model_only = Some(layouts.model_only(&render_device, model));
if let Some(skinned_joints_buffer) = skinned_mesh_uniform.buffer.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,
}));
let skin = skinned_mesh_uniform.buffer.buffer();
if let Some(skin) = skin {
groups.skinned = Some(layouts.skinned(&render_device, model, skin));
}
if let Some(weights) = weights_uniform.buffer.buffer() {
for (id, gpu_mesh) in meshes.iter() {
if let Some(targets) = gpu_mesh.morph_targets.as_ref() {
let group = if let Some(skin) = skin.filter(|_| is_skinned(&gpu_mesh.layout)) {
layouts.morphed_skinned(&render_device, model, skin, weights, targets)
} else {
layouts.morphed(&render_device, model, weights, targets)
};
groups.morph_targets.insert(id.id(), group);
}
}
commands.insert_resource(mesh_bind_group);
}
}
@ -1170,33 +1193,43 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
pub struct SetMeshBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
type Param = SRes<MeshBindGroup>;
type Param = SRes<MeshBindGroups>;
type ViewWorldQuery = ();
type ItemWorldQuery = (
Read<Handle<Mesh>>,
Read<DynamicUniformIndex<MeshUniform>>,
Option<Read<SkinnedMeshJoints>>,
Option<Read<MorphIndex>>,
);
#[inline]
fn render<'w>(
_item: &P,
_view: (),
(mesh_index, skinned_mesh_joints): ROQueryItem<'_, Self::ItemWorldQuery>,
mesh_bind_group: SystemParamItem<'w, '_, Self::Param>,
(mesh, mesh_index, skin_index, morph_index): ROQueryItem<Self::ItemWorldQuery>,
bind_groups: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
if let Some(joints) = skinned_mesh_joints {
pass.set_bind_group(
I,
mesh_bind_group.into_inner().skinned.as_ref().unwrap(),
&[mesh_index.index(), joints.index],
let bind_groups = bind_groups.into_inner();
let is_skinned = skin_index.is_some();
let is_morphed = morph_index.is_some();
let Some(bind_group) = bind_groups.get(mesh.id(), is_skinned, is_morphed) else {
error!(
"The MeshBindGroups resource wasn't set in the render phase. \
It should be set by the queue_mesh_bind_group system.\n\
This is a bevy bug! Please open an issue."
);
} else {
pass.set_bind_group(
I,
&mesh_bind_group.into_inner().normal,
&[mesh_index.index()],
);
}
return RenderCommandResult::Failure;
};
let mut set_bind_group = |indices: &[u32]| pass.set_bind_group(I, bind_group, indices);
let mesh_index = mesh_index.index();
match (skin_index, morph_index) {
(None, None) => set_bind_group(&[mesh_index]),
(Some(skin), None) => set_bind_group(&[mesh_index, skin.index]),
(None, Some(morph)) => set_bind_group(&[mesh_index, morph.index]),
(Some(skin), Some(morph)) => set_bind_group(&[mesh_index, skin.index, morph.index]),
};
RenderCommandResult::Success
}
}

View file

@ -24,6 +24,9 @@ struct Vertex {
@location(5) joint_indices: vec4<u32>,
@location(6) joint_weights: vec4<f32>,
#endif
#ifdef MORPH_TARGETS
@builtin(vertex_index) index: u32,
#endif
};
struct VertexOutput {
@ -31,10 +34,37 @@ struct VertexOutput {
#import bevy_pbr::mesh_vertex_output
};
#ifdef MORPH_TARGETS
fn morph_vertex(vertex_in: Vertex) -> Vertex {
var vertex = vertex_in;
let weight_count = layer_count();
for (var i: u32 = 0u; i < weight_count; i ++) {
let weight = weight_at(i);
if weight == 0.0 {
continue;
}
vertex.position += weight * morph(vertex.index, position_offset, i);
#ifdef VERTEX_NORMALS
vertex.normal += weight * morph(vertex.index, normal_offset, i);
#endif
#ifdef VERTEX_TANGENTS
vertex.tangent += vec4(weight * morph(vertex.index, tangent_offset, i), 0.0);
#endif
}
return vertex;
}
#endif
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex_no_morph);
#else
var vertex = vertex_no_morph;
#endif
#ifdef SKINNED
var model = skin_model(vertex.joint_indices, vertex.joint_weights);
#else

View file

@ -0,0 +1,224 @@
//! Bind group layout related definitions for the mesh pipeline.
use bevy_render::{
mesh::morph::MAX_MORPH_WEIGHTS,
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupLayout, BindGroupLayoutDescriptor, Buffer,
TextureView,
},
renderer::RenderDevice,
};
const MORPH_WEIGHT_SIZE: usize = std::mem::size_of::<f32>();
pub const MORPH_BUFFER_SIZE: usize = MAX_MORPH_WEIGHTS * MORPH_WEIGHT_SIZE;
/// Individual layout entries.
mod layout_entry {
use super::MORPH_BUFFER_SIZE;
use crate::render::mesh::JOINT_BUFFER_SIZE;
use crate::MeshUniform;
use bevy_render::render_resource::{
BindGroupLayoutEntry, BindingType, BufferBindingType, BufferSize, ShaderStages, ShaderType,
TextureSampleType, TextureViewDimension,
};
fn buffer(binding: u32, size: u64, visibility: ShaderStages) -> BindGroupLayoutEntry {
BindGroupLayoutEntry {
binding,
visibility,
count: None,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(size),
},
}
}
pub(super) fn model(binding: u32) -> BindGroupLayoutEntry {
let size = MeshUniform::min_size().get();
buffer(binding, size, ShaderStages::VERTEX | ShaderStages::FRAGMENT)
}
pub(super) fn skinning(binding: u32) -> BindGroupLayoutEntry {
buffer(binding, JOINT_BUFFER_SIZE as u64, ShaderStages::VERTEX)
}
pub(super) fn weights(binding: u32) -> BindGroupLayoutEntry {
buffer(binding, MORPH_BUFFER_SIZE as u64, ShaderStages::VERTEX)
}
pub(super) fn targets(binding: u32) -> BindGroupLayoutEntry {
BindGroupLayoutEntry {
binding,
visibility: ShaderStages::VERTEX,
ty: BindingType::Texture {
view_dimension: TextureViewDimension::D3,
sample_type: TextureSampleType::Float { filterable: false },
multisampled: false,
},
count: None,
}
}
}
/// Individual [`BindGroupEntry`](bevy_render::render_resource::BindGroupEntry)
/// for bind groups.
mod entry {
use super::MORPH_BUFFER_SIZE;
use crate::render::mesh::JOINT_BUFFER_SIZE;
use crate::MeshUniform;
use bevy_render::render_resource::{
BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, ShaderType, TextureView,
};
fn entry(binding: u32, size: u64, buffer: &Buffer) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::Buffer(BufferBinding {
buffer,
offset: 0,
size: Some(BufferSize::new(size).unwrap()),
}),
}
}
pub(super) fn model(binding: u32, buffer: &Buffer) -> BindGroupEntry {
entry(binding, MeshUniform::min_size().get(), buffer)
}
pub(super) fn skinning(binding: u32, buffer: &Buffer) -> BindGroupEntry {
entry(binding, JOINT_BUFFER_SIZE as u64, buffer)
}
pub(super) fn weights(binding: u32, buffer: &Buffer) -> BindGroupEntry {
entry(binding, MORPH_BUFFER_SIZE as u64, buffer)
}
pub(super) fn targets(binding: u32, texture: &TextureView) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::TextureView(texture),
}
}
}
/// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`).
#[derive(Clone)]
pub struct MeshLayouts {
/// The mesh model uniform (transform) and nothing else.
pub model_only: BindGroupLayout,
/// Also includes the uniform for skinning
pub skinned: BindGroupLayout,
/// Also includes the uniform and [`MorphAttributes`] for morph targets.
///
/// [`MorphAttributes`]: bevy_render::mesh::morph::MorphAttributes
pub morphed: BindGroupLayout,
/// Also includes both uniforms for skinning and morph targets, also the
/// morph target [`MorphAttributes`] binding.
///
/// [`MorphAttributes`]: bevy_render::mesh::morph::MorphAttributes
pub morphed_skinned: BindGroupLayout,
}
impl MeshLayouts {
/// Prepare the layouts used by the default bevy [`Mesh`].
///
/// [`Mesh`]: bevy_render::prelude::Mesh
pub fn new(render_device: &RenderDevice) -> Self {
MeshLayouts {
model_only: Self::model_only_layout(render_device),
skinned: Self::skinned_layout(render_device),
morphed: Self::morphed_layout(render_device),
morphed_skinned: Self::morphed_skinned_layout(render_device),
}
}
// ---------- create individual BindGroupLayouts ----------
fn model_only_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[layout_entry::model(0)],
label: Some("mesh_layout"),
})
}
fn skinned_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[layout_entry::model(0), layout_entry::skinning(1)],
label: Some("skinned_mesh_layout"),
})
}
fn morphed_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
layout_entry::model(0),
layout_entry::weights(2),
layout_entry::targets(3),
],
label: Some("morphed_mesh_layout"),
})
}
fn morphed_skinned_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
layout_entry::model(0),
layout_entry::skinning(1),
layout_entry::weights(2),
layout_entry::targets(3),
],
label: Some("morphed_skinned_mesh_layout"),
})
}
// ---------- BindGroup methods ----------
pub fn model_only(&self, render_device: &RenderDevice, model: &Buffer) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor {
entries: &[entry::model(0, model)],
layout: &self.model_only,
label: Some("model_only_mesh_bind_group"),
})
}
pub fn skinned(
&self,
render_device: &RenderDevice,
model: &Buffer,
skin: &Buffer,
) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor {
entries: &[entry::model(0, model), entry::skinning(1, skin)],
layout: &self.skinned,
label: Some("skinned_mesh_bind_group"),
})
}
pub fn morphed(
&self,
render_device: &RenderDevice,
model: &Buffer,
weights: &Buffer,
targets: &TextureView,
) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
entry::model(0, model),
entry::weights(2, weights),
entry::targets(3, targets),
],
layout: &self.morphed,
label: Some("morphed_mesh_bind_group"),
})
}
pub fn morphed_skinned(
&self,
render_device: &RenderDevice,
model: &Buffer,
skin: &Buffer,
weights: &Buffer,
targets: &TextureView,
) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
entry::model(0, model),
entry::skinning(1, skin),
entry::weights(2, weights),
entry::targets(3, targets),
],
layout: &self.morphed_skinned,
label: Some("morphed_skinned_mesh_bind_group"),
})
}
}

View file

@ -4,8 +4,17 @@
@group(2) @binding(0)
var<uniform> mesh: Mesh;
#ifdef SKINNED
@group(2) @binding(1)
var<uniform> joint_matrices: SkinnedMesh;
#import bevy_pbr::skinning
#endif
#ifdef MORPH_TARGETS
@group(2) @binding(2)
var<uniform> morph_weights: MorphWeights;
@group(2) @binding(3)
var morph_targets: texture_3d<f32>;
#import bevy_pbr::morph
#endif

View file

@ -14,6 +14,12 @@ struct SkinnedMesh {
};
#endif
#ifdef MORPH_TARGETS
struct MorphWeights {
weights: array<vec4<f32>, 16u>, // 16 = 64 / 4 (64 = MAX_MORPH_WEIGHTS)
};
#endif
const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u;
// 2^31 - if the flag is set, the sign is positive, else it is negative
const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 2147483648u;

View file

@ -1,7 +1,10 @@
mod fog;
mod light;
mod mesh;
pub(crate) mod mesh;
mod mesh_bindings;
mod morph;
pub use fog::*;
pub use light::*;
pub use mesh::*;
pub use mesh_bindings::MeshLayouts;

View file

@ -0,0 +1,96 @@
use std::{iter, mem};
use bevy_ecs::prelude::*;
use bevy_render::{
mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS},
render_resource::{BufferUsages, BufferVec},
renderer::{RenderDevice, RenderQueue},
view::ComputedVisibility,
Extract,
};
use bytemuck::Pod;
#[derive(Component)]
pub struct MorphIndex {
pub(super) index: u32,
}
#[derive(Resource)]
pub struct MorphUniform {
pub buffer: BufferVec<f32>,
}
impl Default for MorphUniform {
fn default() -> Self {
Self {
buffer: BufferVec::new(BufferUsages::UNIFORM),
}
}
}
pub fn prepare_morphs(
device: Res<RenderDevice>,
queue: Res<RenderQueue>,
mut uniform: ResMut<MorphUniform>,
) {
if uniform.buffer.is_empty() {
return;
}
let buffer = &mut uniform.buffer;
buffer.reserve(buffer.len(), &device);
buffer.write_buffer(&device, &queue);
}
const fn can_align(step: usize, target: usize) -> bool {
step % target == 0 || target % step == 0
}
const WGPU_MIN_ALIGN: usize = 256;
/// Align a [`BufferVec`] to `N` bytes by padding the end with `T::default()` values.
fn add_to_alignment<T: Pod + Default>(buffer: &mut BufferVec<T>) {
let n = WGPU_MIN_ALIGN;
let t_size = mem::size_of::<T>();
if !can_align(n, t_size) {
// This panic is stripped at compile time, due to n, t_size and can_align being const
panic!(
"BufferVec should contain only types with a size multiple or divisible by {n}, \
{} has a size of {t_size}, which is neiter multiple or divisible by {n}",
std::any::type_name::<T>()
);
}
let buffer_size = buffer.len();
let byte_size = t_size * buffer_size;
let bytes_over_n = byte_size % n;
if bytes_over_n == 0 {
return;
}
let bytes_to_add = n - bytes_over_n;
let ts_to_add = bytes_to_add / t_size;
buffer.extend(iter::repeat_with(T::default).take(ts_to_add));
}
pub fn extract_morphs(
mut commands: Commands,
mut previous_len: Local<usize>,
mut uniform: ResMut<MorphUniform>,
query: Extract<Query<(Entity, &ComputedVisibility, &MeshMorphWeights)>>,
) {
uniform.buffer.clear();
let mut values = Vec::with_capacity(*previous_len);
for (entity, computed_visibility, morph_weights) in &query {
if !computed_visibility.is_visible() {
continue;
}
let start = uniform.buffer.len();
let weights = morph_weights.weights();
let legal_weights = weights.iter().take(MAX_MORPH_WEIGHTS).copied();
uniform.buffer.extend(legal_weights);
add_to_alignment::<f32>(&mut uniform.buffer);
let index = (start * mem::size_of::<f32>()) as u32;
values.push((entity, MorphIndex { index }));
}
*previous_len = values.len();
commands.insert_or_spawn_batch(values);
}

View file

@ -0,0 +1,45 @@
// If using this WGSL snippet as an #import, the following should be in scope:
//
// - the `morph_weigths` uniform of type `MorphWeights`
// - the `morph_targets` 3d texture
//
// They are defined in `mesh_types.wgsl` and `mesh_bindings.wgsl`.
#define_import_path bevy_pbr::morph
// NOTE: Those are the "hardcoded" values found in `MorphAttributes` struct
// in crates/bevy_render/src/mesh/morph/visitors.rs
// In an ideal world, the offsets are established dynamically and passed as #defines
// to the shader, but it's out of scope for the initial implementation of morph targets.
const position_offset: u32 = 0u;
const normal_offset: u32 = 3u;
const tangent_offset: u32 = 6u;
const total_component_count: u32 = 9u;
fn layer_count() -> u32 {
let dimensions = textureDimensions(morph_targets);
return u32(dimensions.z);
}
fn component_texture_coord(vertex_index: u32, component_offset: u32) -> vec2<u32> {
let width = u32(textureDimensions(morph_targets).x);
let component_index = total_component_count * vertex_index + component_offset;
return vec2<u32>(component_index % width, component_index / width);
}
fn weight_at(weight_index: u32) -> f32 {
let i = weight_index;
return morph_weights.weights[i / 4u][i % 4u];
}
fn morph_pixel(vertex: u32, component: u32, weight: u32) -> f32 {
let coord = component_texture_coord(vertex, component);
// Due to https://gpuweb.github.io/gpuweb/wgsl/#texel-formats
// While the texture stores a f32, the textureLoad returns a vec4<>, where
// only the first component is set.
return textureLoad(morph_targets, vec3(coord, weight), 0).r;
}
fn morph(vertex_index: u32, component_offset: u32, weight_index: u32) -> vec3<f32> {
return vec3<f32>(
morph_pixel(vertex_index, component_offset, weight_index),
morph_pixel(vertex_index, component_offset + 1u, weight_index),
morph_pixel(vertex_index, component_offset + 2u, weight_index),
);
}

View file

@ -63,6 +63,7 @@ codespan-reporting = "0.11.0"
naga = { version = "0.12.0", features = ["wgsl-in"] }
serde = { version = "1", features = ["derive"] }
bitflags = "2.3"
bytemuck = { version = "1.5", features = ["derive"] }
smallvec = { version = "1.6", features = ["union", "const_generics"] }
once_cell = "1.4.1" # TODO: replace once_cell with std equivalent if/when this lands: https://github.com/rust-lang/rfcs/pull/2788
downcast-rs = "1.2.0"

View file

@ -32,7 +32,7 @@ pub mod prelude {
pub use crate::{
camera::{Camera, OrthographicProjection, PerspectiveProjection, Projection},
color::Color,
mesh::{shape, Mesh},
mesh::{morph::MorphWeights, shape, Mesh},
render_resource::Shader,
spatial_bundle::SpatialBundle,
texture::{Image, ImagePlugin},
@ -49,7 +49,7 @@ use wgpu::Instance;
use crate::{
camera::CameraPlugin,
mesh::MeshPlugin,
mesh::{morph::MorphPlugin, MeshPlugin},
render_resource::{PipelineCache, Shader, ShaderLoader},
renderer::{render_system, RenderInstance},
settings::WgpuSettings,
@ -336,6 +336,7 @@ impl Plugin for RenderPlugin {
ViewPlugin,
MeshPlugin,
GlobalsPlugin,
MorphPlugin,
));
app.register_type::<color::Color>()

View file

@ -3,11 +3,13 @@ pub mod skinning;
pub use wgpu::PrimitiveTopology;
use crate::{
prelude::Image,
primitives::Aabb,
render_asset::{PrepareAssetError, RenderAsset},
render_resource::{Buffer, VertexBufferLayout},
render_asset::{PrepareAssetError, RenderAsset, RenderAssets},
render_resource::{Buffer, TextureView, VertexBufferLayout},
renderer::RenderDevice,
};
use bevy_asset::Handle;
use bevy_core::cast_slice;
use bevy_derive::EnumVariantMeta;
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
@ -35,6 +37,8 @@ pub struct Mesh {
/// which allows easy stable VertexBuffers (i.e. same buffer order)
attributes: BTreeMap<MeshVertexAttributeId, MeshAttributeData>,
indices: Option<Indices>,
morph_targets: Option<Handle<Image>>,
morph_target_names: Option<Vec<String>>,
}
/// Contains geometry in the form of a mesh.
@ -129,6 +133,8 @@ impl Mesh {
primitive_topology,
attributes: Default::default(),
indices: None,
morph_targets: None,
morph_target_names: None,
}
}
@ -444,6 +450,28 @@ impl Mesh {
None
}
/// Whether this mesh has morph targets.
pub fn has_morph_targets(&self) -> bool {
self.morph_targets.is_some()
}
/// Set [morph targets] image for this mesh. This requires a "morph target image". See [`MorphTargetImage`](crate::mesh::morph::MorphTargetImage) for info.
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
pub fn set_morph_targets(&mut self, morph_targets: Handle<Image>) {
self.morph_targets = Some(morph_targets);
}
/// Sets the names of each morph target. This should correspond to the order of the morph targets in `set_morph_targets`.
pub fn set_morph_target_names(&mut self, names: Vec<String>) {
self.morph_target_names = Some(names);
}
/// Gets a list of all morph target names, if they exist.
pub fn morph_target_names(&self) -> Option<&[String]> {
self.morph_target_names.as_deref()
}
}
#[derive(Debug, Clone)]
@ -859,6 +887,7 @@ pub struct GpuMesh {
/// Contains all attribute data for each vertex.
pub vertex_buffer: Buffer,
pub vertex_count: u32,
pub morph_targets: Option<TextureView>,
pub buffer_info: GpuBufferInfo,
pub primitive_topology: PrimitiveTopology,
pub layout: MeshVertexBufferLayout,
@ -879,7 +908,7 @@ pub enum GpuBufferInfo {
impl RenderAsset for Mesh {
type ExtractedAsset = Mesh;
type PreparedAsset = GpuMesh;
type Param = SRes<RenderDevice>;
type Param = (SRes<RenderDevice>, SRes<RenderAssets<Image>>);
/// Clones the mesh.
fn extract_asset(&self) -> Self::ExtractedAsset {
@ -889,7 +918,7 @@ impl RenderAsset for Mesh {
/// Converts the extracted mesh a into [`GpuMesh`].
fn prepare_asset(
mesh: Self::ExtractedAsset,
render_device: &mut SystemParamItem<Self::Param>,
(render_device, images): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
let vertex_buffer_data = mesh.get_vertex_buffer_data();
let vertex_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
@ -920,6 +949,9 @@ impl RenderAsset for Mesh {
buffer_info,
primitive_topology: mesh.primitive_topology(),
layout: mesh_vertex_buffer_layout,
morph_targets: mesh
.morph_targets
.and_then(|mt| images.get(&mt).map(|i| i.texture_view.clone())),
})
}
}

View file

@ -1,5 +1,6 @@
#[allow(clippy::module_inception)]
mod mesh;
pub mod morph;
/// Generation for some primitive shape meshes.
pub mod shape;

View file

@ -0,0 +1,274 @@
use crate::{
mesh::Mesh,
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::Image,
};
use bevy_app::{Plugin, PostUpdate};
use bevy_asset::Handle;
use bevy_ecs::prelude::*;
use bevy_hierarchy::Children;
use bevy_math::Vec3;
use bevy_reflect::Reflect;
use bytemuck::{Pod, Zeroable};
use std::{iter, mem};
use thiserror::Error;
const MAX_TEXTURE_WIDTH: u32 = 2048;
// NOTE: "component" refers to the element count of math objects,
// Vec3 has 3 components, Mat2 has 4 components.
const MAX_COMPONENTS: u32 = MAX_TEXTURE_WIDTH * MAX_TEXTURE_WIDTH;
/// Max target count available for [morph targets](MorphWeights).
pub const MAX_MORPH_WEIGHTS: usize = 64;
/// [Inherit weights](inherit_weights) from glTF mesh parent entity to direct
/// bevy mesh child entities (ie: glTF primitive).
pub struct MorphPlugin;
impl Plugin for MorphPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.register_type::<MorphWeights>()
.register_type::<MeshMorphWeights>()
.add_systems(PostUpdate, inherit_weights);
}
}
#[derive(Error, Clone, Debug)]
pub enum MorphBuildError {
#[error(
"Too many vertex×components in morph target, max is {MAX_COMPONENTS}, \
got {vertex_count}×{component_count} = {}",
*vertex_count * *component_count as usize
)]
TooManyAttributes {
vertex_count: usize,
component_count: u32,
},
#[error(
"Bevy only supports up to {} morph targets (individual poses), tried to \
create a model with {target_count} morph targets",
MAX_MORPH_WEIGHTS
)]
TooManyTargets { target_count: usize },
}
/// An image formatted for use with [`MorphWeights`] for rendering the morph target.
#[derive(Debug)]
pub struct MorphTargetImage(pub Image);
impl MorphTargetImage {
/// Generate textures for each morph target.
///
/// This accepts an "iterator of [`MorphAttributes`] iterators". Each item iterated in the top level
/// iterator corresponds "the attributes of a specific morph target".
///
/// Each pixel of the texture is a component of morph target animated
/// attributes. So a set of 9 pixels is this morph's displacement for
/// position, normal and tangents of a single vertex (each taking 3 pixels).
pub fn new(
targets: impl ExactSizeIterator<Item = impl Iterator<Item = MorphAttributes>>,
vertex_count: usize,
) -> Result<Self, MorphBuildError> {
let max = MAX_TEXTURE_WIDTH;
let target_count = targets.len();
if target_count > MAX_MORPH_WEIGHTS {
return Err(MorphBuildError::TooManyTargets { target_count });
}
let component_count = (vertex_count * MorphAttributes::COMPONENT_COUNT) as u32;
let Some((Rect(width, height), padding)) = lowest_2d(component_count , max) else {
return Err(MorphBuildError::TooManyAttributes { vertex_count, component_count });
};
let data = targets
.flat_map(|mut attributes| {
let layer_byte_count = (padding + component_count) as usize * mem::size_of::<f32>();
let mut buffer = Vec::with_capacity(layer_byte_count);
for _ in 0..vertex_count {
let Some(to_add) = attributes.next() else {
break;
};
buffer.extend_from_slice(bytemuck::bytes_of(&to_add));
}
// Pad each layer so that they fit width * height
buffer.extend(iter::repeat(0).take(padding as usize * mem::size_of::<f32>()));
debug_assert_eq!(buffer.len(), layer_byte_count);
buffer
})
.collect();
let extents = Extent3d {
width,
height,
depth_or_array_layers: target_count as u32,
};
let image = Image::new(extents, TextureDimension::D3, data, TextureFormat::R32Float);
Ok(MorphTargetImage(image))
}
}
/// Controls the [morph targets] for all child [`Handle<Mesh>`] entities. In most cases, [`MorphWeights`] should be considered
/// the "source o[f truth" when writing morph targets for meshes. However you can choose to write child [`MeshMorphWeights`]
/// if your situation requires more granularity. Just note that if you set [`MorphWeights`], it will overwrite child
/// [`MeshMorphWeights`] values.
///
/// This exists because Bevy's [`Mesh`] corresponds to a _single_ surface / material, whereas morph targets
/// as defined in the GLTF spec exist on "multi-primitive meshes" (where each primitive is its own surface with its own material).
/// Therefore in Bevy [`MorphWeights`] an a parent entity are the "canonical weights" from a GLTF perspective, which then
/// synchronized to child [`Handle<Mesh>`] / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective).
///
/// Add this to the parent of one or more [`Entities`](`Entity`) with a [`Handle<Mesh>`] with a [`MeshMorphWeights`].
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
#[derive(Reflect, Default, Debug, Clone, Component)]
#[reflect(Debug, Component)]
pub struct MorphWeights {
weights: Vec<f32>,
/// The first mesh primitive assigned to these weights
first_mesh: Option<Handle<Mesh>>,
}
impl MorphWeights {
pub fn new(
weights: Vec<f32>,
first_mesh: Option<Handle<Mesh>>,
) -> Result<Self, MorphBuildError> {
if weights.len() > MAX_MORPH_WEIGHTS {
let target_count = weights.len();
return Err(MorphBuildError::TooManyTargets { target_count });
}
Ok(MorphWeights {
weights,
first_mesh,
})
}
/// The first child [`Handle<Mesh>`] primitive controlled by these weights.
/// This can be used to look up metadata information such as [`Mesh::morph_target_names`].
pub fn first_mesh(&self) -> Option<&Handle<Mesh>> {
self.first_mesh.as_ref()
}
pub fn weights(&self) -> &[f32] {
&self.weights
}
pub fn weights_mut(&mut self) -> &mut [f32] {
&mut self.weights
}
}
/// Control a specific [`Mesh`] instance's [morph targets]. These control the weights of
/// specific "mesh primitives" in scene formats like GLTF. They can be set manually, but
/// in most cases they should "automatically" synced by setting the [`MorphWeights`] component
/// on a parent entity.
///
/// See [`MorphWeights`] for more details on Bevy's morph target implementation.
///
/// Add this to an [`Entity`] with a [`Handle<Mesh>`] with a [`MorphAttributes`] set
/// to control individual weights of each morph target.
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
#[derive(Reflect, Default, Debug, Clone, Component)]
#[reflect(Debug, Component)]
pub struct MeshMorphWeights {
weights: Vec<f32>,
}
impl MeshMorphWeights {
pub fn new(weights: Vec<f32>) -> Result<Self, MorphBuildError> {
if weights.len() > MAX_MORPH_WEIGHTS {
let target_count = weights.len();
return Err(MorphBuildError::TooManyTargets { target_count });
}
Ok(MeshMorphWeights { weights })
}
pub fn weights(&self) -> &[f32] {
&self.weights
}
pub fn weights_mut(&mut self) -> &mut [f32] {
&mut self.weights
}
}
/// Bevy meshes are gltf primitives, [`MorphWeights`] on the bevy node entity
/// should be inherited by children meshes.
///
/// Only direct children are updated, to fulfill the expectations of glTF spec.
pub fn inherit_weights(
morph_nodes: Query<(&Children, &MorphWeights), (Without<Handle<Mesh>>, Changed<MorphWeights>)>,
mut morph_primitives: Query<&mut MeshMorphWeights, With<Handle<Mesh>>>,
) {
for (children, parent_weights) in &morph_nodes {
let mut iter = morph_primitives.iter_many_mut(children);
while let Some(mut child_weight) = iter.fetch_next() {
child_weight.weights.clear();
child_weight.weights.extend(&parent_weights.weights);
}
}
}
/// Attributes **differences** used for morph targets.
///
/// See [`MorphTargetImage`] for more information.
#[derive(Copy, Clone, PartialEq, Pod, Zeroable, Default)]
#[repr(C)]
pub struct MorphAttributes {
/// The vertex position difference between base mesh and this target.
pub position: Vec3,
/// The vertex normal difference between base mesh and this target.
pub normal: Vec3,
/// The vertex tangent difference between base mesh and this target.
///
/// Note that tangents are a `Vec4`, but only the `xyz` components are
/// animated, as the `w` component is the sign and cannot be animated.
pub tangent: Vec3,
}
impl From<[Vec3; 3]> for MorphAttributes {
fn from([position, normal, tangent]: [Vec3; 3]) -> Self {
MorphAttributes {
position,
normal,
tangent,
}
}
}
impl MorphAttributes {
/// How many components `MorphAttributes` has.
///
/// Each `Vec3` has 3 components, we have 3 `Vec3`, for a total of 9.
pub const COMPONENT_COUNT: usize = 9;
pub fn new(position: Vec3, normal: Vec3, tangent: Vec3) -> Self {
MorphAttributes {
position,
normal,
tangent,
}
}
}
/// Integer division rounded up.
const fn div_ceil(lhf: u32, rhs: u32) -> u32 {
(lhf + rhs - 1) / rhs
}
struct Rect(u32, u32);
/// Find the smallest rectangle of maximum edge size `max_edge` that contains
/// at least `min_includes` cells. `u32` is how many extra cells the rectangle
/// has.
///
/// The following rectangle contains 27 cells, and its longest edge is 9:
/// ```text
/// ----------------------------
/// |1 |2 |3 |4 |5 |6 |7 |8 |9 |
/// ----------------------------
/// |2 | | | | | | | | |
/// ----------------------------
/// |3 | | | | | | | | |
/// ----------------------------
/// ```
///
/// Returns `None` if `max_edge` is too small to build a rectangle
/// containing `min_includes` cells.
fn lowest_2d(min_includes: u32, max_edge: u32) -> Option<(Rect, u32)> {
(1..=max_edge)
.filter_map(|a| {
let b = div_ceil(min_includes, a);
let diff = (a * b).checked_sub(min_includes)?;
Some((Rect(a, b), diff))
})
.filter_map(|(rect, diff)| (rect.1 <= max_edge).then_some((rect, diff)))
.min_by_key(|(_, diff)| *diff)
}

View file

@ -151,6 +151,7 @@ Example | Description
[Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component
[Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve
[Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code
[Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets
[glTF Skinned Mesh](../examples/animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file
## Application

View file

@ -0,0 +1,97 @@
//! Controls morph targets in a loaded scene.
//!
//! Illustrates:
//!
//! - How to access and modify individual morph target weights.
//! See the [`update_weights`] system for details.
//! - How to read morph target names in [`name_morphs`].
//! - How to play morph target animations in [`setup_animations`].
use bevy::prelude::*;
use std::f32::consts::PI;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "morph targets".to_string(),
..default()
}),
..default()
}))
.insert_resource(AmbientLight {
brightness: 1.0,
..default()
})
.add_systems(Startup, setup)
.add_systems(Update, (name_morphs, setup_animations))
.run();
}
#[derive(Resource)]
struct MorphData {
the_wave: Handle<AnimationClip>,
mesh: Handle<Mesh>,
}
fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
commands.insert_resource(MorphData {
the_wave: asset_server.load("models/animated/MorphStressTest.gltf#Animation2"),
mesh: asset_server.load("models/animated/MorphStressTest.gltf#Mesh0/Primitive0"),
});
commands.spawn(SceneBundle {
scene: asset_server.load("models/animated/MorphStressTest.gltf#Scene0"),
..default()
});
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
color: Color::WHITE,
illuminance: 19350.0,
..default()
},
transform: Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)),
..default()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(3.0, 2.1, 10.2).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
/// Plays an [`AnimationClip`] from the loaded [`Gltf`] on the [`AnimationPlayer`] created by the spawned scene.
fn setup_animations(
mut has_setup: Local<bool>,
mut players: Query<(&Name, &mut AnimationPlayer)>,
morph_data: Res<MorphData>,
) {
if *has_setup {
return;
}
for (name, mut player) in &mut players {
// The name of the entity in the GLTF scene containing the AnimationPlayer for our morph targets is "Main"
if name.as_str() != "Main" {
continue;
}
player.play(morph_data.the_wave.clone()).repeat();
*has_setup = true;
}
}
/// You can get the target names in their corresponding [`Mesh`].
/// They are in the order of the weights.
fn name_morphs(
mut has_printed: Local<bool>,
morph_data: Res<MorphData>,
meshes: Res<Assets<Mesh>>,
) {
if *has_printed {
return;
}
let Some(mesh) = meshes.get(&morph_data.mesh) else { return };
let Some(names) = mesh.morph_target_names() else { return };
for name in names {
println!(" {name}");
}
*has_printed = true;
}

View file

@ -0,0 +1,111 @@
//! Control animations of entities in the loaded scene.
use bevy::{gltf::Gltf, prelude::*};
use crate::scene_viewer_plugin::SceneHandle;
/// Controls animation clips for a unique entity.
#[derive(Component)]
struct Clips {
clips: Vec<Handle<AnimationClip>>,
current: usize,
}
impl Clips {
fn new(clips: Vec<Handle<AnimationClip>>) -> Self {
Clips { clips, current: 0 }
}
/// # Panics
///
/// When no clips are present.
fn current(&self) -> Handle<AnimationClip> {
self.clips[self.current].clone_weak()
}
fn advance_to_next(&mut self) {
self.current = (self.current + 1) % self.clips.len();
}
}
/// Read [`AnimationClip`]s from the loaded [`Gltf`] and assign them to the
/// entities they control. [`AnimationClip`]s control specific entities, and
/// trying to play them on an [`AnimationPlayer`] controlling a different
/// entities will result in odd animations, we take extra care to store
/// animation clips for given entities in the [`Clips`] component we defined
/// earlier in this file.
fn assign_clips(
mut players: Query<(Entity, &mut AnimationPlayer, &Name)>,
scene_handle: Res<SceneHandle>,
clips: Res<Assets<AnimationClip>>,
gltf_assets: Res<Assets<Gltf>>,
mut commands: Commands,
mut setup: Local<bool>,
) {
if scene_handle.is_loaded && !*setup {
*setup = true;
} else {
return;
}
let gltf = gltf_assets.get(&scene_handle.gltf_handle).unwrap();
let animations = &gltf.animations;
if !animations.is_empty() {
let count = animations.len();
let plural = if count == 1 { "" } else { "s" };
info!("Found {} animation{plural}", animations.len());
let names: Vec<_> = gltf.named_animations.keys().collect();
info!("Animation names: {names:?}");
}
for (entity, mut player, name) in &mut players {
let clips = clips
.iter()
.filter_map(|(k, v)| v.compatible_with(name).then_some(k))
.map(|id| clips.get_handle(id))
.collect();
let animations = Clips::new(clips);
player.play(animations.current()).repeat();
commands.entity(entity).insert(animations);
}
}
fn handle_inputs(
keyboard_input: Res<Input<KeyCode>>,
mut animation_player: Query<(&mut AnimationPlayer, &mut Clips, Entity, Option<&Name>)>,
) {
for (mut player, mut clips, entity, name) in &mut animation_player {
let display_entity_name = match name {
Some(name) => name.to_string(),
None => format!("entity {entity:?}"),
};
if keyboard_input.just_pressed(KeyCode::Space) {
if player.is_paused() {
info!("resuming animation for {display_entity_name}");
player.resume();
} else {
info!("pausing animation for {display_entity_name}");
player.pause();
}
}
if clips.clips.len() <= 1 {
continue;
}
if keyboard_input.just_pressed(KeyCode::Return) {
info!("switching to new animation for {display_entity_name}");
let resume = !player.is_paused();
// set the current animation to its start and pause it to reset to its starting state
player.set_elapsed(0.0).pause();
clips.advance_to_next();
let current_clip = clips.current();
player.play(current_clip).repeat();
if resume {
player.resume();
}
}
}
}
pub struct AnimationManipulationPlugin;
impl Plugin for AnimationManipulationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, (handle_inputs, assign_clips));
}
}

View file

@ -14,10 +14,14 @@ use bevy::{
window::WindowPlugin,
};
#[cfg(feature = "animation")]
mod animation_plugin;
mod camera_controller_plugin;
mod morph_viewer_plugin;
mod scene_viewer_plugin;
use camera_controller_plugin::{CameraController, CameraControllerPlugin};
use morph_viewer_plugin::MorphViewerPlugin;
use scene_viewer_plugin::{SceneHandle, SceneViewerPlugin};
fn main() {
@ -42,10 +46,14 @@ fn main() {
}),
CameraControllerPlugin,
SceneViewerPlugin,
MorphViewerPlugin,
))
.add_systems(Startup, setup)
.add_systems(PreUpdate, setup_scene_after_load);
#[cfg(feature = "animation")]
app.add_plugins(animation_plugin::AnimationManipulationPlugin);
app.run();
}

View file

@ -0,0 +1,287 @@
//! Enable controls for morph targets detected in a loaded scene.
//!
//! Collect morph targets and assing keys to them,
//! shows on screen additional controls for morph targets.
//!
//! Illustrates how to access and modify individual morph target weights.
//! See the [`update_morphs`] system for details.
//!
//! Also illustrates how to read morph target names in [`detect_morphs`].
use crate::scene_viewer_plugin::SceneHandle;
use bevy::prelude::*;
use std::fmt;
const WEIGHT_PER_SECOND: f32 = 0.8;
const ALL_MODIFIERS: &[KeyCode] = &[KeyCode::ShiftLeft, KeyCode::ControlLeft, KeyCode::AltLeft];
const AVAILABLE_KEYS: [MorphKey; 56] = [
MorphKey::new("r", &[], KeyCode::R),
MorphKey::new("t", &[], KeyCode::T),
MorphKey::new("z", &[], KeyCode::Z),
MorphKey::new("i", &[], KeyCode::I),
MorphKey::new("o", &[], KeyCode::O),
MorphKey::new("p", &[], KeyCode::P),
MorphKey::new("f", &[], KeyCode::F),
MorphKey::new("g", &[], KeyCode::G),
MorphKey::new("h", &[], KeyCode::H),
MorphKey::new("j", &[], KeyCode::J),
MorphKey::new("k", &[], KeyCode::K),
MorphKey::new("y", &[], KeyCode::Y),
MorphKey::new("x", &[], KeyCode::X),
MorphKey::new("c", &[], KeyCode::C),
MorphKey::new("v", &[], KeyCode::V),
MorphKey::new("b", &[], KeyCode::B),
MorphKey::new("n", &[], KeyCode::N),
MorphKey::new("m", &[], KeyCode::M),
MorphKey::new("0", &[], KeyCode::Key0),
MorphKey::new("1", &[], KeyCode::Key1),
MorphKey::new("2", &[], KeyCode::Key2),
MorphKey::new("3", &[], KeyCode::Key3),
MorphKey::new("4", &[], KeyCode::Key4),
MorphKey::new("5", &[], KeyCode::Key5),
MorphKey::new("6", &[], KeyCode::Key6),
MorphKey::new("7", &[], KeyCode::Key7),
MorphKey::new("8", &[], KeyCode::Key8),
MorphKey::new("9", &[], KeyCode::Key9),
MorphKey::new("lshift-R", &[KeyCode::ShiftLeft], KeyCode::R),
MorphKey::new("lshift-T", &[KeyCode::ShiftLeft], KeyCode::T),
MorphKey::new("lshift-Z", &[KeyCode::ShiftLeft], KeyCode::Z),
MorphKey::new("lshift-I", &[KeyCode::ShiftLeft], KeyCode::I),
MorphKey::new("lshift-O", &[KeyCode::ShiftLeft], KeyCode::O),
MorphKey::new("lshift-P", &[KeyCode::ShiftLeft], KeyCode::P),
MorphKey::new("lshift-F", &[KeyCode::ShiftLeft], KeyCode::F),
MorphKey::new("lshift-G", &[KeyCode::ShiftLeft], KeyCode::G),
MorphKey::new("lshift-H", &[KeyCode::ShiftLeft], KeyCode::H),
MorphKey::new("lshift-J", &[KeyCode::ShiftLeft], KeyCode::J),
MorphKey::new("lshift-K", &[KeyCode::ShiftLeft], KeyCode::K),
MorphKey::new("lshift-Y", &[KeyCode::ShiftLeft], KeyCode::Y),
MorphKey::new("lshift-X", &[KeyCode::ShiftLeft], KeyCode::X),
MorphKey::new("lshift-C", &[KeyCode::ShiftLeft], KeyCode::C),
MorphKey::new("lshift-V", &[KeyCode::ShiftLeft], KeyCode::V),
MorphKey::new("lshift-B", &[KeyCode::ShiftLeft], KeyCode::B),
MorphKey::new("lshift-N", &[KeyCode::ShiftLeft], KeyCode::N),
MorphKey::new("lshift-M", &[KeyCode::ShiftLeft], KeyCode::M),
MorphKey::new("lshift-0", &[KeyCode::ShiftLeft], KeyCode::Key0),
MorphKey::new("lshift-1", &[KeyCode::ShiftLeft], KeyCode::Key1),
MorphKey::new("lshift-2", &[KeyCode::ShiftLeft], KeyCode::Key2),
MorphKey::new("lshift-3", &[KeyCode::ShiftLeft], KeyCode::Key3),
MorphKey::new("lshift-4", &[KeyCode::ShiftLeft], KeyCode::Key4),
MorphKey::new("lshift-5", &[KeyCode::ShiftLeft], KeyCode::Key5),
MorphKey::new("lshift-6", &[KeyCode::ShiftLeft], KeyCode::Key6),
MorphKey::new("lshift-7", &[KeyCode::ShiftLeft], KeyCode::Key7),
MorphKey::new("lshift-8", &[KeyCode::ShiftLeft], KeyCode::Key8),
MorphKey::new("lshift-9", &[KeyCode::ShiftLeft], KeyCode::Key9),
];
#[derive(Clone, Copy)]
enum WeightChange {
Increase,
Decrease,
}
impl WeightChange {
fn reverse(&mut self) {
*self = match *self {
WeightChange::Increase => WeightChange::Decrease,
WeightChange::Decrease => WeightChange::Increase,
}
}
fn sign(self) -> f32 {
match self {
WeightChange::Increase => 1.0,
WeightChange::Decrease => -1.0,
}
}
fn change_weight(&mut self, weight: f32, change: f32) -> f32 {
let mut change = change * self.sign();
let new_weight = weight + change;
if new_weight <= 0.0 || new_weight >= 1.0 {
self.reverse();
change = -change;
}
weight + change
}
}
struct Target {
entity_name: Option<String>,
entity: Entity,
name: Option<String>,
index: usize,
weight: f32,
change_dir: WeightChange,
}
impl fmt::Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.name.as_ref(), self.entity_name.as_ref()) {
(None, None) => write!(f, "animation{} of {:?}", self.index, self.entity),
(None, Some(entity)) => write!(f, "animation{} of {entity}", self.index),
(Some(target), None) => write!(f, "{target} of {:?}", self.entity),
(Some(target), Some(entity)) => write!(f, "{target} of {entity}"),
}?;
write!(f, ": {}", self.weight)
}
}
impl Target {
fn text_section(&self, key: &str, style: TextStyle) -> TextSection {
TextSection::new(format!("[{key}] {self}\n"), style)
}
fn new(
entity_name: Option<&Name>,
weights: &[f32],
target_names: Option<&[String]>,
entity: Entity,
) -> Vec<Target> {
let get_name = |i| target_names.and_then(|names| names.get(i));
let entity_name = entity_name.map(|n| n.as_str());
weights
.iter()
.enumerate()
.map(|(index, weight)| Target {
entity_name: entity_name.map(|n| n.to_owned()),
entity,
name: get_name(index).cloned(),
index,
weight: *weight,
change_dir: WeightChange::Increase,
})
.collect()
}
}
#[derive(Resource)]
struct WeightsControl {
weights: Vec<Target>,
}
struct MorphKey {
name: &'static str,
modifiers: &'static [KeyCode],
key: KeyCode,
}
impl MorphKey {
const fn new(name: &'static str, modifiers: &'static [KeyCode], key: KeyCode) -> Self {
MorphKey {
name,
modifiers,
key,
}
}
fn active(&self, inputs: &Input<KeyCode>) -> bool {
let mut modifier = self.modifiers.iter();
let mut non_modifier = ALL_MODIFIERS.iter().filter(|m| !self.modifiers.contains(m));
let key = inputs.pressed(self.key);
let modifier = modifier.all(|m| inputs.pressed(*m));
let non_modifier = non_modifier.all(|m| !inputs.pressed(*m));
key && modifier && non_modifier
}
}
fn update_text(
controls: Option<ResMut<WeightsControl>>,
mut text: Query<&mut Text>,
morphs: Query<&MorphWeights>,
) {
let Some(mut controls) = controls else { return; };
for (i, target) in controls.weights.iter_mut().enumerate() {
let Ok(weights) = morphs.get(target.entity) else {
continue;
};
let Some(&actual_weight) = weights.weights().get(target.index) else {
continue;
};
if actual_weight != target.weight {
target.weight = actual_weight;
}
let key_name = &AVAILABLE_KEYS[i].name;
let mut text = text.single_mut();
text.sections[i + 2].value = format!("[{key_name}] {target}\n");
}
}
fn update_morphs(
controls: Option<ResMut<WeightsControl>>,
mut morphs: Query<&mut MorphWeights>,
input: Res<Input<KeyCode>>,
time: Res<Time>,
) {
let Some(mut controls) = controls else { return; };
for (i, target) in controls.weights.iter_mut().enumerate() {
if !AVAILABLE_KEYS[i].active(&input) {
continue;
}
let Ok(mut weights) = morphs.get_mut(target.entity) else {
continue;
};
// To update individual morph target weights, get the `MorphWeights`
// component and call `weights_mut` to get access to the weights.
let weights_slice = weights.weights_mut();
let i = target.index;
let change = time.delta_seconds() * WEIGHT_PER_SECOND;
let new_weight = target.change_dir.change_weight(weights_slice[i], change);
weights_slice[i] = new_weight;
target.weight = new_weight;
}
}
fn detect_morphs(
mut commands: Commands,
morphs: Query<(Entity, &MorphWeights, Option<&Name>)>,
meshes: Res<Assets<Mesh>>,
scene_handle: Res<SceneHandle>,
mut setup: Local<bool>,
asset_server: Res<AssetServer>,
) {
let no_morphing = morphs.iter().len() == 0;
if no_morphing {
return;
}
if scene_handle.is_loaded && !*setup {
*setup = true;
} else {
return;
}
let mut detected = Vec::new();
for (entity, weights, name) in &morphs {
let target_names = weights
.first_mesh()
.and_then(|h| meshes.get(h))
.and_then(|m| m.morph_target_names());
let targets = Target::new(name, weights.weights(), target_names, entity);
detected.extend(targets);
}
detected.truncate(AVAILABLE_KEYS.len());
let style = TextStyle {
font: asset_server.load("assets/fonts/FiraMono-Medium.ttf"),
font_size: 13.0,
color: Color::WHITE,
};
let mut sections = vec![
TextSection::new("Morph Target Controls\n", style.clone()),
TextSection::new("---------------\n", style.clone()),
];
let target_to_text =
|(i, target): (usize, &Target)| target.text_section(AVAILABLE_KEYS[i].name, style.clone());
sections.extend(detected.iter().enumerate().map(target_to_text));
commands.insert_resource(WeightsControl { weights: detected });
commands.spawn(TextBundle::from_sections(sections).with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(10.0),
left: Val::Px(10.0),
..default()
}));
}
pub struct MorphViewerPlugin;
impl Plugin for MorphViewerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
update_morphs,
detect_morphs,
update_text.after(update_morphs),
),
);
}
}

View file

@ -1,4 +1,4 @@
//! A glTF scene viewer plugin. Provides controls for animation, directional lighting, and switching between scene cameras.
//! A glTF scene viewer plugin. Provides controls for directional lighting, and switching between scene cameras.
//! To use in your own application:
//! - Copy the code for the `SceneViewerPlugin` and add the plugin to your App.
//! - Insert an initialized `SceneHandle` resource into your App's `AssetServer`.
@ -15,10 +15,8 @@ use super::camera_controller_plugin::*;
#[derive(Resource)]
pub struct SceneHandle {
gltf_handle: Handle<Gltf>,
pub gltf_handle: Handle<Gltf>,
scene_index: usize,
#[cfg(feature = "animation")]
animations: Vec<Handle<AnimationClip>>,
instance_id: Option<InstanceId>,
pub is_loaded: bool,
pub has_light: bool,
@ -29,8 +27,6 @@ impl SceneHandle {
Self {
gltf_handle,
scene_index,
#[cfg(feature = "animation")]
animations: Vec::new(),
instance_id: None,
is_loaded: false,
has_light: false,
@ -38,11 +34,18 @@ impl SceneHandle {
}
}
impl fmt::Display for SceneHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"
#[cfg(not(feature = "animation"))]
const INSTRUCTIONS: &str = r#"
Scene Controls:
L - animate light direction
U - toggle shadows
C - cycle through the camera controller and any cameras loaded from the scene
compile with "--features animation" for animation controls.
"#;
#[cfg(feature = "animation")]
const INSTRUCTIONS: &str = "
Scene Controls:
L - animate light direction
U - toggle shadows
@ -51,8 +54,11 @@ Scene Controls:
Space - Play/Pause animation
Enter - Cycle through animations
"
)
";
impl fmt::Display for SceneHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{INSTRUCTIONS}")
}
}
@ -68,8 +74,6 @@ impl Plugin for SceneViewerPlugin {
update_lights,
camera_tracker,
toggle_bounding_boxes.run_if(input_just_pressed(KeyCode::B)),
#[cfg(feature = "animation")]
(start_animation, keyboard_animation_control),
),
);
}
@ -82,7 +86,7 @@ fn toggle_bounding_boxes(mut config: ResMut<GizmoConfig>) {
fn scene_load_check(
asset_server: Res<AssetServer>,
mut scenes: ResMut<Assets<Scene>>,
gltf_assets: ResMut<Assets<Gltf>>,
gltf_assets: Res<Assets<Gltf>>,
mut scene_handle: ResMut<SceneHandle>,
mut scene_spawner: ResMut<SceneSpawner>,
) {
@ -123,22 +127,6 @@ fn scene_load_check(
scene_handle.instance_id =
Some(scene_spawner.spawn(gltf_scene_handle.clone_weak()));
#[cfg(feature = "animation")]
{
scene_handle.animations = gltf.animations.clone();
if !scene_handle.animations.is_empty() {
info!(
"Found {} animation{}",
scene_handle.animations.len(),
if scene_handle.animations.len() == 1 {
""
} else {
"s"
}
);
}
}
info!("Spawning scene...");
}
}
@ -151,62 +139,6 @@ fn scene_load_check(
Some(_) => {}
}
}
#[cfg(feature = "animation")]
fn start_animation(
mut player: Query<&mut AnimationPlayer>,
mut done: Local<bool>,
scene_handle: Res<SceneHandle>,
) {
if !*done {
if let Ok(mut player) = player.get_single_mut() {
if let Some(animation) = scene_handle.animations.first() {
player.play(animation.clone_weak()).repeat();
*done = true;
}
}
}
}
#[cfg(feature = "animation")]
fn keyboard_animation_control(
keyboard_input: Res<Input<KeyCode>>,
mut animation_player: Query<&mut AnimationPlayer>,
scene_handle: Res<SceneHandle>,
mut current_animation: Local<usize>,
mut changing: Local<bool>,
) {
if scene_handle.animations.is_empty() {
return;
}
if let Ok(mut player) = animation_player.get_single_mut() {
if keyboard_input.just_pressed(KeyCode::Space) {
if player.is_paused() {
player.resume();
} else {
player.pause();
}
}
if *changing {
// change the animation the frame after return was pressed
*current_animation = (*current_animation + 1) % scene_handle.animations.len();
player
.play(scene_handle.animations[*current_animation].clone_weak())
.repeat();
*changing = false;
}
if keyboard_input.just_pressed(KeyCode::Return) {
// delay the animation change for one frame
*changing = true;
// set the current animation to its start and pause it to reset to its starting state
player.set_elapsed(0.0).pause();
}
}
}
fn update_lights(
key_input: Res<Input<KeyCode>>,
time: Res<Time>,