Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add morph targets #8158

Merged
merged 39 commits into from
Jun 22, 2023
Merged
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
85634b3
Add morph targets to bevy_pbr
nicopap Mar 10, 2023
e14d164
Replace GltfMeshExtras by MorphTragetNames
nicopap Apr 30, 2023
0317470
Apply doc suggestion
nicopap May 3, 2023
603956d
Rename loader.rs
nicopap May 3, 2023
e817b44
Use a simpler morph model; Do not bind to same wgsl variable
nicopap May 9, 2023
7baf474
Also fix prepass shader
nicopap May 9, 2023
fe79752
Be less gnomic in SetMeshBindGroup::render error messages
nicopap May 10, 2023
d9c62ca
Remove unused link in CREDITS.md
nicopap May 10, 2023
faa9e9a
Clear up match in queue_mesh_bind_group
nicopap May 10, 2023
0934898
Re-use common bind groups in queue_mesh_bind_group
nicopap May 10, 2023
d338d61
Update crates/bevy_pbr/src/render/mesh/morph.rs
nicopap May 10, 2023
b17c345
Clarify panic in add_to_alignment
nicopap May 10, 2023
0386f56
Fix animations
nicopap May 10, 2023
5f44424
Use resource to store mesh bind groups
nicopap May 26, 2023
11567bc
Make meshes queue_mesh_bind_group param immutable
nicopap May 26, 2023
b7c9051
Rename joint to skin for consistency
nicopap May 26, 2023
d1d9c2b
Keep naming consistent
nicopap May 26, 2023
5063b86
Remove has_skin method
nicopap May 26, 2023
2de9951
Remove HARDCODED_ATTRIBUTES
nicopap May 31, 2023
d40a694
Revert mesh module restructure
nicopap Jun 14, 2023
c48c36c
Fix doc error
nicopap Jun 16, 2023
62e4b68
Fix scene viewer
nicopap Jun 16, 2023
80b85a8
Rename function to setup morph shader defs
nicopap Jun 17, 2023
4e115a5
Add changes from #8581
nicopap Jun 17, 2023
14718bc
More minor fixups
nicopap Jun 17, 2023
68542f8
Remove extra ! in morph_targets example
nicopap Jun 17, 2023
719413a
Add morph targets caveats to TAA
nicopap Jun 17, 2023
ea1ebb1
Significantly simplify morph target construction
cart Jun 19, 2023
c21ed92
Fix typo
cart Jun 19, 2023
8a91994
Dont allocate morph target label when unnecessary
cart Jun 19, 2023
2720a52
Simply and optimize queue_mesh_bind_group
cart Jun 20, 2023
3b9755d
Move morph target names to Mesh
cart Jun 20, 2023
09437d6
Rename items of the 'morph' module
nicopap Jun 20, 2023
e0918a4
Spread clips across primitives. Give primitives unique / addressable …
cart Jun 21, 2023
4d08449
Simplify example
cart Jun 21, 2023
615de1a
Update morph_viewer_plugin to flat weigths
nicopap Jun 21, 2023
edb4b56
Split MorphWeights and MeshMorphWeights and re-add "morph inheritance…
cart Jun 21, 2023
1e8eba1
Merge remote-tracking branch 'origin/main' into pr/nicopap/8158
cart Jun 21, 2023
573d0ca
Swap out deprecated add_plugin
cart Jun 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add morph targets to bevy_pbr
Objective
---------

