diff --git a/Cargo.toml b/Cargo.toml index 40ef75724c..74927f5531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,10 @@ path = "examples/3d/wireframe.rs" name = "animated_fox" path = "examples/animation/animated_fox.rs" +[[example]] +name = "animated_transform" +path = "examples/animation/animated_transform.rs" + [[example]] name = "custom_skinned_mesh" path = "examples/animation/custom_skinned_mesh.rs" diff --git a/examples/README.md b/examples/README.md index d560eddd27..8ad65c7b04 100644 --- a/examples/README.md +++ b/examples/README.md @@ -123,6 +123,7 @@ Example | File | Description Example | File | Description --- | --- | --- `animated_fox` | [`animation/animated_fox.rs`](./animation/animated_fox.rs) | Plays an animation from a skinned glTF. +`animated_transform` | [`animation/animated_transform.rs`](./animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component. `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. diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs new file mode 100644 index 0000000000..5084253615 --- /dev/null +++ b/examples/animation/animated_transform.rs @@ -0,0 +1,141 @@ +use std::f32::consts::{FRAC_PI_2, PI}; + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 1.0, + }) + .add_startup_system(setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut animations: ResMut>, +) { + // Camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // The animation API uses the `Name` component to target entities + let planet = Name::new("planet"); + let orbit_controller = Name::new("orbit_controller"); + let satellite = Name::new("satellite"); + + // Creating the animation + let mut animation = AnimationClip::default(); + // A curve can modify a single part of a transform, here the translation + animation.add_curve_to_path( + EntityPath { + parts: vec![planet.clone()], + }, + VariableCurve { + keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], + keyframes: Keyframes::Translation(vec![ + Vec3::new(1.0, 0.0, 1.0), + Vec3::new(-1.0, 0.0, 1.0), + Vec3::new(-1.0, 0.0, -1.0), + Vec3::new(1.0, 0.0, -1.0), + // in case seamless looping is wanted, the last keyframe should + // be the same as the first one + Vec3::new(1.0, 0.0, 1.0), + ]), + }, + ); + // Or it can modify the rotation of the transform. + // To find the entity to modify, the hierarchy will be traversed looking for + // an entity with the right name at each level + animation.add_curve_to_path( + EntityPath { + parts: vec![planet.clone(), orbit_controller.clone()], + }, + VariableCurve { + keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], + keyframes: Keyframes::Rotation(vec![ + Quat::from_axis_angle(Vec3::Y, 0.0), + Quat::from_axis_angle(Vec3::Y, FRAC_PI_2), + Quat::from_axis_angle(Vec3::Y, PI), + Quat::from_axis_angle(Vec3::Y, 3.0 * FRAC_PI_2), + Quat::from_axis_angle(Vec3::Y, 0.0), + ]), + }, + ); + // If a curve in an animation is shorter than the other, it will not repeat + // until all other curves are finished. In that case, another animation should + // be created for each part that would have a different duration / period + animation.add_curve_to_path( + EntityPath { + parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()], + }, + VariableCurve { + keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], + keyframes: Keyframes::Scale(vec![ + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + ]), + }, + ); + // There can be more than one curve targeting the same entity path + animation.add_curve_to_path( + EntityPath { + parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()], + }, + VariableCurve { + keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0], + keyframes: Keyframes::Rotation(vec![ + Quat::from_axis_angle(Vec3::Y, 0.0), + Quat::from_axis_angle(Vec3::Y, FRAC_PI_2), + Quat::from_axis_angle(Vec3::Y, PI), + Quat::from_axis_angle(Vec3::Y, 3.0 * FRAC_PI_2), + Quat::from_axis_angle(Vec3::Y, 0.0), + ]), + }, + ); + + // Create the animation player, and set it to repeat + let mut player = AnimationPlayer::default(); + player.play(animations.add(animation)).repeat(); + + // Create the scene that will be animated + // First entity is the planet + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Icosphere::default())), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + ..default() + }) + // Add the Name component, and the animation player + .insert_bundle((planet, player)) + .with_children(|p| { + // This entity is just used for animation, but doesn't display anything + p.spawn_bundle(TransformBundle { ..default() }) + // Add the Name component + .insert(orbit_controller) + .with_children(|p| { + // The satellite, placed at a distance of the planet + p.spawn_bundle(PbrBundle { + transform: Transform::from_xyz(1.5, 0.0, 0.0), + mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })), + material: materials.add(Color::rgb(0.3, 0.9, 0.3).into()), + ..default() + }) + // Add the Name component + .insert(satellite); + }); + }); +}