mirror of
https://github.com/bevyengine/bevy
synced 2024-11-14 00:47:32 +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)
|
* 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)
|
* 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)
|
* 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 animated fox from [glTF Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox)
|
||||||
* 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))
|
* 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]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
|
"animation",
|
||||||
"bevy_audio",
|
"bevy_audio",
|
||||||
"bevy_gilrs",
|
"bevy_gilrs",
|
||||||
"bevy_winit",
|
"bevy_winit",
|
||||||
|
@ -43,6 +44,7 @@ render = [
|
||||||
]
|
]
|
||||||
|
|
||||||
# Optional bevy crates
|
# Optional bevy crates
|
||||||
|
bevy_animation = ["bevy_internal/bevy_animation"]
|
||||||
bevy_audio = ["bevy_internal/bevy_audio"]
|
bevy_audio = ["bevy_internal/bevy_audio"]
|
||||||
bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"]
|
bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"]
|
||||||
bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"]
|
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
|
# Enable the "debug asset server" for hot reloading internal assets
|
||||||
debug_asset_server = ["bevy_internal/debug_asset_server"]
|
debug_asset_server = ["bevy_internal/debug_asset_server"]
|
||||||
|
|
||||||
|
# Enable animation support, and glTF animation loading
|
||||||
|
animation = ["bevy_internal/animation"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.7.0-dev", default-features = false, optional = true }
|
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 }
|
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"
|
name = "load_gltf"
|
||||||
path = "examples/3d/load_gltf.rs"
|
path = "examples/3d/load_gltf.rs"
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "manual_gltf_animation_player"
|
|
||||||
path = "examples/3d/manual_gltf_animation_player.rs"
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "many_cubes"
|
name = "many_cubes"
|
||||||
path = "examples/3d/many_cubes.rs"
|
path = "examples/3d/many_cubes.rs"
|
||||||
|
@ -236,6 +237,10 @@ name = "wireframe"
|
||||||
path = "examples/3d/wireframe.rs"
|
path = "examples/3d/wireframe.rs"
|
||||||
|
|
||||||
# Animation
|
# Animation
|
||||||
|
[[example]]
|
||||||
|
name = "animated_fox"
|
||||||
|
path = "examples/animation/animated_fox.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "custom_skinned_mesh"
|
name = "custom_skinned_mesh"
|
||||||
path = "examples/animation/custom_skinned_mesh.rs"
|
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]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
|
bevy_animation = { path = "../bevy_animation", version = "0.7.0-dev", optional = true }
|
||||||
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
|
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
|
||||||
bevy_asset = { path = "../bevy_asset", 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_core = { path = "../bevy_core", version = "0.7.0-dev" }
|
||||||
bevy_ecs = { path = "../bevy_ecs", 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_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_pbr = { path = "../bevy_pbr", version = "0.7.0-dev" }
|
||||||
bevy_reflect = { path = "../bevy_reflect", version = "0.7.0-dev", features = ["bevy"] }
|
bevy_reflect = { path = "../bevy_reflect", version = "0.7.0-dev", features = ["bevy"] }
|
||||||
bevy_render = { path = "../bevy_render", version = "0.7.0-dev" }
|
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_transform = { path = "../bevy_transform", version = "0.7.0-dev" }
|
||||||
bevy_utils = { path = "../bevy_utils", 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
|
# other
|
||||||
gltf = { version = "1.0.0", default-features = false, features = [
|
gltf = { version = "1.0.0", default-features = false, features = [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
#[cfg(feature = "bevy_animation")]
|
||||||
use bevy_math::{Quat, Vec3};
|
use bevy_animation::AnimationClip;
|
||||||
use bevy_utils::HashMap;
|
use bevy_utils::HashMap;
|
||||||
|
|
||||||
mod loader;
|
mod loader;
|
||||||
|
@ -8,7 +8,7 @@ pub use loader::*;
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_asset::{AddAsset, Handle};
|
use bevy_asset::{AddAsset, Handle};
|
||||||
use bevy_pbr::StandardMaterial;
|
use bevy_pbr::StandardMaterial;
|
||||||
use bevy_reflect::{Reflect, TypeUuid};
|
use bevy_reflect::TypeUuid;
|
||||||
use bevy_render::mesh::Mesh;
|
use bevy_render::mesh::Mesh;
|
||||||
use bevy_scene::Scene;
|
use bevy_scene::Scene;
|
||||||
|
|
||||||
|
@ -22,9 +22,7 @@ impl Plugin for GltfPlugin {
|
||||||
.add_asset::<Gltf>()
|
.add_asset::<Gltf>()
|
||||||
.add_asset::<GltfNode>()
|
.add_asset::<GltfNode>()
|
||||||
.add_asset::<GltfPrimitive>()
|
.add_asset::<GltfPrimitive>()
|
||||||
.add_asset::<GltfMesh>()
|
.add_asset::<GltfMesh>();
|
||||||
.add_asset::<GltfAnimation>()
|
|
||||||
.register_type::<GltfAnimatedNode>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,8 +39,10 @@ pub struct Gltf {
|
||||||
pub nodes: Vec<Handle<GltfNode>>,
|
pub nodes: Vec<Handle<GltfNode>>,
|
||||||
pub named_nodes: HashMap<String, Handle<GltfNode>>,
|
pub named_nodes: HashMap<String, Handle<GltfNode>>,
|
||||||
pub default_scene: Option<Handle<Scene>>,
|
pub default_scene: Option<Handle<Scene>>,
|
||||||
pub animations: Vec<Handle<GltfAnimation>>,
|
#[cfg(feature = "bevy_animation")]
|
||||||
pub named_animations: HashMap<String, Handle<GltfAnimation>>,
|
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
|
/// A glTF node with all of its child nodes, its [`GltfMesh`] and
|
||||||
|
@ -69,52 +69,3 @@ pub struct GltfPrimitive {
|
||||||
pub mesh: Handle<Mesh>,
|
pub mesh: Handle<Mesh>,
|
||||||
pub material: Option<Handle<StandardMaterial>>,
|
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;
|
use anyhow::Result;
|
||||||
|
#[cfg(feature = "bevy_animation")]
|
||||||
|
use bevy_animation::{AnimationClip, AnimationPlayer, EntityPath, Keyframes, VariableCurve};
|
||||||
use bevy_asset::{
|
use bevy_asset::{
|
||||||
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
|
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
|
||||||
};
|
};
|
||||||
|
@ -33,15 +35,12 @@ use bevy_utils::{HashMap, HashSet};
|
||||||
use gltf::{
|
use gltf::{
|
||||||
mesh::Mode,
|
mesh::Mode,
|
||||||
texture::{MagFilter, MinFilter, WrappingMode},
|
texture::{MagFilter, MinFilter, WrappingMode},
|
||||||
Material, Primitive,
|
Material, Node, Primitive,
|
||||||
};
|
};
|
||||||
use std::{collections::VecDeque, path::Path};
|
use std::{collections::VecDeque, path::Path};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{Gltf, GltfNode};
|
||||||
Gltf, GltfAnimatedNode, GltfAnimation, GltfAnimationInterpolation, GltfNode, GltfNodeAnimation,
|
|
||||||
GltfNodeAnimationKeyframes,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An error that occurs when loading a glTF file.
|
/// An error that occurs when loading a glTF file.
|
||||||
#[derive(Error, Debug)]
|
#[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 animations = vec![];
|
||||||
let mut named_animations = HashMap::default();
|
let mut named_animations = HashMap::default();
|
||||||
let mut animated_nodes = HashSet::default();
|
|
||||||
for animation in gltf.animations() {
|
for animation in gltf.animations() {
|
||||||
let mut gltf_animation = GltfAnimation::default();
|
let mut animation_clip = AnimationClip::default();
|
||||||
for channel in animation.channels() {
|
for channel in animation.channels() {
|
||||||
let interpolation = match channel.sampler().interpolation() {
|
match channel.sampler().interpolation() {
|
||||||
gltf::animation::Interpolation::Linear => GltfAnimationInterpolation::Linear,
|
gltf::animation::Interpolation::Linear => (),
|
||||||
gltf::animation::Interpolation::Step => GltfAnimationInterpolation::Step,
|
other => warn!(
|
||||||
gltf::animation::Interpolation::CubicSpline => {
|
"Animation interpolation {:?} is not supported, will use linear",
|
||||||
GltfAnimationInterpolation::CubicSpline
|
other
|
||||||
}
|
),
|
||||||
};
|
};
|
||||||
let node = channel.target().node();
|
let node = channel.target().node();
|
||||||
let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
|
let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
|
||||||
|
@ -148,27 +159,25 @@ async fn load_gltf<'a, 'b>(
|
||||||
match inputs {
|
match inputs {
|
||||||
gltf::accessor::Iter::Standard(times) => times.collect(),
|
gltf::accessor::Iter::Standard(times) => times.collect(),
|
||||||
gltf::accessor::Iter::Sparse(_) => {
|
gltf::accessor::Iter::Sparse(_) => {
|
||||||
warn!("sparse accessor not supported for animation sampler input");
|
warn!("Sparse accessor not supported for animation sampler input");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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()));
|
return Err(GltfError::MissingAnimationSampler(animation.index()));
|
||||||
};
|
};
|
||||||
|
|
||||||
let keyframes = if let Some(outputs) = reader.read_outputs() {
|
let keyframes = if let Some(outputs) = reader.read_outputs() {
|
||||||
match outputs {
|
match outputs {
|
||||||
gltf::animation::util::ReadOutputs::Translations(tr) => {
|
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) => {
|
gltf::animation::util::ReadOutputs::Rotations(rots) => {
|
||||||
GltfNodeAnimationKeyframes::Rotation(
|
Keyframes::Rotation(rots.into_f32().map(Quat::from_array).collect())
|
||||||
rots.into_f32().map(Quat::from_array).collect(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
gltf::animation::util::ReadOutputs::Scales(scale) => {
|
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(_) => {
|
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
|
||||||
warn!("Morph animation property not yet supported");
|
warn!("Morph animation property not yet supported");
|
||||||
|
@ -176,30 +185,38 @@ async fn load_gltf<'a, 'b>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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()));
|
return Err(GltfError::MissingAnimationSampler(animation.index()));
|
||||||
};
|
};
|
||||||
|
|
||||||
gltf_animation
|
if let Some(path) = paths.get(&node.index()) {
|
||||||
.node_animations
|
animation_clip.add_curve_to_path(
|
||||||
.entry(node.index())
|
EntityPath {
|
||||||
.or_default()
|
parts: path.clone(),
|
||||||
.push(GltfNodeAnimation {
|
},
|
||||||
|
VariableCurve {
|
||||||
keyframe_timestamps,
|
keyframe_timestamps,
|
||||||
keyframes,
|
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(
|
let handle = load_context.set_labeled_asset(
|
||||||
&format!("Animation{}", animation.index()),
|
&format!("Animation{}", animation.index()),
|
||||||
LoadedAsset::new(gltf_animation),
|
LoadedAsset::new(animation_clip),
|
||||||
);
|
);
|
||||||
if let Some(name) = animation.name() {
|
if let Some(name) = animation.name() {
|
||||||
named_animations.insert(name.to_string(), handle.clone());
|
named_animations.insert(name.to_string(), handle.clone());
|
||||||
}
|
}
|
||||||
animations.push(handle);
|
animations.push(handle);
|
||||||
}
|
}
|
||||||
|
(animations, named_animations)
|
||||||
|
};
|
||||||
|
|
||||||
let mut meshes = vec![];
|
let mut meshes = vec![];
|
||||||
let mut named_meshes = HashMap::default();
|
let mut named_meshes = HashMap::default();
|
||||||
|
@ -436,7 +453,6 @@ async fn load_gltf<'a, 'b>(
|
||||||
parent,
|
parent,
|
||||||
load_context,
|
load_context,
|
||||||
&buffer_data,
|
&buffer_data,
|
||||||
&animated_nodes,
|
|
||||||
&mut node_index_to_entity_map,
|
&mut node_index_to_entity_map,
|
||||||
&mut entity_to_skin_index_map,
|
&mut entity_to_skin_index_map,
|
||||||
);
|
);
|
||||||
|
@ -450,6 +466,13 @@ async fn load_gltf<'a, 'b>(
|
||||||
return Err(err);
|
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 {
|
for (&entity, &skin_index) in &entity_to_skin_index_map {
|
||||||
let mut entity = world.entity_mut(entity);
|
let mut entity = world.entity_mut(entity);
|
||||||
let skin = gltf.skins().nth(skin_index).unwrap();
|
let skin = gltf.skins().nth(skin_index).unwrap();
|
||||||
|
@ -486,13 +509,26 @@ async fn load_gltf<'a, 'b>(
|
||||||
named_materials,
|
named_materials,
|
||||||
nodes,
|
nodes,
|
||||||
named_nodes,
|
named_nodes,
|
||||||
|
#[cfg(feature = "bevy_animation")]
|
||||||
animations,
|
animations,
|
||||||
|
#[cfg(feature = "bevy_animation")]
|
||||||
named_animations,
|
named_animations,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Ok(())
|
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.
|
/// Loads a glTF texture as a bevy [`Image`] and returns it together with its label.
|
||||||
async fn load_texture<'a>(
|
async fn load_texture<'a>(
|
||||||
gltf_texture: gltf::Texture<'a>,
|
gltf_texture: gltf::Texture<'a>,
|
||||||
|
@ -633,7 +669,6 @@ fn load_node(
|
||||||
world_builder: &mut WorldChildBuilder,
|
world_builder: &mut WorldChildBuilder,
|
||||||
load_context: &mut LoadContext,
|
load_context: &mut LoadContext,
|
||||||
buffer_data: &[Vec<u8>],
|
buffer_data: &[Vec<u8>],
|
||||||
animated_nodes: &HashSet<usize>,
|
|
||||||
node_index_to_entity_map: &mut HashMap<usize, Entity>,
|
node_index_to_entity_map: &mut HashMap<usize, Entity>,
|
||||||
entity_to_skin_index_map: &mut HashMap<Entity, usize>,
|
entity_to_skin_index_map: &mut HashMap<Entity, usize>,
|
||||||
) -> Result<(), GltfError> {
|
) -> Result<(), GltfError> {
|
||||||
|
@ -643,12 +678,6 @@ fn load_node(
|
||||||
Mat4::from_cols_array_2d(&transform.matrix()),
|
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() {
|
if let Some(name) = gltf_node.name() {
|
||||||
node.insert(Name::new(name.to_string()));
|
node.insert(Name::new(name.to_string()));
|
||||||
}
|
}
|
||||||
|
@ -798,7 +827,6 @@ fn load_node(
|
||||||
parent,
|
parent,
|
||||||
load_context,
|
load_context,
|
||||||
buffer_data,
|
buffer_data,
|
||||||
animated_nodes,
|
|
||||||
node_index_to_entity_map,
|
node_index_to_entity_map,
|
||||||
entity_to_skin_index_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
|
# enable systems that allow for automated testing on CI
|
||||||
bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render/ci_limits"]
|
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]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
|
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_window = { path = "../bevy_window", version = "0.7.0-dev" }
|
||||||
bevy_tasks = { path = "../bevy_tasks", version = "0.7.0-dev" }
|
bevy_tasks = { path = "../bevy_tasks", version = "0.7.0-dev" }
|
||||||
# bevy (optional)
|
# 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_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_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" }
|
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.7.0-dev" }
|
||||||
|
|
|
@ -68,6 +68,9 @@ impl PluginGroup for DefaultPlugins {
|
||||||
|
|
||||||
#[cfg(feature = "bevy_gilrs")]
|
#[cfg(feature = "bevy_gilrs")]
|
||||||
group.add(bevy_gilrs::GilrsPlugin::default());
|
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::*;
|
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")]
|
#[cfg(feature = "bevy_audio")]
|
||||||
pub mod audio {
|
pub mod audio {
|
||||||
//! Provides types and plugins for audio playback.
|
//! Provides types and plugins for audio playback.
|
||||||
|
|
|
@ -11,6 +11,10 @@ pub use bevy_derive::{bevy_main, Deref, DerefMut};
|
||||||
#[cfg(feature = "bevy_audio")]
|
#[cfg(feature = "bevy_audio")]
|
||||||
pub use crate::audio::prelude::*;
|
pub use crate::audio::prelude::*;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[cfg(feature = "bevy_animation")]
|
||||||
|
pub use crate::animation::prelude::*;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[cfg(feature = "bevy_core_pipeline")]
|
#[cfg(feature = "bevy_core_pipeline")]
|
||||||
pub use crate::core_pipeline::prelude::*;
|
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
|
`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
|
`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
|
`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
|
`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
|
`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)
|
`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
|
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.
|
`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.
|
`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