Gltf animations (#3751)

# Objective

- Load informations for animations from GLTF
- Make experimenting on animations easier

# Non Objective

- Implement a solutions for all animations in Bevy. This would need a discussion / RFC. The goal here is only to have the information available to try different APIs

## Solution

- Load animations with a representation close to the GLTF spec
- Add an example to display animations. There is an animation driver in the example, not in Bevy code, to show how it can be used. The example is cycling between examples from the official gltf sample ([AnimatedTriangle](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedTriangle), [BoxAnimated](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/BoxAnimated)), and one from me with some cases not present in the official examples.


https://user-images.githubusercontent.com/8672791/150696656-073403f0-d921-43b6-beaf-099c7aee16ed.mp4




Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
François 2022-03-22 02:26:34 +00:00
parent 6c085cba47
commit fbe7a49d5b
9 changed files with 1422 additions and 6 deletions

View file

@ -21,3 +21,5 @@
* 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))

View file

@ -183,6 +183,10 @@ 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"

View file

@ -0,0 +1,118 @@
{
"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

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,5 @@
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_math::{Quat, Vec3};
use bevy_utils::HashMap;
mod loader;
@ -6,7 +8,7 @@ pub use loader::*;
use bevy_app::prelude::*;
use bevy_asset::{AddAsset, Handle};
use bevy_pbr::StandardMaterial;
use bevy_reflect::TypeUuid;
use bevy_reflect::{Reflect, TypeUuid};
use bevy_render::mesh::Mesh;
use bevy_scene::Scene;
@ -20,7 +22,9 @@ impl Plugin for GltfPlugin {
.add_asset::<Gltf>()
.add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>();
.add_asset::<GltfMesh>()
.add_asset::<GltfAnimation>()
.register_type::<GltfAnimatedNode>();
}
}
@ -37,6 +41,8 @@ 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>>,
}
/// A glTF node with all of its child nodes, its [`GltfMesh`] and
@ -63,3 +69,52 @@ 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,
}

View file

@ -6,7 +6,7 @@ use bevy_core::Name;
use bevy_ecs::{prelude::FromWorld, world::World};
use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder};
use bevy_log::warn;
use bevy_math::{Mat4, Vec3};
use bevy_math::{Mat4, Quat, Vec3};
use bevy_pbr::{
AlphaMode, DirectionalLight, DirectionalLightBundle, PbrBundle, PointLight, PointLightBundle,
StandardMaterial,
@ -35,7 +35,10 @@ use gltf::{
use std::{collections::VecDeque, path::Path};
use thiserror::Error;
use crate::{Gltf, GltfNode};
use crate::{
Gltf, GltfAnimatedNode, GltfAnimation, GltfAnimationInterpolation, GltfNode, GltfNodeAnimation,
GltfNodeAnimationKeyframes,
};
/// An error that occurs when loading a glTF file.
#[derive(Error, Debug)]
@ -56,6 +59,8 @@ pub enum GltfError {
ImageError(#[from] TextureError),
#[error("failed to load an asset path: {0}")]
AssetIoError(#[from] AssetIoError),
#[error("Missing sampler for animation {0}")]
MissingAnimationSampler(usize),
}
/// Loads glTF files with all of their data as their corresponding bevy representations.
@ -121,6 +126,78 @@ async fn load_gltf<'a, 'b>(
}
}
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();
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
}
};
let node = channel.target().node();
let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
let keyframe_timestamps: Vec<f32> = if let Some(inputs) = reader.read_inputs() {
match inputs {
gltf::accessor::Iter::Standard(times) => times.collect(),
gltf::accessor::Iter::Sparse(_) => {
warn!("sparse accessor not supported for animation sampler input");
continue;
}
}
} else {
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())
}
gltf::animation::util::ReadOutputs::Rotations(rots) => {
GltfNodeAnimationKeyframes::Rotation(
rots.into_f32().map(Quat::from_array).collect(),
)
}
gltf::animation::util::ReadOutputs::Scales(scale) => {
GltfNodeAnimationKeyframes::Scale(scale.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
warn!("Morph animation property not yet supported");
continue;
}
}
} else {
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 {
keyframe_timestamps,
keyframes,
interpolation,
});
animated_nodes.insert(node.index());
}
let handle = load_context.set_labeled_asset(
&format!("Animation{}", animation.index()),
LoadedAsset::new(gltf_animation),
);
if let Some(name) = animation.name() {
named_animations.insert(name.to_string(), handle.clone());
}
animations.push(handle);
}
let mut meshes = vec![];
let mut named_meshes = HashMap::default();
for mesh in gltf.meshes() {
@ -317,7 +394,8 @@ async fn load_gltf<'a, 'b>(
.insert_bundle(TransformBundle::identity())
.with_children(|parent| {
for node in scene.nodes() {
let result = load_node(&node, parent, load_context, &buffer_data);
let result =
load_node(&node, parent, load_context, &buffer_data, &animated_nodes);
if result.is_err() {
err = Some(result);
return;
@ -349,6 +427,8 @@ async fn load_gltf<'a, 'b>(
named_materials,
nodes,
named_nodes,
animations,
named_animations,
}));
Ok(())
@ -494,6 +574,7 @@ fn load_node(
world_builder: &mut WorldChildBuilder,
load_context: &mut LoadContext,
buffer_data: &[Vec<u8>],
animated_nodes: &HashSet<usize>,
) -> Result<(), GltfError> {
let transform = gltf_node.transform();
let mut gltf_error = None;
@ -501,6 +582,12 @@ 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()));
}
@ -636,7 +723,7 @@ fn load_node(
// append other nodes
for child in gltf_node.children() {
if let Err(err) = load_node(&child, parent, load_context, buffer_data) {
if let Err(err) = load_node(&child, parent, load_context, buffer_data, animated_nodes) {
gltf_error = Some(err);
return;
}

View file

@ -0,0 +1,230 @@
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(&current_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;
}
}
}
}
}
}

View file

@ -104,6 +104,7 @@ 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)