bevy_director 0.7.0-dev

Unreal-Sequencer-inspired cinematic camera and sequence system for Bevy
Documentation
//! A pillar canyon and a directed take through it, authored in code.
//!
//!     cargo run -p bevy_director --example flythrough
//!
//! Space plays the take (again). Esc skips it. The take carries its own
//! title card, captions, and a filmic pass (vignette, chromatic
//! aberration, a warming grade), and the director borrows this app's
//! only camera rather than swapping in one of its own — a plain game
//! build, no editor anywhere.

use bevy::prelude::*;
use bevy_director::prelude::*;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, DirectorPlugin))
        .add_systems(Startup, setup)
        .add_systems(Update, (play_on_space, skip_on_esc))
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let ground = materials.add(StandardMaterial {
        base_color: Color::srgb(0.22, 0.24, 0.28),
        ..Default::default()
    });
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::new(60.0, 1.0, 60.0))),
        MeshMaterial3d(ground.clone()),
        Transform::from_xyz(0.0, -0.5, 0.0),
    ));
    let pillar_mesh = meshes.add(Cuboid::new(1.6, 8.0, 1.6));
    for x in -3..=3 {
        for z in -3..=3 {
            if (x + z) % 2 == 0 {
                continue;
            }
            let height_seed = ((x * 7 + z * 13) % 5) as f32;
            commands.spawn((
                Mesh3d(pillar_mesh.clone()),
                MeshMaterial3d(materials.add(StandardMaterial {
                    base_color: Color::hsl(210.0 + height_seed * 20.0, 0.3, 0.5),
                    ..Default::default()
                })),
                Transform::from_xyz(x as f32 * 6.0, 4.0 - height_seed * 0.5, z as f32 * 6.0),
            ));
        }
    }
    // The one entity a shot tracks by name.
    commands.spawn((
        Name::new("beacon"),
        Mesh3d(meshes.add(Sphere::new(0.6))),
        MeshMaterial3d(materials.add(StandardMaterial {
            emissive: LinearRgba::rgb(2.0, 5.0, 12.0),
            ..Default::default()
        })),
        Transform::from_xyz(0.0, 6.0, 0.0),
    ));
    commands.spawn((
        DirectionalLight::default(),
        Transform::from_xyz(20.0, 40.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
    // The "gameplay" camera the director hands off from and back to.
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 5.0, 26.0).looking_at(Vec3::new(0.0, 3.0, 0.0), Vec3::Y),
    ));
    info!("space plays the take, esc skips it");
}