[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 storage 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
nicopap committed Jun 19, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 85634b31338e36ef2324bb7d9f6018b062a510cb
20 changes: 17 additions & 3 deletions CREDITS.md
Original file line number Diff line number Diff line change
@@ -21,8 +21,22 @@
* 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)
* `multiple_morph_target_meshes.gltf`, glTF morph targets from glTF Sample Models, a combination of 4 different models:
* [Animated Morph Cube] (CC0 1.0 Universal)
* [Animated Morph Sphere] (CC0 1.0 Universal)
* [MorphStressTest] ([CC-BY 4.0] by Analytical Graphics, Inc, Model and textures by Ed Mackey)
* [Morph-Primitives Test] ([CC-BY 4.0] by [ft-lab](https://github.com/ft-lab))

[Animated Morph Cube]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedMorphCube
[Animated Morph Sphere]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedMorphSphere
[MorphStressTest]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/MorphStressTest
[Morph-Primitives Test]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/MorphPrimitivesTest
[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/
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
1,817 changes: 1,817 additions & 0 deletions assets/models/animated/multiple_morph_target_meshes.gltf
nicopap marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/bevy_animation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
57 changes: 54 additions & 3 deletions crates/bevy_animation/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,13 +433,36 @@ fn run_animation_player(
animations,
names,
transforms,
morphs,
maybe_parent,
parents,
children,
);
}
}

/// 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 Ok(mut morphs) = (unsafe { morphs.get_unchecked(target) }) else { continue };
nicopap marked this conversation as resolved.
Show resolved Hide resolved
for curve in curves {
// Some curves have only one keyframe used to set a transform
if curve.keyframe_timestamps.len() == 1 {
@@ -484,6 +529,9 @@ fn apply_animation(
Keyframes::Scale(keyframes) => {
transform.scale = transform.scale.lerp(keyframes[0], weight);
}
Keyframes::Weights(keyframes) => {
lerp_morph_weights(morphs.weights_mut(), weight, keyframes, 0);
}
}
continue;
}
@@ -529,6 +577,9 @@ fn apply_animation(
let result = scale_start.lerp(scale_end, lerp);
transform.scale = transform.scale.lerp(result, weight);
}
Keyframes::Weights(keyframes) => {
lerp_morph_weights(morphs.weights_mut(), weight, keyframes, step_start);
}
}
}
}
9 changes: 9 additions & 0 deletions crates/bevy_gltf/src/lib.rs
Original file line number Diff line number Diff line change
@@ -50,6 +50,7 @@ impl Plugin for GltfPlugin {
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
})
.register_type::<GltfExtras>()
.register_type::<GltfMeshExtras>()
.add_asset::<Gltf>()
.add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>()
@@ -111,3 +112,11 @@ pub struct GltfPrimitive {
pub struct GltfExtras {
pub value: String,
}
/// Gltf `extras` field present in the gltf `mesh` of this node.
///
/// This allows accessing the `extras` field of a mesh as a component.
#[derive(Clone, Debug, Reflect, Default, Component)]
#[reflect(Component)]
pub struct GltfMeshExtras {
pub value: String,
}
81 changes: 64 additions & 17 deletions crates/bevy_gltf/src/loader.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod morph;

use anyhow::Result;
use bevy_asset::{
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
@@ -16,6 +18,7 @@ use bevy_render::{
camera::{Camera, OrthographicProjection, PerspectiveProjection, Projection, ScalingMode},
color::Color,
mesh::{
morph::MorphWeights,
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
Indices, Mesh, MeshVertexAttribute, VertexAttributeValues,
},
@@ -64,6 +67,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 +137,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 +169,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())
}
gltf::animation::util::ReadOutputs::Rotations(rots) => {
bevy_animation::Keyframes::Rotation(
rots.into_f32().map(bevy_math::Quat::from_array).collect(),
)
ReadOutputs::Translations(tr) => {
Keyframes::Translation(tr.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::Scales(scale) => {
bevy_animation::Keyframes::Scale(scale.map(Vec3::from).collect())
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::MorphTargetWeights(_) => {
warn!("Morph animation property not yet supported");
continue;
ReadOutputs::MorphTargetWeights(weights) => {
Keyframes::Weights(weights.into_f32().collect())
}
}
} else {
@@ -219,6 +223,7 @@ async fn load_gltf<'a, 'b>(
let mut primitives = vec![];
for primitive in mesh.primitives() {
let primitive_label = primitive_label(&mesh, &primitive);
let morph_targets_label = morph_targets_label(&mesh, &primitive);
let primitive_topology = get_primitive_topology(primitive.mode())?;

let mut mesh = Mesh::new(primitive_topology);
@@ -246,6 +251,15 @@ async fn load_gltf<'a, 'b>(
}));
};

let target_count = reader.read_morph_targets().len();
if target_count != 0 {
let walker = morph::PrimitiveMorphTargets::new(&reader);
let store = |image| {
load_context.set_labeled_asset(&morph_targets_label, LoadedAsset::new(image))
};
mesh.set_morph_targets(walker, store)?;
}

if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none()
&& matches!(mesh.primitive_topology(), PrimitiveTopology::TriangleList)
{
@@ -731,6 +745,16 @@ 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(extras) = mesh.extras().as_ref() {
node.insert(super::GltfMeshExtras {
value: extras.get().to_string(),
});
}
if let Some(weights) = mesh.weights() {
node.insert(MorphWeights::new(weights.to_vec())?);
}
};
node.with_children(|parent| {
if let Some(mesh) = gltf_node.mesh() {
// append primitives
@@ -752,27 +776,41 @@ 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 `MorphWeights::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(MorphWeights::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(name.to_string()));
}
// 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 +923,15 @@ fn primitive_label(mesh: &gltf::Mesh, primitive: &Primitive) -> String {
format!("Mesh{}/Primitive{}", mesh.index(), primitive.index())
}

/// 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() {
85 changes: 85 additions & 0 deletions crates/bevy_gltf/src/loader/morph.rs
nicopap marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::iter;

use bevy_render::mesh::morph::{MorphAttributes, VisitAttributes, VisitMorphTargets};
use gltf::{
accessor::Iter,
mesh::{util::ReadMorphTargets, Reader},
Buffer,
};

pub(super) struct PrimitiveMorphTargetAttributes<'s> {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
positions: Option<Iter<'s, [f32; 3]>>,
normals: Option<Iter<'s, [f32; 3]>>,
tangents: Option<Iter<'s, [f32; 3]>>,
}
type AllAttributesIter<'s> = (
Option<Iter<'s, [f32; 3]>>,
Option<Iter<'s, [f32; 3]>>,
Option<Iter<'s, [f32; 3]>>,
);

impl<'s> PrimitiveMorphTargetAttributes<'s> {
fn new((positions, normals, tangents): AllAttributesIter<'s>) -> Self {
PrimitiveMorphTargetAttributes {
positions,
normals,
tangents,
}
}
}
/// A wrapper struct around [`Reader`] to implement [`VisitMorphTargets`].
///
/// The unreasonable parameter list is a side effect of [`Reader`] having
/// an involved type signature itself.
pub(super) struct PrimitiveMorphTargets<'a, 'b, 's, F>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
inner: &'b Reader<'a, 's, F>,
}
impl<'a, 'b, 's, F> PrimitiveMorphTargets<'a, 'b, 's, F>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
pub fn new(inner: &'b Reader<'a, 's, F>) -> Self {
PrimitiveMorphTargets { inner }
}
}
impl<'a, 'b, 's, F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>> VisitMorphTargets
for PrimitiveMorphTargets<'a, 'b, 's, F>
{
type Visitor = PrimitiveMorphTargetAttributes<'s>;

type Attributes = iter::Map<
ReadMorphTargets<'a, 's, F>,
fn(AllAttributesIter<'s>) -> PrimitiveMorphTargetAttributes<'s>,
>;

fn target_count(&self) -> usize {
self.inner.read_morph_targets().len()
}

fn targets(&mut self) -> Self::Attributes {
self.inner
.read_morph_targets()
.map(PrimitiveMorphTargetAttributes::new)
}
}
impl<'a> VisitAttributes for PrimitiveMorphTargetAttributes<'a> {
// inline: should allow vectorization in the tight loop that calls this method
#[inline]
fn next_attributes(&mut self) -> Option<MorphAttributes> {
// TODO(#8158): Check beforehand if all entries of an attribute
// are empty or None, eliminate them from attributes
const ZERO: [f32; 3] = [0., 0., 0.];
let query_next = |iter: &mut Option<Iter<_>>| match iter {
Some(iter) => iter.next().unwrap_or(ZERO),
None => ZERO,
};
Some(MorphAttributes {
position: query_next(&mut self.positions).into(),
normal: query_next(&mut self.normals).into(),
tangent: query_next(&mut self.tangents).into(),
})
}
}
3 changes: 3 additions & 0 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
@@ -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;
34 changes: 17 additions & 17 deletions crates/bevy_pbr/src/prepass/mod.rs
Original file line number Diff line number Diff line change
@@ -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, set_mesh_binding_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),
@@ -416,16 +414,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 = set_mesh_binding_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)?;

@@ -806,6 +803,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 => {}
29 changes: 29 additions & 0 deletions crates/bevy_pbr/src/prepass/prepass.wgsl
Original file line number Diff line number Diff line change
@@ -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 {
@@ -43,10 +47,35 @@ struct VertexOutput {
#endif // MOTION_VECTOR_PREPASS
}

#ifdef MORPH_TARGETS
fn morph_vertex(vertex: Vertex) -> Vertex {
var vertex = vertex;
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 {
var out: VertexOutput;

#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex);
#endif

#ifdef SKINNED
var model = skin_model(vertex.joint_indices, vertex.joint_weights);
#else // SKINNED
8 changes: 8 additions & 0 deletions crates/bevy_pbr/src/prepass/prepass_bindings.wgsl
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions crates/bevy_pbr/src/render/light.rs
Original file line number Diff line number Diff line change
@@ -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;
}
199 changes: 199 additions & 0 deletions crates/bevy_pbr/src/render/mesh/bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//! Bind [`group`] layout related definitions for the mesh pipeline.
use bevy_render::mesh::morph::MAX_MORPH_WEIGHTS;

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,
}
}
}
/// [`BindGroupLayout`](bevy_render::render_resource::BindGroupLayout)s.
pub mod layout {
use bevy_render::{
render_resource::{BindGroupLayout, BindGroupLayoutDescriptor},
renderer::RenderDevice,
};

use super::layout_entry;

pub fn model_only(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[layout_entry::model(0)],
label: Some("mesh_layout"),
})
}
pub fn skinned(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"),
})
}
pub fn morphed(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"),
})
}
pub fn morphed_and_skinned(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_and_skinned_mesh_layout"),
})
}
}
/// Individual [`BindGroupEntry`](bevy_render::render_resource::BindGroupEntry)
/// for bind [`group`]s.
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),
}
}
}
/// [`BindGroup`](bevy_render::render_resource::BindGroup)s.
pub mod group {
use bevy_render::{
render_resource::{BindGroup, BindGroupDescriptor, BindGroupLayout, Buffer, TextureView},
renderer::RenderDevice,
};

use super::entry;

pub fn model_only(
render_device: &RenderDevice,
layout: &BindGroupLayout,
model: &Buffer,
) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor {
entries: &[entry::model(0, model)],
layout,
label: Some("model_only_mesh_bind_group"),
})
}
pub fn skinned(
render_device: &RenderDevice,
layout: &BindGroupLayout,
model: &Buffer,
skin: &Buffer,
) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor {
entries: &[entry::model(0, model), entry::skinning(1, skin)],
layout,
label: Some("skinned_mesh_bind_group"),
})
}
pub fn morphed(
render_device: &RenderDevice,
layout: &BindGroupLayout,
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,
label: Some("morphed_mesh_bind_group"),
})
}
pub fn morphed_and_skinned(
render_device: &RenderDevice,
layout: &BindGroupLayout,
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,
label: Some("morphed_and_skinned_mesh_bind_group"),
})
}
}
Original file line number Diff line number Diff line change
@@ -24,16 +24,43 @@ 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 {
@builtin(position) clip_position: vec4<f32>,
#import bevy_pbr::mesh_vertex_output
};

#ifdef MORPH_TARGETS
fn morph_vertex(vertex: Vertex) -> Vertex {
var vertex = vertex;
nicopap marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is duplicated in prepass.wgsl because I didn't want to move the struct Vertex definition to mesh_types.wgsl (which probably should be the place where Vertex is defined)


@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex);
nicopap marked this conversation as resolved.
Show resolved Hide resolved
#endif

#ifdef SKINNED
var model = skin_model(vertex.joint_indices, vertex.joint_weights);
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
nicopap marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions crates/bevy_pbr/src/render/mesh/morph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::{iter, mem};

use bevy_ecs::prelude::*;
use bevy_render::{
mesh::morph::{MorphWeights, MAX_MORPH_WEIGHTS},
render_resource::{BufferUsages, BufferVec},
renderer::{RenderDevice, RenderQueue},
view::ComputedVisibility,
Extract,
};
use bytemuck::Pod;

#[derive(Component)]
pub struct Index {
pub(super) index: u32,
}
#[derive(Resource)]
pub struct Uniform {
pub buffer: BufferVec<f32>,
}
impl Default for Uniform {
fn default() -> Self {
Self {
buffer: BufferVec::new(BufferUsages::UNIFORM),
}
}
}

pub fn prepare(device: Res<RenderDevice>, queue: Res<RenderQueue>, mut uniform: ResMut<Uniform>) {
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
}

/// Align a [`BufferVec`] to `N` bytes by padding the end with `T::default()` values.
fn add_to_alignment<const N: usize, T: Pod + Default>(buffer: &mut BufferVec<T>) {
let t_size = mem::size_of::<T>();
if !can_align(t_size, N) {
panic!("BufferVec should contain only types with a size multiple or divisible by N");
nicopap marked this conversation as resolved.
Show resolved Hide resolved
}
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(
mut commands: Commands,
mut previous_len: Local<usize>,
mut uniform: ResMut<Uniform>,
query: Extract<Query<(Entity, &ComputedVisibility, &MorphWeights)>>,
) {
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::<256, f32>(&mut uniform.buffer);

let index = (start * mem::size_of::<f32>()) as u32;
values.push((entity, Index { index }));
}
*previous_len = values.len();
commands.insert_or_spawn_batch(values);
}
45 changes: 45 additions & 0 deletions crates/bevy_pbr/src/render/mesh/morph.wgsl
Original file line number Diff line number Diff line change
@@ -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 const HARDCODED_ATTRIBUTES
// in crates/bevy_render/src/mesh/morph/attribute_iter.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),
);
}
File renamed without changes.
2 changes: 1 addition & 1 deletion crates/bevy_pbr/src/render/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod fog;
mod light;
mod mesh;
pub(crate) mod mesh;

pub use fog::*;
pub use light::*;
1 change: 1 addition & 0 deletions crates/bevy_render/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 5 additions & 2 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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},
@@ -43,6 +43,7 @@ pub mod prelude {

use bevy_window::{PrimaryWindow, RawHandleWrapper};
use globals::GlobalsPlugin;
use mesh::morph::MorphPlugin;
pub use once_cell;
use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue};
use wgpu::Instance;
@@ -334,10 +335,12 @@ impl Plugin for RenderPlugin {
.add_plugin(CameraPlugin)
.add_plugin(ViewPlugin)
.add_plugin(MeshPlugin)
.add_plugin(GlobalsPlugin);
.add_plugin(GlobalsPlugin)
.add_plugin(MorphPlugin);

app.register_type::<color::Color>()
.register_type::<primitives::Aabb>()
.register_type::<mesh::morph::MorphWeights>()
.register_type::<primitives::CascadesFrusta>()
.register_type::<primitives::CubemapFrusta>()
.register_type::<primitives::Frustum>();
55 changes: 50 additions & 5 deletions crates/bevy_render/src/mesh/mesh/mod.rs
Original file line number Diff line number Diff line change
@@ -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::{BindGroup, 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};
@@ -21,6 +23,8 @@ use wgpu::{
VertexStepMode,
};

use super::morph::{MorphAttributesImage, MorphBuildError, MorphTargetImage, VisitMorphTargets};

pub const INDEX_BUFFER_ASSET_INDEX: u64 = 0;
pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10;

@@ -35,6 +39,7 @@ pub struct Mesh {
/// which allows easy stable VertexBuffers (i.e. same buffer order)
attributes: BTreeMap<MeshVertexAttributeId, MeshAttributeData>,
indices: Option<Indices>,
morph_targets: Option<MorphAttributesImage>,
}

/// Contains geometry in the form of a mesh.
@@ -91,14 +96,42 @@ impl Mesh {
primitive_topology,
attributes: Default::default(),
indices: None,
morph_targets: None,
}
}

/// Whether this mesh has skeletal skinning vertex attributes.
pub fn is_skinned(&self) -> bool {
let weight_id = &Self::ATTRIBUTE_JOINT_WEIGHT.id;
let index_id = &Self::ATTRIBUTE_JOINT_INDEX.id;
self.attributes.contains_key(weight_id) && self.attributes.contains_key(index_id)
}

/// Returns the topology of the mesh.
pub fn primitive_topology(&self) -> PrimitiveTopology {
self.primitive_topology
}

/// Whether this mesh has morph targets.
pub fn has_morph_targets(&self) -> bool {
self.morph_targets.is_some()
}

/// Set [morph targets] for this mesh using [`VisitMorphTargets`].
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
pub fn set_morph_targets<Visit: VisitMorphTargets>(
Copy link
Member

@cart cart Jun 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The various abstractions here (VisitMorphTargets, store_image, MorphAttributesImage) introduce too much complexity relative to their value. store_image fights standard dataflow in a way that indicates to me that this needs rethinking.

I've put together a refactor that I think significantly improves legibility, code size, and extensibility. It removes 1,700 187 lines of code text and makes this interface much more straightforward to work with. The core ideas being:

  • Remove VisitMorphTargets in favor of iterators over MorphAttributes
  • Turn this function "inside out". Embrace the fact that morph_targets is just an Image and allow callers to decide how that is populated. Embrace MorphTargetImage as the public interface for creating morph target images. Yes this means users could pass in any image ... but we're already letting them do that with store_image ... the abstraction already leaks.
  • Remove MorphAttributesImage. It seems to exist only to aid in binding lookups. We already have a simple interface for looking up bindings (images.get(handle).map(|i| i.texture_view.clone()). This is called exactly once ... I don't think it merits abstraction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: only 187 lines apparently I grossly misused git diff --stat

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind MorphAttributesImage was to avoid publicly exposing the internal representation of a morph target. It's very "bytely" typed (like stringly typed but with bytes). What if we change the way each weight is laid out in the image? What if instead of an image we switch to a buffer or a more complex representation that can be spliced (for example, to handle subsetting the image to only relevant weights to reduce GPU bandwidth usage)

The Image is also fairly difficult to construct and tightly bound to our shader, so it becomes a perilous path to tread for the user. Though I tend to fall on the side "let the user do whatever they want"

I really like the rest of your proposed changes. I didn't know it was possible to nest impls like that.

&mut self,
visitor: Visit,
store_image: impl FnOnce(Image) -> Handle<Image>,
) -> Result<(), MorphBuildError> {
let vertex_count = self.count_vertices();
let image = MorphTargetImage::new(visitor, vertex_count as u32)?;
let handle = store_image(image.image);
self.morph_targets = Some(MorphAttributesImage(handle));
Ok(())
}

/// Sets the data for a vertex attribute (position, normal etc.). The name will
/// often be one of the associated constants such as [`Mesh::ATTRIBUTE_POSITION`].
///
@@ -816,14 +849,24 @@ impl From<&Indices> for IndexFormat {

/// The GPU-representation of a [`Mesh`].
/// Consists of a vertex data buffer and an optional index data buffer.
#[derive(Debug, Clone)]
#[derive(Debug)]
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,
pub bind_group: Option<BindGroup>,
}
impl GpuMesh {
/// Whether this mesh has skeletal skinning vertex attributes.
pub fn is_skinned(&self) -> bool {
let weight_id = Mesh::ATTRIBUTE_JOINT_WEIGHT.id;
let index_id = Mesh::ATTRIBUTE_JOINT_INDEX.id;
self.layout.contains(weight_id) && self.layout.contains(index_id)
}
}

/// The index/vertex buffer info of a [`GpuMesh`].
@@ -841,7 +884,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 {
@@ -851,7 +894,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 {
@@ -882,6 +925,8 @@ impl RenderAsset for Mesh {
buffer_info,
primitive_topology: mesh.primitive_topology(),
layout: mesh_vertex_buffer_layout,
morph_targets: mesh.morph_targets.and_then(|mt| mt.binding(images)),
bind_group: None,
})
}
}
1 change: 1 addition & 0 deletions crates/bevy_render/src/mesh/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#[allow(clippy::module_inception)]
mod mesh;
pub mod morph;
/// Generation for some primitive shape meshes.
pub mod shape;

233 changes: 233 additions & 0 deletions crates/bevy_render/src/mesh/morph/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
mod visitors;

use bevy_app::{Plugin, PostUpdate};
use bevy_hierarchy::Children;
use thiserror::Error;

use crate::{
mesh::MeshVertexAttribute,
render_asset::RenderAssets,
render_resource::{Extent3d, TextureDimension, TextureFormat, TextureView},
texture::Image,
};
use bevy_asset::Handle;
use bevy_ecs::{
component::Component,
prelude::ReflectComponent,
query::{Changed, With, Without},
system::Query,
};
use bevy_reflect::Reflect;
use std::{iter, mem};

pub use visitors::{MorphAttributes, VisitAttributes, VisitMorphTargets};

use self::visitors::HARDCODED_ATTRIBUTES;

use super::Mesh;

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;

#[derive(Error, Clone, Debug)]
pub enum MorphBuildError {
#[error("components of morph target attributes must all be f32, one of them isn't: {0:?}")]
InvalidAttribute(MeshVertexAttribute),
#[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 },
}
pub type Result<T> = std::result::Result<T, MorphBuildError>;

/// Value of [`Mesh`]'s [morph targets]. See also [`Mesh::set_morph_targets`].
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
/// [`Mesh::set_morph_targets`]: super::Mesh::set_morph_targets
/// [`Mesh`]: super::Mesh
#[derive(Debug, Clone)]
pub(crate) struct MorphAttributesImage(pub(crate) Handle<Image>);
impl MorphAttributesImage {
pub(crate) fn binding(&self, images: &RenderAssets<Image>) -> Option<TextureView> {
Some(images.get(&self.0)?.texture_view.clone())
}
}

/// 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)
}

#[derive(Debug)]
pub(crate) struct MorphTargetImage {
/// The image used with [`MorphWeights`] for rendering the morph target.
pub(crate) image: Image,
}
impl MorphTargetImage {
pub(crate) fn new(targets: impl VisitMorphTargets, vertex_count: u32) -> Result<Self> {
let attributes = HARDCODED_ATTRIBUTES;
let total_components_size: u64 = attributes.iter().map(|a| a.format.size()).sum();
if total_components_size % 4 != 0 {
// Unwrap: we have the guarentee that at least one format is not %4
let first_invalid = attributes
.iter()
.find(|a| a.format.size() % 4 != 0)
.unwrap();
return Err(MorphBuildError::InvalidAttribute(first_invalid.clone()));
}
let total_components = total_components_size as usize / mem::size_of::<f32>();

let target_count = targets.target_count();
if target_count > MAX_MORPH_WEIGHTS {
return Err(MorphBuildError::TooManyTargets { target_count });
}
let image = Self::displacements_buffer(
targets,
vertex_count as usize,
target_count,
total_components as u32,
)?;
Ok(MorphTargetImage { image })
}

/// Generate textures for each 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).
fn displacements_buffer(
mut targets: impl VisitMorphTargets,
vertex_count: usize,
target_count: usize,
total_components: u32,
) -> Result<Image> {
let max = MAX_TEXTURE_WIDTH;
let component_count = vertex_count as u32 * total_components;
let Some((Rect(width, height), padding)) = lowest_2d(component_count, max) else {
return Err(MorphBuildError::TooManyAttributes { vertex_count, component_count });
};
let data = targets
.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_attributes() 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(image)
}
}

/// Control a [`Mesh`]'s [morph targets].
///
/// 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
/// [`Entity`]: bevy_ecs::prelude::Entity
#[derive(Reflect, Default, Debug, Clone, Component)]
#[reflect(Debug, Component)]
pub struct MorphWeights {
weights: Vec<f32>,
}
impl MorphWeights {
pub fn new(weights: Vec<f32>) -> Result<Self> {
if weights.len() > MAX_MORPH_WEIGHTS {
let target_count = weights.len();
return Err(MorphBuildError::TooManyTargets { target_count });
}
Ok(MorphWeights { 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 MorphWeights, With<Handle<Mesh>>>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect splitting this in two different components is preferable. Otherwise it might become a bit awkward for the user to Query MorphWeights for specific mesh (as glTF mesh) or mesh (as bevy 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);
}
}
}

/// [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.add_systems(PostUpdate, inherit_weights);
}
}
141 changes: 141 additions & 0 deletions crates/bevy_render/src/mesh/morph/visitors.rs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The traits are a bit involved here. But two things to keep in mind:

  1. This would be used by people who implement their own mesh loaders. bevy_gltf already has a visitor implementation.
  2. Hopefully the example shows well enough how a user could successfully implement the VisitMorphTarget.

Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use bevy_core::{Pod, Zeroable};
use bevy_math::Vec3;
use wgpu::VertexFormat;

use crate::mesh::{Mesh, MeshVertexAttribute};

/// The set of attributes conviniently accessible in the `gltf` crate.
///
/// Also the only ones useable with the current morph targets implementation.
/// See `morph.wgsl`.
pub const HARDCODED_ATTRIBUTES: &[MeshVertexAttribute] = &[
Mesh::ATTRIBUTE_POSITION,
Mesh::ATTRIBUTE_NORMAL,
MeshVertexAttribute::new("Vertex_Tangent_Morph_Attribute", 7, VertexFormat::Float32x3),
nicopap marked this conversation as resolved.
Show resolved Hide resolved
];

/// Attributes **differences** used for morph targets.
///
/// See [`VisitMorphTargets`] for more informations.
#[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 {
pub fn new(position: Vec3, normal: Vec3, tangent: Vec3) -> Self {
MorphAttributes {
position,
normal,
tangent,
}
}
}

/// All attributes of all vertices for a given [morph target][VisitMorphTargets].
pub trait VisitAttributes {
/// Morph target attributes data for a single vertex.
///
/// `Self` acts like an iterator, each call to `next_attributes` advances
/// to the attributes for the next vertex.
fn next_attributes(&mut self) -> Option<MorphAttributes>;
}
/// An accessor to read morph target attributes into bevy's mesh internal
/// morph target representation.
///
/// `Attributes` is an iterator where each individual item is all attributes
/// for all vertices used in a single target (think of "targets" as "poses"),
/// see [`VisitAttributes`].
///
/// Note that morph target attributes are **differences** between the base
/// mesh attribute value and the given pose's attribute value, following
/// closely the [glTF spec].
///
/// This is simplified pseudocode showing how bevy implements morph targets in
/// its vertex shader:
///
/// ```ignore
/// fn morph_vertex(mut vertex: Vertex) -> Vertex {
/// for (i, weight) in weights.enumerate() {
/// vertex.position += weight * morph(vertex.index, position_offset, i);
/// vertex.normal += weight * morph(vertex.index, normal_offset, i);
/// vertex.tangent += vec4(weight * morph(vertex.index, tangent_offset, i), 0.0);
/// }
/// return vertex;
/// }
/// ```
///
/// # Example
///
/// ```rust
/// use bevy_render::mesh::morph::{VisitAttributes, VisitMorphTargets, MorphAttributes};
/// use bevy_render::mesh::{Mesh, MeshVertexAttribute};
/// use bevy_math::Vec3;
/// use std::slice::Iter;
///
/// // Each entry in the slice `.0: &[T]` is a target.
/// // Consider `T: [&[Vec3]; 3]` a target. It has `3` attributes,
/// // each is a slice of `Vec3` (`&[ Vec3 ]`) where an entry is the
/// // attribute for the target for a single vertex.
/// struct TargetsCollection<'a>(&'a [[&'a [Vec3]; 3]]);
///
/// struct SingleTarget<'a>(Iter<'a, Vec3>, Iter<'a, Vec3>, Iter<'a, Vec3>);
///
/// impl<'a> VisitAttributes for SingleTarget<'a> {
/// fn next_attributes(&mut self) -> Option<MorphAttributes> {
/// let mut item = || {
/// Some(MorphAttributes::new(
/// *self.0.next()?,
/// *self.1.next()?,
/// *self.2.next()?,
/// ))
/// };
/// Some(item().unwrap_or_else(MorphAttributes::default))
/// }
/// }
/// impl<'a> VisitMorphTargets for TargetsCollection<'a> {
/// type Visitor = SingleTarget<'a>;
/// type Attributes =
/// std::iter::Map<Iter<'a, [&'a [Vec3]; 3]>, fn(&[&'a [Vec3]; 3]) -> Self::Visitor>;
/// fn target_count(&self) -> usize {
/// self.0.len()
/// }
/// fn targets(&mut self) -> Self::Attributes {
/// self.0
/// .iter()
/// .map(|[p, n, t]| SingleTarget(p.iter(), n.iter(), t.iter()))
/// }
/// }
/// ```
///
/// ## Accounting
///
/// - a mesh has: `T` morph targets or poses
/// - a mesh has: `V` vertices
/// - a morph target has `A` animated attributes
/// - there is `V` animated attribute per mesh per morph target
///
/// [glTF spec]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets
pub trait VisitMorphTargets {
type Visitor: VisitAttributes;
type Attributes: Iterator<Item = Self::Visitor>;
fn target_count(&self) -> usize;
fn targets(&mut self) -> Self::Attributes;
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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
141 changes: 141 additions & 0 deletions examples/animation/morph_targets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! 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 std::f32::consts::PI;

use bevy::{
gltf::{Gltf, GltfMeshExtras},
prelude::*,
};
use serde::Deserialize;
use serde_json::from_str;

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, update_weights))
.run();
}

fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
commands.spawn(SceneBundle {
scene: asset_server.load("models/animated/multiple_morph_target_meshes.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()
});
}

/// Marker for weights that are not animated by the animation system.
#[derive(Component)]
struct UpdateWeights;

/// To update weights, query for [`MorphWeights`].
///
/// Note that direct children with a [`Handle<Mesh>`] component of entities
/// with a [`MorphWeights`] component will inherit their parent's weights.
fn update_weights(mut morphs: Query<&mut MorphWeights, With<UpdateWeights>>, time: Res<Time>) {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
let mut t = time.elapsed_seconds();
let offset_per_weight = PI / 4.0;
for mut morph in &mut morphs {
for weight in morph.weights_mut() {
*weight = t.cos().abs();
t += offset_per_weight;
}
}
}

/// Deserialize the json field used in `gltf.mesh.extras` to associate
/// weight indices to target names.
#[derive(Component, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TargetNames {
target_names: Vec<String>,
}

/// You can get the target names by reading the `GltfMeshExtras` component.
/// They are in the order of the weights.
fn name_morphs(query: Query<(&Name, &GltfMeshExtras)>, mut has_printed: Local<bool>) {
if *has_printed {
return;
}
for (name, extras) in &query {
info!("Node {name} has the following targets:");
let target_names: TargetNames = from_str(&extras.value).unwrap();
for name in &target_names.target_names {
info!("\t{name}");
}
*has_printed = true;
}
}

/// 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.
fn setup_animations(
mut query: Query<
(&Name, Entity, Option<&mut AnimationPlayer>),
(With<MorphWeights>, Without<Handle<Mesh>>),
>,
gltf: Res<Assets<Gltf>>,
clips: Res<Assets<AnimationClip>>,
mut commands: Commands,
mut has_setup: Local<bool>,
) {
if *has_setup {
return;
}
let Some((_, gltf)) = gltf.iter().next() else {
return;
};
// We check compatibility by getting the [`AnimationClip`] out of the
// [`Assets<AnimationClip>`] and using `compatible_with(&Name)`
let is_compatible = |name, clip| {
let Some(clip) = clips.get(clip) else { return false };
clip.compatible_with(name)
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is a bit involved. I think maybe the Handle<AnimationClip> could be stored in the AnimationPlayer, or a different component. But I didn't want to revamp the whole of bevy_animation because of #8357, so I decided to just provide this compatible_with method. to make it possible to use animation clips correctly with morph targets.

The issue already exists. Morph targets exacerbates it by causing a panic when they are misused. I just think it's not part of the scope of this already large PR.

for (name, entity, player) in &mut query {
match player {
Some(mut player) => {
let compatible = gltf
.animations
.iter()
.find(|clip| is_compatible(name, clip))
.unwrap();
player.play(compatible.clone_weak()).repeat();
}
None => {
commands.entity(entity).insert(UpdateWeights);
}
}
*has_setup = true;
}
}
111 changes: 111 additions & 0 deletions examples/tools/scene_viewer/animation_plugin.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}
8 changes: 8 additions & 0 deletions examples/tools/scene_viewer/main.rs
Original file line number Diff line number Diff line change
@@ -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() {
@@ -43,9 +47,13 @@ fn main() {
)
.add_plugin(CameraControllerPlugin)
.add_plugin(SceneViewerPlugin)
.add_plugin(MorphViewerPlugin)
.add_systems(Startup, setup)
.add_systems(PreUpdate, setup_scene_after_load);

#[cfg(feature = "animation")]
app.add_plugin(animation_plugin::AnimationManipulationPlugin);

app.run();
}

308 changes: 308 additions & 0 deletions examples/tools/scene_viewer/morph_viewer_plugin.rs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big list of "available keys" is a bit of a ugly hack, but I'm not sure there is a better way of handling it. Any suggestion?

Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
//! 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 std::fmt;

use bevy::{gltf::GltfMeshExtras, prelude::*};
use serde::Deserialize;
use serde_json::from_str;

use crate::scene_viewer_plugin::SceneHandle;

const WEIGHT_PER_SECOND: f32 = 0.8;
const ALL_MODIFIERS: &[KeyCode] = &[KeyCode::LShift, KeyCode::LControl, KeyCode::LAlt];
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::LShift], KeyCode::R),
MorphKey::new("lshift-T", &[KeyCode::LShift], KeyCode::T),
MorphKey::new("lshift-Z", &[KeyCode::LShift], KeyCode::Z),
MorphKey::new("lshift-I", &[KeyCode::LShift], KeyCode::I),
MorphKey::new("lshift-O", &[KeyCode::LShift], KeyCode::O),
MorphKey::new("lshift-P", &[KeyCode::LShift], KeyCode::P),
MorphKey::new("lshift-F", &[KeyCode::LShift], KeyCode::F),
MorphKey::new("lshift-G", &[KeyCode::LShift], KeyCode::G),
MorphKey::new("lshift-H", &[KeyCode::LShift], KeyCode::H),
MorphKey::new("lshift-J", &[KeyCode::LShift], KeyCode::J),
MorphKey::new("lshift-K", &[KeyCode::LShift], KeyCode::K),
MorphKey::new("lshift-Y", &[KeyCode::LShift], KeyCode::Y),
MorphKey::new("lshift-X", &[KeyCode::LShift], KeyCode::X),
MorphKey::new("lshift-C", &[KeyCode::LShift], KeyCode::C),
MorphKey::new("lshift-V", &[KeyCode::LShift], KeyCode::V),
MorphKey::new("lshift-B", &[KeyCode::LShift], KeyCode::B),
MorphKey::new("lshift-N", &[KeyCode::LShift], KeyCode::N),
MorphKey::new("lshift-M", &[KeyCode::LShift], KeyCode::M),
MorphKey::new("lshift-0", &[KeyCode::LShift], KeyCode::Key0),
MorphKey::new("lshift-1", &[KeyCode::LShift], KeyCode::Key1),
MorphKey::new("lshift-2", &[KeyCode::LShift], KeyCode::Key2),
MorphKey::new("lshift-3", &[KeyCode::LShift], KeyCode::Key3),
MorphKey::new("lshift-4", &[KeyCode::LShift], KeyCode::Key4),
MorphKey::new("lshift-5", &[KeyCode::LShift], KeyCode::Key5),
MorphKey::new("lshift-6", &[KeyCode::LShift], KeyCode::Key6),
MorphKey::new("lshift-7", &[KeyCode::LShift], KeyCode::Key7),
MorphKey::new("lshift-8", &[KeyCode::LShift], KeyCode::Key8),
MorphKey::new("lshift-9", &[KeyCode::LShift], KeyCode::Key9),
];

/// Deserialize the json field used in `gltf.mesh.extras` to associate
/// weight indices to target names.
#[derive(Component, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TargetNames {
target_names: Vec<String>,
}

#[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<TargetNames>,
entity: Entity,
) -> Vec<Target> {
let get_name = |i| {
target_names
.as_ref()
.and_then(|names| names.target_names.get(i).cloned())
};
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),
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(
morphs: Query<
(
Entity,
&MorphWeights,
Option<&Name>,
Option<&GltfMeshExtras>,
),
Without<Handle<Mesh>>,
>,
mut commands: Commands,
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, extras) in &morphs {
// You can get the target names by reading the `GltfMeshExtras` component.
let target_names = extras.and_then(|e| from_str(&e.value).ok());
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),
),
);
}
}
108 changes: 20 additions & 88 deletions examples/tools/scene_viewer/scene_viewer_plugin.rs
Original file line number Diff line number Diff line change
@@ -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,20 +27,25 @@ impl SceneHandle {
Self {
gltf_handle,
scene_index,
#[cfg(feature = "animation")]
animations: Vec::new(),
instance_id: None,
is_loaded: false,
has_light: false,
}
}
}

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