bevy_director 0.5.0-dev

Unreal-Sequencer-inspired cinematic camera and sequence system for Bevy
Documentation
//! The overlay: where the camera will go, drawn in the world. Used by the
//! viewfinder; games can call draw_sequence from their own debug systems.

use bevy::{gizmos::config::GizmoConfigGroup, prelude::*};

use crate::sequence::{Rig, SequenceAsset};

/// The director's own gizmo group, so games can tune or disable the
/// overlay without touching their default gizmos.
#[derive(GizmoConfigGroup, Reflect, Default)]
pub struct DirectorGizmoGroup;

const PATH_SAMPLES: usize = 48;
const KEY_RADIUS: f32 = 0.14;
/// A small camera body drawn at each key, long axis looking down -Z.
const BODY_HALF: Vec3 = Vec3::new(0.14, 0.10, 0.22);
const LOOK_RAY: f32 = 1.6;

fn shot_color(index: usize) -> Color {
    // Azure to ember and around again; distinct enough per shot.
    let hues = [200.0, 30.0, 130.0, 280.0, 60.0, 330.0];
    Color::hsl(hues[index % hues.len()], 0.85, 0.6)
}

/// Draw every shot's path, keys, and aim rays. Rails redraw their spline
/// each call: a handful of points, dev-overlay territory.
pub fn draw_sequence(gizmos: &mut Gizmos<DirectorGizmoGroup>, sequence: &SequenceAsset) {
    use bevy::math::cubic_splines::{CubicBSpline, CubicCardinalSpline, CubicGenerator};

    use crate::sequence::{RailKind, TargetRef};

    for (index, shot) in sequence.shots.iter().enumerate() {
        let color = shot_color(index);
        match &shot.rig {
            Rig::Keys { keys, .. } => {
                // The travelled path, approximated through eased segments.
                if keys.len() > 1 {
                    let first = keys.first().unwrap().time;
                    let last = keys.last().unwrap().time;
                    let span = (last - first).max(f32::EPSILON);
                    let points = (0..=PATH_SAMPLES).map(|i| {
                        let t = first + span * i as f32 / PATH_SAMPLES as f32;
                        sample_keys_pos(keys, t)
                    });
                    gizmos.linestrip(points, color.with_alpha(0.8));
                }
                for key in keys {
                    gizmos.sphere(Isometry3d::from_translation(key.pos), KEY_RADIUS, color);
                    if let Some(rot) = key.rot {
                        let iso = Isometry3d::new(key.pos, rot);
                        gizmos.primitive_3d(
                            &Cuboid {
                                half_size: BODY_HALF,
                            },
                            iso,
                            color,
                        );
                        gizmos.line(
                            key.pos,
                            key.pos + rot * (Vec3::NEG_Z * LOOK_RAY),
                            color.with_alpha(0.5),
                        );
                    }
                }
            }
            Rig::Rail { points, kind, .. } => {
                let curve = match kind {
                    RailKind::CatmullRom => CubicCardinalSpline::new_catmull_rom(points.clone())
                        .to_curve()
                        .ok(),
                    RailKind::BSpline => CubicBSpline::new(points.clone()).to_curve().ok(),
                };
                if let Some(curve) = curve {
                    let segs = curve.segments().len() as f32;
                    let sampled = (0..=PATH_SAMPLES)
                        .map(|i| curve.position(segs * i as f32 / PATH_SAMPLES as f32));
                    gizmos.linestrip(sampled, color.with_alpha(0.8));
                }
                for point in points {
                    gizmos.sphere(Isometry3d::from_translation(*point), KEY_RADIUS, color);
                }
            }
            Rig::Orbit { center, radius, .. } => {
                // Entity centers need the world; the overlay only knows
                // fixed points.
                if let TargetRef::Point(center) = center {
                    gizmos.sphere(
                        Isometry3d::from_translation(*center),
                        KEY_RADIUS * 1.4,
                        color,
                    );
                    let r = radius.sample(0.0).max(0.0);
                    let flat = Quat::from_rotation_x(std::f32::consts::FRAC_PI_2);
                    gizmos
                        .circle(Isometry3d::new(*center, flat), r, color.with_alpha(0.6))
                        .resolution(48);
                }
            }
        }
    }
}

/// Where the playhead sits right now, as a brighter ghost.
pub fn draw_playhead(gizmos: &mut Gizmos<DirectorGizmoGroup>, pos: Vec3, rot: Quat) {
    let color = Color::srgb(0.95, 0.97, 1.0);
    let iso = Isometry3d::new(pos, rot);
    gizmos.primitive_3d(
        &Cuboid {
            half_size: BODY_HALF * 1.3,
        },
        iso,
        color,
    );
    gizmos.line(pos, pos + rot * (Vec3::NEG_Z * LOOK_RAY * 1.5), color);
}

fn sample_keys_pos(keys: &[crate::sequence::Key], t: f32) -> Vec3 {
    if keys.len() == 1 || t <= keys[0].time {
        return keys[0].pos;
    }
    if let Some(seg) = keys.windows(2).find(|p| t <= p[1].time) {
        crate::curve::eased_segment(&seg[0], &seg[1], t).0
    } else {
        keys.last().unwrap().pos
    }
}