fn play_on_space(
    keys: Res<ButtonInput<KeyCode>>,
    mut assets: ResMut<Assets<SequenceAsset>>,
    mut commands: Commands,
) {
    if !keys.just_pressed(KeyCode::Space) {
        return;
    }
    let take = SequenceAsset {
        name: "flythrough".into(),
        shots: vec![
            // Orbit the beacon, pulling in.
            Shot {
                start: 0.0,
                duration: 6.0,
                blend_in: Some(Blend {
                    secs: 1.0,
                    ease: EaseFunction::SmoothStep,
                }),
                rig: Rig::Orbit {
                    center: TargetRef::Entity("beacon".into()),
                    radius: ScalarTrack {
                        keys: vec![
                            ScalarKey {
                                time: 0.0,
                                value: 22.0,
                                ease: EaseFunction::SmoothStep,
                            },
                            ScalarKey {
                                time: 6.0,
                                value: 12.0,
                                ease: EaseFunction::Linear,
                            },
                        ],
                    },
                    yaw_deg: ScalarTrack {
                        keys: vec![
                            ScalarKey {
                                time: 0.0,
                                value: 0.0,
                                ease: EaseFunction::Linear,
                            },
                            ScalarKey {
                                time: 6.0,
                                value: 160.0,
                                ease: EaseFunction::Linear,
                            },
                        ],
                    },
                    pitch_deg: ScalarTrack::constant(18.0),
                },
                look: Look::Free, // orbits face their center by default
                lens: Lens::default(),
                shake: None,
                camera: None,
            },
            // A low handheld rail between the pillars.
            Shot {
                start: 6.0,
                duration: 6.0,
                blend_in: Some(Blend {
                    secs: 1.2,
                    ease: EaseFunction::SmoothStep,
                }),
                rig: Rig::Rail {
                    points: vec![
                        Vec3::new(-14.0, 2.0, 14.0),
                        Vec3::new(-4.0, 2.5, 2.0),
                        Vec3::new(8.0, 3.0, -6.0),
                        Vec3::new(16.0, 5.0, -16.0),
                    ],
                    kind: RailKind::CatmullRom,
                    constant_speed: true,
                    progress: ScalarTrack::default(),
                },
                look: Look::At {
                    target: TargetRef::Entity("beacon".into()),
                    damping: Some(7.0),
                },
                lens: Lens {
                    fov: FovSpec::FocalLengthMm {
                        track: ScalarTrack::constant(32.0),
                        filmback: Filmback::Super35,
                    },
                    focus: Some(FocusTrack::Target {
                        target: TargetRef::Entity("beacon".into()),
                        offset: 0.0,
                    }),
                    aperture_f_stops: Some(ScalarTrack::constant(2.0)),
                    // The filmic pass: edges close in, the lens picks up
                    // a little fringing, and the grade warms toward the
                    // end. All of it comes back off when the take does.
                    vignette: Some(ScalarTrack {
                        keys: vec![
                            ScalarKey {
                                time: 0.0,
                                value: 0.15,
                                ease: EaseFunction::SmoothStep,
                            },
                            ScalarKey {
                                time: 6.0,
                                value: 0.7,
                                ease: EaseFunction::Linear,
                            },
                        ],
                    }),
                    aberration: Some(ScalarTrack::constant(0.02)),
                    grading: Some(GradeTrack {
                        temperature: Some(ScalarTrack {
                            keys: vec![
                                ScalarKey {
                                    time: 0.0,
                                    value: 0.0,
                                    ease: EaseFunction::SmoothStep,
                                },
                                ScalarKey {
                                    time: 6.0,
                                    value: 0.25,
                                    ease: EaseFunction::Linear,
                                },
                            ],
                        }),
                        saturation: Some(ScalarTrack::constant(1.15)),
                        ..Default::default()
                    }),
                    ..Default::default()
                },
                shake: Some(Shake {
                    amplitude_deg: 0.8,
                    frequency_hz: 8.0,
                    seed: 7,
                    pos_amplitude: 0.03,
                    ramp: None,
                    fov_amplitude_deg: None,
                    focus_amplitude: None,
                }),
                camera: None,
            },
        ],
        markers: vec![Marker {
            time: 6.0,
            name: "cut_to_rail".into(),
        }],
        // Text rides the sequence, not the game: DirectorPlugin renders
        // these with no editor and no extra plugin, because the `titles`
        // feature is on by default.
        texts: vec![
            TextBlock::at(0.6, 3.4, "THE CANYON")
                .anchor(TextAnchor::Center)
                .fades(0.6, 0.8),
            TextBlock::at(6.4, 4.0, "and out along the rail")
                .anchor(TextAnchor::LowerThird)
                .fades(0.4, 0.8),
        ],
        // An actor track: the director reports these cues to the game via
        // ActiveActorCues; a real game would map "beacon.pulse" onto its
        // own animation graph. This example has no rig to drive, so the
        // cues simply demonstrate the authoring shape.
        actors: vec![
            ActorTrack::entity("beacon")
                .cue(ActorCue::at(0.0, 6.0, "beacon.pulse").fades(0.5, 0.5)),
        ],
        blend_out: Some(Blend {
            secs: 1.5,
            ease: EaseFunction::SmoothStep,
        }),
    };
    let handle = assets.add(take);
    commands.play_sequence_with(
        handle,
        PlayOptions {
            letterbox: Some(2.39),
            ..Default::default()
        },
    );
}

fn skip_on_esc(keys: Res<ButtonInput<KeyCode>>, mut commands: Commands) {
    if keys.just_pressed(KeyCode::Escape) {
        commands.skip_sequence();
    }
}