animated_transform/
animated_transform.rs

1//! Create and play an animation defined by code that operates on the [`Transform`] component.
2
3use std::f32::consts::PI;
4
5use bevy::{
6    animation::{animated_field, AnimationTarget, AnimationTargetId},
7    prelude::*,
8};
9
10fn main() {
11    App::new()
12        .add_plugins(DefaultPlugins)
13        .insert_resource(AmbientLight {
14            color: Color::WHITE,
15            brightness: 150.0,
16        })
17        .add_systems(Startup, setup)
18        .run();
19}
20
21fn setup(
22    mut commands: Commands,
23    mut meshes: ResMut<Assets<Mesh>>,
24    mut materials: ResMut<Assets<StandardMaterial>>,
25    mut animations: ResMut<Assets<AnimationClip>>,
26    mut graphs: ResMut<Assets<AnimationGraph>>,
27) {
28    // Camera
29    commands.spawn((
30        Camera3d::default(),
31        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
32    ));
33
34    // Light
35    commands.spawn((
36        PointLight {
37            intensity: 500_000.0,
38            ..default()
39        },
40        Transform::from_xyz(0.0, 2.5, 0.0),
41    ));
42
43    // Let's use the `Name` component to target entities. We can use anything we
44    // like, but names are convenient.
45    let planet = Name::new("planet");
46    let orbit_controller = Name::new("orbit_controller");
47    let satellite = Name::new("satellite");
48
49    // Creating the animation
50    let mut animation = AnimationClip::default();
51    // A curve can modify a single part of a transform: here, the translation.
52    let planet_animation_target_id = AnimationTargetId::from_name(&planet);
53    animation.add_curve_to_target(
54        planet_animation_target_id,
55        AnimatableCurve::new(
56            animated_field!(Transform::translation),
57            UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
58                Vec3::new(1.0, 0.0, 1.0),
59                Vec3::new(-1.0, 0.0, 1.0),
60                Vec3::new(-1.0, 0.0, -1.0),
61                Vec3::new(1.0, 0.0, -1.0),
62                // in case seamless looping is wanted, the last keyframe should
63                // be the same as the first one
64                Vec3::new(1.0, 0.0, 1.0),
65            ]))
66            .expect("should be able to build translation curve because we pass in valid samples"),
67        ),
68    );
69    // Or it can modify the rotation of the transform.
70    // To find the entity to modify, the hierarchy will be traversed looking for
71    // an entity with the right name at each level.
72    let orbit_controller_animation_target_id =
73        AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter());
74    animation.add_curve_to_target(
75        orbit_controller_animation_target_id,
76        AnimatableCurve::new(
77            animated_field!(Transform::rotation),
78            UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
79                Quat::IDENTITY,
80                Quat::from_axis_angle(Vec3::Y, PI / 2.),
81                Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
82                Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
83                Quat::IDENTITY,
84            ]))
85            .expect("Failed to build rotation curve"),
86        ),
87    );
88    // If a curve in an animation is shorter than the other, it will not repeat
89    // until all other curves are finished. In that case, another animation should
90    // be created for each part that would have a different duration / period.
91    let satellite_animation_target_id = AnimationTargetId::from_names(
92        [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(),
93    );
94    animation.add_curve_to_target(
95        satellite_animation_target_id,
96        AnimatableCurve::new(
97            animated_field!(Transform::scale),
98            UnevenSampleAutoCurve::new(
99                [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]
100                    .into_iter()
101                    .zip([
102                        Vec3::splat(0.8),
103                        Vec3::splat(1.2),
104                        Vec3::splat(0.8),
105                        Vec3::splat(1.2),
106                        Vec3::splat(0.8),
107                        Vec3::splat(1.2),
108                        Vec3::splat(0.8),
109                        Vec3::splat(1.2),
110                        Vec3::splat(0.8),
111                    ]),
112            )
113            .expect("Failed to build scale curve"),
114        ),
115    );
116    // There can be more than one curve targeting the same entity path.
117    animation.add_curve_to_target(
118        AnimationTargetId::from_names(
119            [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(),
120        ),
121        AnimatableCurve::new(
122            animated_field!(Transform::rotation),
123            UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
124                Quat::IDENTITY,
125                Quat::from_axis_angle(Vec3::Y, PI / 2.),
126                Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
127                Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
128                Quat::IDENTITY,
129            ]))
130            .expect("should be able to build translation curve because we pass in valid samples"),
131        ),
132    );
133
134    // Create the animation graph
135    let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));
136
137    // Create the animation player, and set it to repeat
138    let mut player = AnimationPlayer::default();
139    player.play(animation_index).repeat();
140
141    // Create the scene that will be animated
142    // First entity is the planet
143    let planet_entity = commands
144        .spawn((
145            Mesh3d(meshes.add(Sphere::default())),
146            MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
147            // Add the animation graph and player
148            planet,
149            AnimationGraphHandle(graphs.add(graph)),
150            player,
151        ))
152        .id();
153    commands
154        .entity(planet_entity)
155        .insert(AnimationTarget {
156            id: planet_animation_target_id,
157            player: planet_entity,
158        })
159        .with_children(|p| {
160            // This entity is just used for animation, but doesn't display anything
161            p.spawn((
162                Transform::default(),
163                Visibility::default(),
164                orbit_controller,
165                AnimationTarget {
166                    id: orbit_controller_animation_target_id,
167                    player: planet_entity,
168                },
169            ))
170            .with_children(|p| {
171                // The satellite, placed at a distance of the planet
172                p.spawn((
173                    Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))),
174                    MeshMaterial3d(materials.add(Color::srgb(0.3, 0.9, 0.3))),
175                    Transform::from_xyz(1.5, 0.0, 0.0),
176                    AnimationTarget {
177                        id: satellite_animation_target_id,
178                        player: planet_entity,
179                    },
180                    satellite,
181                ));
182            });
183        });
184}