mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
animation player (#4375)
# Objective - Add a basic animation player - Single track - Not generic, can only animate `Transform`s - With plenty of possible optimisations available - Close-ish to https://github.com/bevyengine/rfcs/pull/49 - https://discord.com/channels/691052431525675048/774027865020039209/958820063148929064 ## Solution - Can play animations - looping or not - Can pause animations - Can seek in animation - Can alter speed of animation - I also removed the previous gltf animation example https://user-images.githubusercontent.com/8672791/161051887-e79283f0-9803-448a-93d0-5f7a62acb02d.mp4
This commit is contained in:
parent
0ed08d6a15
commit
449a1d223c
18 changed files with 585 additions and 1417 deletions
|
@ -21,5 +21,6 @@
|
|||
* 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 triangle from [glTF Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedTriangle) (CC0 1.0 Universal)
|
||||
* glTF box animated from [glTF Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/BoxAnimated) ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - by [Cesium](https://cesium.com))
|
||||
* 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/))
|
||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -17,6 +17,7 @@ members = ["crates/*", "examples/ios", "tools/ci", "errors"]
|
|||
|
||||
[features]
|
||||
default = [
|
||||
"animation",
|
||||
"bevy_audio",
|
||||
"bevy_gilrs",
|
||||
"bevy_winit",
|
||||
|
@ -43,6 +44,7 @@ render = [
|
|||
]
|
||||
|
||||
# Optional bevy crates
|
||||
bevy_animation = ["bevy_internal/bevy_animation"]
|
||||
bevy_audio = ["bevy_internal/bevy_audio"]
|
||||
bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"]
|
||||
bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"]
|
||||
|
@ -98,6 +100,9 @@ bevy_ci_testing = ["bevy_internal/bevy_ci_testing"]
|
|||
# Enable the "debug asset server" for hot reloading internal assets
|
||||
debug_asset_server = ["bevy_internal/debug_asset_server"]
|
||||
|
||||
# Enable animation support, and glTF animation loading
|
||||
animation = ["bevy_internal/animation"]
|
||||
|
||||
[dependencies]
|
||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.7.0-dev", default-features = false, optional = true }
|
||||
bevy_internal = { path = "crates/bevy_internal", version = "0.7.0-dev", default-features = false }
|
||||
|
@ -183,10 +188,6 @@ path = "examples/3d/lighting.rs"
|
|||
name = "load_gltf"
|
||||
path = "examples/3d/load_gltf.rs"
|
||||
|
||||
[[example]]
|
||||
name = "manual_gltf_animation_player"
|
||||
path = "examples/3d/manual_gltf_animation_player.rs"
|
||||
|
||||
[[example]]
|
||||
name = "many_cubes"
|
||||
path = "examples/3d/many_cubes.rs"
|
||||
|
@ -236,6 +237,10 @@ name = "wireframe"
|
|||
path = "examples/3d/wireframe.rs"
|
||||
|
||||
# Animation
|
||||
[[example]]
|
||||
name = "animated_fox"
|
||||
path = "examples/animation/animated_fox.rs"
|
||||
|
||||
[[example]]
|
||||
name = "custom_skinned_mesh"
|
||||
path = "examples/animation/custom_skinned_mesh.rs"
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
{
|
||||
"scene" : 0,
|
||||
"scenes" : [
|
||||
{
|
||||
"nodes" : [ 0 ]
|
||||
}
|
||||
],
|
||||
|
||||
"nodes" : [
|
||||
{
|
||||
"mesh" : 0,
|
||||
"rotation" : [ 0.0, 0.0, 0.0, 1.0 ]
|
||||
}
|
||||
],
|
||||
|
||||
"meshes" : [
|
||||
{
|
||||
"primitives" : [ {
|
||||
"attributes" : {
|
||||
"POSITION" : 1
|
||||
},
|
||||
"indices" : 0
|
||||
} ]
|
||||
}
|
||||
],
|
||||
|
||||
"animations": [
|
||||
{
|
||||
"samplers" : [
|
||||
{
|
||||
"input" : 2,
|
||||
"interpolation" : "LINEAR",
|
||||
"output" : 3
|
||||
}
|
||||
],
|
||||
"channels" : [ {
|
||||
"sampler" : 0,
|
||||
"target" : {
|
||||
"node" : 0,
|
||||
"path" : "rotation"
|
||||
}
|
||||
} ]
|
||||
}
|
||||
],
|
||||
|
||||
"buffers" : [
|
||||
{
|
||||
"uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
|
||||
"byteLength" : 44
|
||||
},
|
||||
{
|
||||
"uri" : "data:application/octet-stream;base64,AAAAAAAAgD4AAAA/AABAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAPT9ND/0/TS/AAAAAAAAAAAAAAAAAACAPw==",
|
||||
"byteLength" : 100
|
||||
}
|
||||
],
|
||||
"bufferViews" : [
|
||||
{
|
||||
"buffer" : 0,
|
||||
"byteOffset" : 0,
|
||||
"byteLength" : 6,
|
||||
"target" : 34963
|
||||
},
|
||||
{
|
||||
"buffer" : 0,
|
||||
"byteOffset" : 8,
|
||||
"byteLength" : 36,
|
||||
"target" : 34962
|
||||
},
|
||||
{
|
||||
"buffer" : 1,
|
||||
"byteOffset" : 0,
|
||||
"byteLength" : 100
|
||||
}
|
||||
],
|
||||
"accessors" : [
|
||||
{
|
||||
"bufferView" : 0,
|
||||
"byteOffset" : 0,
|
||||
"componentType" : 5123,
|
||||
"count" : 3,
|
||||
"type" : "SCALAR",
|
||||
"max" : [ 2 ],
|
||||
"min" : [ 0 ]
|
||||
},
|
||||
{
|
||||
"bufferView" : 1,
|
||||
"byteOffset" : 0,
|
||||
"componentType" : 5126,
|
||||
"count" : 3,
|
||||
"type" : "VEC3",
|
||||
"max" : [ 1.0, 1.0, 0.0 ],
|
||||
"min" : [ 0.0, 0.0, 0.0 ]
|
||||
},
|
||||
{
|
||||
"bufferView" : 2,
|
||||
"byteOffset" : 0,
|
||||
"componentType" : 5126,
|
||||
"count" : 5,
|
||||
"type" : "SCALAR",
|
||||
"max" : [ 1.0 ],
|
||||
"min" : [ 0.0 ]
|
||||
},
|
||||
{
|
||||
"bufferView" : 2,
|
||||
"byteOffset" : 20,
|
||||
"componentType" : 5126,
|
||||
"count" : 5,
|
||||
"type" : "VEC4",
|
||||
"max" : [ 0.0, 0.0, 1.0, 1.0 ],
|
||||
"min" : [ 0.0, 0.0, 0.0, -0.707 ]
|
||||
}
|
||||
],
|
||||
|
||||
"asset" : {
|
||||
"version" : "2.0"
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because one or more lines are too long
BIN
assets/models/animated/Fox.glb
Normal file
BIN
assets/models/animated/Fox.glb
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
21
crates/bevy_animation/Cargo.toml
Normal file
21
crates/bevy_animation/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "bevy_animation"
|
||||
version = "0.7.0-dev"
|
||||
edition = "2021"
|
||||
description = "Provides animation functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.7.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.7.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.7.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.7.0-dev", features = ["bevy"] }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.7.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.7.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.7.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.7.0-dev" }
|
284
crates/bevy_animation/src/lib.rs
Normal file
284
crates/bevy_animation/src/lib.rs
Normal file
|
@ -0,0 +1,284 @@
|
|||
//! Animation for the game engine Bevy
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use bevy_app::{App, CoreStage, Plugin};
|
||||
use bevy_asset::{AddAsset, Assets, Handle};
|
||||
use bevy_core::{Name, Time};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
entity::Entity,
|
||||
prelude::Component,
|
||||
reflect::ReflectComponent,
|
||||
schedule::ParallelSystemDescriptorCoercion,
|
||||
system::{Query, Res},
|
||||
};
|
||||
use bevy_hierarchy::{Children, HierarchySystem};
|
||||
use bevy_math::{Quat, Vec3};
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_transform::{prelude::Transform, TransformSystem};
|
||||
use bevy_utils::{tracing::warn, HashMap};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{
|
||||
AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Keyframes, VariableCurve,
|
||||
};
|
||||
}
|
||||
|
||||
/// List of keyframes for one of the attribute of a [`Transform`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Keyframes {
|
||||
/// Keyframes for rotation.
|
||||
Rotation(Vec<Quat>),
|
||||
/// Keyframes for translation.
|
||||
Translation(Vec<Vec3>),
|
||||
/// Keyframes for scale.
|
||||
Scale(Vec<Vec3>),
|
||||
}
|
||||
|
||||
/// Describes how an attribute of a [`Transform`] should be animated.
|
||||
///
|
||||
/// `keyframe_timestamps` and `keyframes` should have the same length.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VariableCurve {
|
||||
/// Timestamp for each of the keyframes.
|
||||
pub keyframe_timestamps: Vec<f32>,
|
||||
/// List of the keyframes.
|
||||
pub keyframes: Keyframes,
|
||||
}
|
||||
|
||||
/// Path to an entity, with [`Name`]s. Each entity in a path must have a name.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
|
||||
pub struct EntityPath {
|
||||
/// Parts of the path
|
||||
pub parts: Vec<Name>,
|
||||
}
|
||||
|
||||
/// A list of [`VariableCurve`], and the [`EntityPath`] to which they apply.
|
||||
#[derive(Clone, TypeUuid, Debug, Default)]
|
||||
#[uuid = "d81b7179-0448-4eb0-89fe-c067222725bf"]
|
||||
pub struct AnimationClip {
|
||||
curves: HashMap<EntityPath, Vec<VariableCurve>>,
|
||||
duration: f32,
|
||||
}
|
||||
|
||||
impl AnimationClip {
|
||||
#[inline]
|
||||
/// Hashmap of the [`VariableCurve`]s per [`EntityPath`].
|
||||
pub fn curves(&self) -> &HashMap<EntityPath, Vec<VariableCurve>> {
|
||||
&self.curves
|
||||
}
|
||||
|
||||
/// Add a [`VariableCurve`] to an [`EntityPath`].
|
||||
pub fn add_curve_to_path(&mut self, path: EntityPath, curve: VariableCurve) {
|
||||
// Update the duration of the animation by this curve duration if it's longer
|
||||
self.duration = self
|
||||
.duration
|
||||
.max(*curve.keyframe_timestamps.last().unwrap_or(&0.0));
|
||||
self.curves.entry(path).or_default().push(curve);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animation controls
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct AnimationPlayer {
|
||||
paused: bool,
|
||||
repeat: bool,
|
||||
speed: f32,
|
||||
elapsed: f32,
|
||||
animation_clip: Handle<AnimationClip>,
|
||||
}
|
||||
|
||||
impl Default for AnimationPlayer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
paused: false,
|
||||
repeat: false,
|
||||
speed: 1.0,
|
||||
elapsed: 0.0,
|
||||
animation_clip: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationPlayer {
|
||||
/// Start playing an animation, resetting state of the player
|
||||
pub fn play(&mut self, handle: Handle<AnimationClip>) -> &mut Self {
|
||||
*self = Self {
|
||||
animation_clip: handle,
|
||||
..Default::default()
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the animation to repeat
|
||||
pub fn repeat(&mut self) -> &mut Self {
|
||||
self.repeat = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Stop the animation from repeating
|
||||
pub fn stop_repeating(&mut self) -> &mut Self {
|
||||
self.repeat = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Pause the animation
|
||||
pub fn pause(&mut self) {
|
||||
self.paused = true;
|
||||
}
|
||||
|
||||
/// Unpause the animation
|
||||
pub fn resume(&mut self) {
|
||||
self.paused = false;
|
||||
}
|
||||
|
||||
/// Is the animation paused
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.paused
|
||||
}
|
||||
|
||||
/// Speed of the animation playback
|
||||
pub fn speed(&self) -> f32 {
|
||||
self.speed
|
||||
}
|
||||
|
||||
/// Set the speed of the animation playback
|
||||
pub fn set_speed(&mut self, speed: f32) -> &mut Self {
|
||||
self.speed = speed;
|
||||
self
|
||||
}
|
||||
|
||||
/// Time elapsed playing the animation
|
||||
pub fn elapsed(&self) -> f32 {
|
||||
self.elapsed
|
||||
}
|
||||
|
||||
/// Seek to a specific time in the animation
|
||||
pub fn set_elapsed(&mut self, elapsed: f32) -> &mut Self {
|
||||
self.elapsed = elapsed;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// System that will play all animations, using any entity with a [`AnimationPlayer`]
|
||||
/// and a [`Handle<AnimationClip>`] as an animation root
|
||||
pub fn animation_player(
|
||||
time: Res<Time>,
|
||||
animations: Res<Assets<AnimationClip>>,
|
||||
mut animation_players: Query<(Entity, &mut AnimationPlayer)>,
|
||||
names: Query<&Name>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
children: Query<&Children>,
|
||||
) {
|
||||
for (entity, mut player) in animation_players.iter_mut() {
|
||||
if let Some(animation_clip) = animations.get(&player.animation_clip) {
|
||||
// Continue if paused unless the `AnimationPlayer` was changed
|
||||
// This allow the animation to still be updated if the player.elapsed field was manually updated in pause
|
||||
if player.paused && !player.is_changed() {
|
||||
continue;
|
||||
}
|
||||
if !player.paused {
|
||||
player.elapsed += time.delta_seconds() * player.speed;
|
||||
}
|
||||
let mut elapsed = player.elapsed;
|
||||
if player.repeat {
|
||||
elapsed %= animation_clip.duration;
|
||||
}
|
||||
if elapsed < 0.0 {
|
||||
elapsed += animation_clip.duration;
|
||||
}
|
||||
'entity: for (path, curves) in &animation_clip.curves {
|
||||
// PERF: finding the target entity can be optimised
|
||||
let mut current_entity = entity;
|
||||
// Ignore the first name, it is the root node which we already have
|
||||
for part in path.parts.iter().skip(1) {
|
||||
let mut found = false;
|
||||
if let Ok(children) = children.get(current_entity) {
|
||||
for child in children.deref() {
|
||||
if let Ok(name) = names.get(*child) {
|
||||
if name == part {
|
||||
// Found a children with the right name, continue to the next part
|
||||
current_entity = *child;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
warn!("Entity not found for path {:?} on part {:?}", path, part);
|
||||
continue 'entity;
|
||||
}
|
||||
}
|
||||
if let Ok(mut transform) = transforms.get_mut(current_entity) {
|
||||
for curve in curves {
|
||||
// Find the current keyframe
|
||||
// PERF: finding the current keyframe can be optimised
|
||||
let step_start = match curve
|
||||
.keyframe_timestamps
|
||||
.binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap())
|
||||
{
|
||||
Ok(i) => i,
|
||||
Err(0) => continue, // this curve isn't started yet
|
||||
Err(n) if n > curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished
|
||||
Err(i) => i - 1,
|
||||
};
|
||||
let ts_start = curve.keyframe_timestamps[step_start];
|
||||
let ts_end = curve.keyframe_timestamps[step_start + 1];
|
||||
let lerp = (elapsed - ts_start) / (ts_end - ts_start);
|
||||
|
||||
// Apply the keyframe
|
||||
match &curve.keyframes {
|
||||
Keyframes::Rotation(keyframes) => {
|
||||
let rot_start = keyframes[step_start];
|
||||
let mut rot_end = keyframes[step_start + 1];
|
||||
// Choose the smallest angle for the rotation
|
||||
if rot_end.dot(rot_start) < 0.0 {
|
||||
rot_end = -rot_end;
|
||||
}
|
||||
// Rotations are using a spherical linear interpolation
|
||||
transform.rotation = Quat::from_array(rot_start.normalize().into())
|
||||
.slerp(Quat::from_array(rot_end.normalize().into()), lerp);
|
||||
}
|
||||
Keyframes::Translation(keyframes) => {
|
||||
let translation_start = keyframes[step_start];
|
||||
let translation_end = keyframes[step_start + 1];
|
||||
let result = translation_start.lerp(translation_end, lerp);
|
||||
transform.translation = result;
|
||||
}
|
||||
Keyframes::Scale(keyframes) => {
|
||||
let scale_start = keyframes[step_start];
|
||||
let scale_end = keyframes[step_start + 1];
|
||||
let result = scale_start.lerp(scale_end, lerp);
|
||||
transform.scale = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds animation support to an app
|
||||
#[derive(Default)]
|
||||
pub struct AnimationPlugin {}
|
||||
|
||||
impl Plugin for AnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<AnimationClip>()
|
||||
.register_type::<AnimationPlayer>()
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
animation_player
|
||||
.before(TransformSystem::TransformPropagate)
|
||||
.after(HierarchySystem::ParentUpdate),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,19 +10,20 @@ keywords = ["bevy"]
|
|||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_animation = { path = "../bevy_animation", version = "0.7.0-dev", optional = true }
|
||||
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.7.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.7.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.7.0-dev" }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.7.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.7.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.7.0-dev" }
|
||||
bevy_pbr = { path = "../bevy_pbr", version = "0.7.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.7.0-dev", features = ["bevy"] }
|
||||
bevy_render = { path = "../bevy_render", version = "0.7.0-dev" }
|
||||
bevy_scene = { path = "../bevy_scene", version = "0.7.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.7.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.7.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.7.0-dev" }
|
||||
bevy_scene = { path = "../bevy_scene", version = "0.7.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.7.0-dev" }
|
||||
|
||||
# other
|
||||
gltf = { version = "1.0.0", default-features = false, features = [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
||||
use bevy_math::{Quat, Vec3};
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
use bevy_animation::AnimationClip;
|
||||
use bevy_utils::HashMap;
|
||||
|
||||
mod loader;
|
||||
|
@ -8,7 +8,7 @@ pub use loader::*;
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{AddAsset, Handle};
|
||||
use bevy_pbr::StandardMaterial;
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::mesh::Mesh;
|
||||
use bevy_scene::Scene;
|
||||
|
||||
|
@ -22,9 +22,7 @@ impl Plugin for GltfPlugin {
|
|||
.add_asset::<Gltf>()
|
||||
.add_asset::<GltfNode>()
|
||||
.add_asset::<GltfPrimitive>()
|
||||
.add_asset::<GltfMesh>()
|
||||
.add_asset::<GltfAnimation>()
|
||||
.register_type::<GltfAnimatedNode>();
|
||||
.add_asset::<GltfMesh>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,8 +39,10 @@ pub struct Gltf {
|
|||
pub nodes: Vec<Handle<GltfNode>>,
|
||||
pub named_nodes: HashMap<String, Handle<GltfNode>>,
|
||||
pub default_scene: Option<Handle<Scene>>,
|
||||
pub animations: Vec<Handle<GltfAnimation>>,
|
||||
pub named_animations: HashMap<String, Handle<GltfAnimation>>,
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
pub animations: Vec<Handle<AnimationClip>>,
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
pub named_animations: HashMap<String, Handle<AnimationClip>>,
|
||||
}
|
||||
|
||||
/// A glTF node with all of its child nodes, its [`GltfMesh`] and
|
||||
|
@ -69,52 +69,3 @@ pub struct GltfPrimitive {
|
|||
pub mesh: Handle<Mesh>,
|
||||
pub material: Option<Handle<StandardMaterial>>,
|
||||
}
|
||||
|
||||
/// Interpolation method for an animation. Part of a [`GltfNodeAnimation`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GltfAnimationInterpolation {
|
||||
Linear,
|
||||
Step,
|
||||
CubicSpline,
|
||||
}
|
||||
|
||||
/// How a property of a glTF node should be animated. The property and its value can be found
|
||||
/// through the [`GltfNodeAnimationKeyframes`] attribute.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GltfNodeAnimation {
|
||||
pub keyframe_timestamps: Vec<f32>,
|
||||
pub keyframes: GltfNodeAnimationKeyframes,
|
||||
pub interpolation: GltfAnimationInterpolation,
|
||||
}
|
||||
|
||||
/// A glTF animation, listing how each node (by its index) that is part of it should be animated.
|
||||
#[derive(Default, Clone, TypeUuid, Debug)]
|
||||
#[uuid = "d81b7179-0448-4eb0-89fe-c067222725bf"]
|
||||
pub struct GltfAnimation {
|
||||
pub node_animations: HashMap<usize, Vec<GltfNodeAnimation>>,
|
||||
}
|
||||
|
||||
/// Key frames of an animation.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GltfNodeAnimationKeyframes {
|
||||
Rotation(Vec<Quat>),
|
||||
Translation(Vec<Vec3>),
|
||||
Scale(Vec<Vec3>),
|
||||
}
|
||||
|
||||
impl Default for GltfNodeAnimation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keyframe_timestamps: Default::default(),
|
||||
keyframes: GltfNodeAnimationKeyframes::Translation(Default::default()),
|
||||
interpolation: GltfAnimationInterpolation::Linear,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A glTF node that is part of an animation, with its index.
|
||||
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
pub struct GltfAnimatedNode {
|
||||
pub index: usize,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use anyhow::Result;
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
use bevy_animation::{AnimationClip, AnimationPlayer, EntityPath, Keyframes, VariableCurve};
|
||||
use bevy_asset::{
|
||||
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
|
||||
};
|
||||
|
@ -33,15 +35,12 @@ use bevy_utils::{HashMap, HashSet};
|
|||
use gltf::{
|
||||
mesh::Mode,
|
||||
texture::{MagFilter, MinFilter, WrappingMode},
|
||||
Material, Primitive,
|
||||
Material, Node, Primitive,
|
||||
};
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
Gltf, GltfAnimatedNode, GltfAnimation, GltfAnimationInterpolation, GltfNode, GltfNodeAnimation,
|
||||
GltfNodeAnimationKeyframes,
|
||||
};
|
||||
use crate::{Gltf, GltfNode};
|
||||
|
||||
/// An error that occurs when loading a glTF file.
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -129,18 +128,30 @@ async fn load_gltf<'a, 'b>(
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
let paths = {
|
||||
let mut paths = HashMap::<usize, Vec<Name>>::new();
|
||||
for scene in gltf.scenes() {
|
||||
for node in scene.nodes() {
|
||||
paths_recur(node, &[], &mut paths);
|
||||
}
|
||||
}
|
||||
paths
|
||||
};
|
||||
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
let (animations, named_animations) = {
|
||||
let mut animations = vec![];
|
||||
let mut named_animations = HashMap::default();
|
||||
let mut animated_nodes = HashSet::default();
|
||||
for animation in gltf.animations() {
|
||||
let mut gltf_animation = GltfAnimation::default();
|
||||
let mut animation_clip = AnimationClip::default();
|
||||
for channel in animation.channels() {
|
||||
let interpolation = match channel.sampler().interpolation() {
|
||||
gltf::animation::Interpolation::Linear => GltfAnimationInterpolation::Linear,
|
||||
gltf::animation::Interpolation::Step => GltfAnimationInterpolation::Step,
|
||||
gltf::animation::Interpolation::CubicSpline => {
|
||||
GltfAnimationInterpolation::CubicSpline
|
||||
}
|
||||
match channel.sampler().interpolation() {
|
||||
gltf::animation::Interpolation::Linear => (),
|
||||
other => warn!(
|
||||
"Animation interpolation {:?} is not supported, will use linear",
|
||||
other
|
||||
),
|
||||
};
|
||||
let node = channel.target().node();
|
||||
let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
|
||||
|
@ -148,27 +159,25 @@ async fn load_gltf<'a, 'b>(
|
|||
match inputs {
|
||||
gltf::accessor::Iter::Standard(times) => times.collect(),
|
||||
gltf::accessor::Iter::Sparse(_) => {
|
||||
warn!("sparse accessor not supported for animation sampler input");
|
||||
warn!("Sparse accessor not supported for animation sampler input");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("animations without a sampler input are not supported");
|
||||
warn!("Animations without a sampler input are not supported");
|
||||
return Err(GltfError::MissingAnimationSampler(animation.index()));
|
||||
};
|
||||
|
||||
let keyframes = if let Some(outputs) = reader.read_outputs() {
|
||||
match outputs {
|
||||
gltf::animation::util::ReadOutputs::Translations(tr) => {
|
||||
GltfNodeAnimationKeyframes::Translation(tr.map(Vec3::from).collect())
|
||||
Keyframes::Translation(tr.map(Vec3::from).collect())
|
||||
}
|
||||
gltf::animation::util::ReadOutputs::Rotations(rots) => {
|
||||
GltfNodeAnimationKeyframes::Rotation(
|
||||
rots.into_f32().map(Quat::from_array).collect(),
|
||||
)
|
||||
Keyframes::Rotation(rots.into_f32().map(Quat::from_array).collect())
|
||||
}
|
||||
gltf::animation::util::ReadOutputs::Scales(scale) => {
|
||||
GltfNodeAnimationKeyframes::Scale(scale.map(Vec3::from).collect())
|
||||
Keyframes::Scale(scale.map(Vec3::from).collect())
|
||||
}
|
||||
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
|
||||
warn!("Morph animation property not yet supported");
|
||||
|
@ -176,30 +185,38 @@ async fn load_gltf<'a, 'b>(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
warn!("animations without a sampler output are not supported");
|
||||
warn!("Animations without a sampler output are not supported");
|
||||
return Err(GltfError::MissingAnimationSampler(animation.index()));
|
||||
};
|
||||
|
||||
gltf_animation
|
||||
.node_animations
|
||||
.entry(node.index())
|
||||
.or_default()
|
||||
.push(GltfNodeAnimation {
|
||||
if let Some(path) = paths.get(&node.index()) {
|
||||
animation_clip.add_curve_to_path(
|
||||
EntityPath {
|
||||
parts: path.clone(),
|
||||
},
|
||||
VariableCurve {
|
||||
keyframe_timestamps,
|
||||
keyframes,
|
||||
interpolation,
|
||||
});
|
||||
animated_nodes.insert(node.index());
|
||||
},
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
"Animation ignored for node {}: part of its hierarchy is missing a name",
|
||||
node.index()
|
||||
);
|
||||
}
|
||||
}
|
||||
let handle = load_context.set_labeled_asset(
|
||||
&format!("Animation{}", animation.index()),
|
||||
LoadedAsset::new(gltf_animation),
|
||||
LoadedAsset::new(animation_clip),
|
||||
);
|
||||
if let Some(name) = animation.name() {
|
||||
named_animations.insert(name.to_string(), handle.clone());
|
||||
}
|
||||
animations.push(handle);
|
||||
}
|
||||
(animations, named_animations)
|
||||
};
|
||||
|
||||
let mut meshes = vec![];
|
||||
let mut named_meshes = HashMap::default();
|
||||
|
@ -436,7 +453,6 @@ async fn load_gltf<'a, 'b>(
|
|||
parent,
|
||||
load_context,
|
||||
&buffer_data,
|
||||
&animated_nodes,
|
||||
&mut node_index_to_entity_map,
|
||||
&mut entity_to_skin_index_map,
|
||||
);
|
||||
|
@ -450,6 +466,13 @@ async fn load_gltf<'a, 'b>(
|
|||
return Err(err);
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
if !animations.is_empty() {
|
||||
world
|
||||
.entity_mut(*node_index_to_entity_map.get(&0).unwrap())
|
||||
.insert(AnimationPlayer::default());
|
||||
}
|
||||
|
||||
for (&entity, &skin_index) in &entity_to_skin_index_map {
|
||||
let mut entity = world.entity_mut(entity);
|
||||
let skin = gltf.skins().nth(skin_index).unwrap();
|
||||
|
@ -486,13 +509,26 @@ async fn load_gltf<'a, 'b>(
|
|||
named_materials,
|
||||
nodes,
|
||||
named_nodes,
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
animations,
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
named_animations,
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn paths_recur(node: Node, current_path: &[Name], paths: &mut HashMap<usize, Vec<Name>>) {
|
||||
if let Some(name) = node.name() {
|
||||
let mut path = current_path.to_owned();
|
||||
path.push(Name::new(name.to_string()));
|
||||
for child in node.children() {
|
||||
paths_recur(child, &path, paths);
|
||||
}
|
||||
paths.insert(node.index(), path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a glTF texture as a bevy [`Image`] and returns it together with its label.
|
||||
async fn load_texture<'a>(
|
||||
gltf_texture: gltf::Texture<'a>,
|
||||
|
@ -633,7 +669,6 @@ fn load_node(
|
|||
world_builder: &mut WorldChildBuilder,
|
||||
load_context: &mut LoadContext,
|
||||
buffer_data: &[Vec<u8>],
|
||||
animated_nodes: &HashSet<usize>,
|
||||
node_index_to_entity_map: &mut HashMap<usize, Entity>,
|
||||
entity_to_skin_index_map: &mut HashMap<Entity, usize>,
|
||||
) -> Result<(), GltfError> {
|
||||
|
@ -643,12 +678,6 @@ fn load_node(
|
|||
Mat4::from_cols_array_2d(&transform.matrix()),
|
||||
)));
|
||||
|
||||
if animated_nodes.contains(&gltf_node.index()) {
|
||||
node.insert(GltfAnimatedNode {
|
||||
index: gltf_node.index(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(name) = gltf_node.name() {
|
||||
node.insert(Name::new(name.to_string()));
|
||||
}
|
||||
|
@ -798,7 +827,6 @@ fn load_node(
|
|||
parent,
|
||||
load_context,
|
||||
buffer_data,
|
||||
animated_nodes,
|
||||
node_index_to_entity_map,
|
||||
entity_to_skin_index_map,
|
||||
) {
|
||||
|
|
|
@ -53,6 +53,9 @@ webgl = ["bevy_pbr/webgl", "bevy_render/webgl"]
|
|||
# enable systems that allow for automated testing on CI
|
||||
bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render/ci_limits"]
|
||||
|
||||
# Enable animation support, and glTF animation loading
|
||||
animation = ["bevy_animation", "bevy_gltf/bevy_animation"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
|
||||
|
@ -72,6 +75,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.7.0-dev" }
|
|||
bevy_window = { path = "../bevy_window", version = "0.7.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.7.0-dev" }
|
||||
# bevy (optional)
|
||||
bevy_animation = { path = "../bevy_animation", optional = true, version = "0.7.0-dev" }
|
||||
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.7.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.7.0-dev" }
|
||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.7.0-dev" }
|
||||
|
|
|
@ -68,6 +68,9 @@ impl PluginGroup for DefaultPlugins {
|
|||
|
||||
#[cfg(feature = "bevy_gilrs")]
|
||||
group.add(bevy_gilrs::GilrsPlugin::default());
|
||||
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
group.add(bevy_animation::AnimationPlugin::default());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,12 @@ pub mod window {
|
|||
pub use bevy_window::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
pub mod animation {
|
||||
//! Provides types and plugins for animations.
|
||||
pub use bevy_animation::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_audio")]
|
||||
pub mod audio {
|
||||
//! Provides types and plugins for audio playback.
|
||||
|
|
|
@ -11,6 +11,10 @@ pub use bevy_derive::{bevy_main, Deref, DerefMut};
|
|||
#[cfg(feature = "bevy_audio")]
|
||||
pub use crate::audio::prelude::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
pub use crate::animation::prelude::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "bevy_core_pipeline")]
|
||||
pub use crate::core_pipeline::prelude::*;
|
||||
|
|
|
@ -1,230 +0,0 @@
|
|||
use bevy::{
|
||||
core::FixedTimestep,
|
||||
gltf::*,
|
||||
math::{const_quat, const_vec3},
|
||||
prelude::*,
|
||||
scene::InstanceId,
|
||||
};
|
||||
|
||||
/// This illustrates loading animations from GLTF files and manually playing them.
|
||||
/// Note that a higher level animation api is in the works. This exists to illustrate how to
|
||||
/// read and build a custom GLTF animator, not to illustrate a final Bevy animation api.
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 1.0,
|
||||
})
|
||||
.add_startup_system(setup)
|
||||
.add_system_set(
|
||||
SystemSet::new()
|
||||
.with_run_criteria(FixedTimestep::step(10.0))
|
||||
.with_system(switch_scene),
|
||||
)
|
||||
.add_system(setup_scene_once_loaded)
|
||||
.add_system(gltf_animation_driver)
|
||||
.run();
|
||||
}
|
||||
|
||||
struct Example {
|
||||
model_name: &'static str,
|
||||
camera_transform: Transform,
|
||||
speed: f32,
|
||||
}
|
||||
impl Example {
|
||||
const fn new(model_name: &'static str, camera_transform: Transform, speed: f32) -> Self {
|
||||
Self {
|
||||
model_name,
|
||||
camera_transform,
|
||||
speed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const ANIMATIONS: [(&str, Transform, f32); 3] = [
|
||||
const ANIMATIONS: [Example; 3] = [
|
||||
// https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedTriangle
|
||||
Example::new(
|
||||
"models/animated/AnimatedTriangle.gltf",
|
||||
Transform {
|
||||
translation: const_vec3!([0.0, 0.0, 3.0]),
|
||||
rotation: const_quat!([0.0, 0.0, 0.0, 1.0]),
|
||||
scale: const_vec3!([1.0; 3]),
|
||||
},
|
||||
0.12,
|
||||
),
|
||||
// https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/BoxAnimated
|
||||
Example::new(
|
||||
"models/animated/BoxAnimated.gltf",
|
||||
Transform {
|
||||
translation: const_vec3!([4.0, 2.0, 4.0]),
|
||||
rotation: const_quat!([-0.08, 0.38, 0.03, 0.92]),
|
||||
scale: const_vec3!([1.0; 3]),
|
||||
},
|
||||
0.4,
|
||||
),
|
||||
Example::new(
|
||||
"models/animated/animations.gltf",
|
||||
Transform {
|
||||
translation: const_vec3!([-10.0, 5.0, -3.0]),
|
||||
rotation: const_quat!([0.16, 0.69, 0.16, -0.69]),
|
||||
scale: const_vec3!([1.0; 3]),
|
||||
},
|
||||
2.5,
|
||||
),
|
||||
];
|
||||
|
||||
struct CurrentScene {
|
||||
instance_id: InstanceId,
|
||||
animation: Handle<GltfAnimation>,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
struct CurrentAnimation {
|
||||
start_time: f64,
|
||||
animation: GltfAnimation,
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut scene_spawner: ResMut<SceneSpawner>,
|
||||
) {
|
||||
// Insert a resource with the current scene information
|
||||
commands.insert_resource(CurrentScene {
|
||||
// Its instance id, to be able to check that it's loaded
|
||||
instance_id: scene_spawner
|
||||
.spawn(asset_server.load(&format!("{}#Scene0", ANIMATIONS[0].model_name))),
|
||||
// The handle to the first animation
|
||||
animation: asset_server.load(&format!("{}#Animation0", ANIMATIONS[0].model_name)),
|
||||
// The animation speed modifier
|
||||
speed: ANIMATIONS[0].speed,
|
||||
});
|
||||
|
||||
// Add a camera
|
||||
commands.spawn_bundle(PerspectiveCameraBundle {
|
||||
transform: ANIMATIONS[0].camera_transform,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Add a directional light
|
||||
commands.spawn_bundle(DirectionalLightBundle::default());
|
||||
}
|
||||
|
||||
// Switch the scene to the next one every 10 seconds
|
||||
fn switch_scene(
|
||||
mut commands: Commands,
|
||||
scene_root: Query<Entity, (Without<Camera>, Without<DirectionalLight>, Without<Parent>)>,
|
||||
mut camera: Query<&mut Transform, With<Camera>>,
|
||||
mut current: Local<usize>,
|
||||
mut current_scene: ResMut<CurrentScene>,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut scene_spawner: ResMut<SceneSpawner>,
|
||||
) {
|
||||
*current = (*current + 1) % ANIMATIONS.len();
|
||||
|
||||
// Despawn the existing scene, then start loading the next one
|
||||
commands.entity(scene_root.single()).despawn_recursive();
|
||||
current_scene.instance_id = scene_spawner
|
||||
.spawn(asset_server.load(&format!("{}#Scene0", ANIMATIONS[*current].model_name)));
|
||||
current_scene.animation =
|
||||
asset_server.load(&format!("{}#Animation0", ANIMATIONS[*current].model_name));
|
||||
current_scene.speed = ANIMATIONS[*current].speed;
|
||||
|
||||
// Update the camera position
|
||||
*camera.single_mut() = ANIMATIONS[*current].camera_transform;
|
||||
|
||||
// Reset the current animation
|
||||
commands.remove_resource::<CurrentAnimation>();
|
||||
}
|
||||
|
||||
// Setup the scene for animation once it is loaded, by adding the animation to a resource with
|
||||
// the start time.
|
||||
fn setup_scene_once_loaded(
|
||||
mut commands: Commands,
|
||||
scene_spawner: Res<SceneSpawner>,
|
||||
current_scene: Res<CurrentScene>,
|
||||
time: Res<Time>,
|
||||
mut done: Local<bool>,
|
||||
animations: Res<Assets<GltfAnimation>>,
|
||||
) {
|
||||
// If the current scene resource has changed, start waiting for it to be loaded
|
||||
if current_scene.is_changed() {
|
||||
*done = false;
|
||||
}
|
||||
// Once the scene and the animation are loaded, start the animation
|
||||
if !*done && scene_spawner.instance_is_ready(current_scene.instance_id) {
|
||||
if let Some(animation) = animations.get(¤t_scene.animation) {
|
||||
*done = true;
|
||||
commands.insert_resource(CurrentAnimation {
|
||||
start_time: time.seconds_since_startup(),
|
||||
animation: animation.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This animation driver is not made to work in the general case. It will work with only one
|
||||
// animation per scene, and will ignore the specified interpolation method to only do linear.
|
||||
fn gltf_animation_driver(
|
||||
mut animated: Query<(&mut Transform, &GltfAnimatedNode)>,
|
||||
current_animation: Option<Res<CurrentAnimation>>,
|
||||
current_scene: Res<CurrentScene>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
if let Some(current_animation) = current_animation {
|
||||
let elapsed = (time.seconds_since_startup() - current_animation.start_time) as f32
|
||||
* current_scene.speed;
|
||||
for (mut transform, node) in animated.iter_mut() {
|
||||
let node_animations = current_animation
|
||||
.animation
|
||||
.node_animations
|
||||
.get(&node.index)
|
||||
.unwrap();
|
||||
for node_animation in node_animations.iter() {
|
||||
let mut keyframe_timestamps = node_animation.keyframe_timestamps.iter().enumerate();
|
||||
let mut step_start = keyframe_timestamps.next().unwrap();
|
||||
if elapsed < *step_start.1 {
|
||||
continue;
|
||||
}
|
||||
for next in keyframe_timestamps {
|
||||
if *next.1 > elapsed {
|
||||
break;
|
||||
}
|
||||
step_start = next;
|
||||
}
|
||||
if step_start.0 == node_animation.keyframe_timestamps.len() - 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let step_end = node_animation.keyframe_timestamps[step_start.0 + 1];
|
||||
let lerp = (elapsed - *step_start.1) / (step_end - step_start.1);
|
||||
match &node_animation.keyframes {
|
||||
GltfNodeAnimationKeyframes::Rotation(keyframes) => {
|
||||
let rot_start = keyframes[step_start.0];
|
||||
let mut rot_end = keyframes[step_start.0 + 1];
|
||||
if rot_end.dot(rot_start) < 0.0 {
|
||||
rot_end = -rot_end;
|
||||
}
|
||||
let result = Quat::from_array(rot_start.normalize().into())
|
||||
.slerp(Quat::from_array(rot_end.normalize().into()), lerp);
|
||||
transform.rotation = result;
|
||||
}
|
||||
GltfNodeAnimationKeyframes::Translation(keyframes) => {
|
||||
let translation_start = keyframes[step_start.0];
|
||||
let translation_end = keyframes[step_start.0 + 1];
|
||||
let result = translation_start.lerp(translation_end, lerp);
|
||||
transform.translation = result;
|
||||
}
|
||||
GltfNodeAnimationKeyframes::Scale(keyframes) => {
|
||||
let scale_start = keyframes[step_start.0];
|
||||
let scale_end = keyframes[step_start.0 + 1];
|
||||
let result = scale_start.lerp(scale_end, lerp);
|
||||
transform.scale = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -105,7 +105,6 @@ Example | File | Description
|
|||
`3d_scene` | [`3d/3d_scene.rs`](./3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting
|
||||
`lighting` | [`3d/lighting.rs`](./3d/lighting.rs) | Illustrates various lighting options in a simple scene
|
||||
`load_gltf` | [`3d/load_gltf.rs`](./3d/load_gltf.rs) | Loads and renders a gltf file as a scene
|
||||
`manual_gltf_animation_player` | [`3d/manual_gltf_animation_player.rs`](./3d/manual_gltf_animation_player.rs) | Loads and manually renders a gltf file with animations
|
||||
`many_cubes` | [`3d/many_cubes.rs`](./3d/many_cubes.rs) | Simple benchmark to test per-entity draw overhead
|
||||
`msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges
|
||||
`orthographic` | [`3d/orthographic.rs`](./3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look games or CAD applications)
|
||||
|
@ -123,6 +122,7 @@ Example | File | Description
|
|||
|
||||
Example | File | Description
|
||||
--- | --- | ---
|
||||
`animated_fox` | [`animation/animated_fox.rs`](./animation/animated_fox.rs) | Plays an animation from a skinned glTF.
|
||||
`custom_skinned_mesh` | [`animation/custom_skinned_mesh.rs`](./animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code.
|
||||
`gltf_skinned_mesh` | [`animation/gltf_skinned_mesh.rs`](./animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file.
|
||||
|
||||
|
|
127
examples/animation/animated_fox.rs
Normal file
127
examples/animation/animated_fox.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 1.0,
|
||||
})
|
||||
.add_startup_system(setup)
|
||||
.add_system(setup_scene_once_loaded)
|
||||
.add_system(keyboard_animation_control)
|
||||
.run();
|
||||
}
|
||||
|
||||
struct Animations(Vec<Handle<AnimationClip>>);
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut scene_spawner: ResMut<SceneSpawner>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
// Insert a resource with the current scene information
|
||||
commands.insert_resource(Animations(vec![
|
||||
asset_server.load("models/animated/Fox.glb#Animation2"),
|
||||
asset_server.load("models/animated/Fox.glb#Animation1"),
|
||||
asset_server.load("models/animated/Fox.glb#Animation0"),
|
||||
]));
|
||||
|
||||
// Camera
|
||||
commands.spawn_bundle(PerspectiveCameraBundle {
|
||||
transform: Transform::from_xyz(100.0, 100.0, 150.0)
|
||||
.looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Plane
|
||||
commands.spawn_bundle(PbrBundle {
|
||||
mesh: meshes.add(Mesh::from(shape::Plane { size: 500000.0 })),
|
||||
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
|
||||
..default()
|
||||
});
|
||||
|
||||
// Light
|
||||
commands.spawn_bundle(DirectionalLightBundle {
|
||||
transform: Transform::from_rotation(Quat::from_euler(
|
||||
EulerRot::ZYX,
|
||||
0.0,
|
||||
1.0,
|
||||
-std::f32::consts::FRAC_PI_4,
|
||||
)),
|
||||
directional_light: DirectionalLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
|
||||
// Fox
|
||||
scene_spawner.spawn(asset_server.load("models/animated/Fox.glb#Scene0"));
|
||||
|
||||
println!("Animation controls:");
|
||||
println!(" - spacebar: play / pause");
|
||||
println!(" - arrow up / down: speed up / slow down animation playback");
|
||||
println!(" - arrow left / right: seek backward / forward");
|
||||
println!(" - return: change animation");
|
||||
}
|
||||
|
||||
// Once the scene is loaded, start the animation
|
||||
fn setup_scene_once_loaded(
|
||||
animations: Res<Animations>,
|
||||
mut player: Query<&mut AnimationPlayer>,
|
||||
mut done: Local<bool>,
|
||||
) {
|
||||
if !*done {
|
||||
if let Ok(mut player) = player.get_single_mut() {
|
||||
player.play(animations.0[0].clone_weak()).repeat();
|
||||
*done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_animation_control(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut animation_player: Query<&mut AnimationPlayer>,
|
||||
animations: Res<Animations>,
|
||||
mut current_animation: Local<usize>,
|
||||
) {
|
||||
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 keyboard_input.just_pressed(KeyCode::Up) {
|
||||
let speed = player.speed();
|
||||
player.set_speed(speed * 1.2);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Down) {
|
||||
let speed = player.speed();
|
||||
player.set_speed(speed * 0.8);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Left) {
|
||||
let elapsed = player.elapsed();
|
||||
player.set_elapsed(elapsed - 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Right) {
|
||||
let elapsed = player.elapsed();
|
||||
player.set_elapsed(elapsed + 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Return) {
|
||||
*current_animation = (*current_animation + 1) % animations.0.len();
|
||||
player
|
||||
.play(animations.0[*current_animation].clone_weak())
|
||||
.repeat();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue