saddle-camera-cinematic-camera


Reusable cinematic camera toolkit for Bevy: authored rails, weighted look targets, shot sequencing, per-shot blends, and clean gameplay-camera handoff.

The runtime now also exposes an explicit CinematicVirtualCamera authoring component that syncs into the underlying rig/binding system, giving downstream games a clearer “virtual camera + brain” vocabulary on top of the existing solver.

The crate stays project-agnostic. It does not depend on game_core, Screen, GameSet, or any game-specific vocabulary. Consumers wire it into their own schedules and bind it to any Bevy camera entity they already own.

For always-on examples, tools, or sandboxes, CinematicCameraPlugin::always_on(Update) is the simplest entrypoint. For real games, prefer CinematicCameraPlugin::new(...) so activation and teardown stay aligned with your own state flow.

Quick Start

[dependencies]
bevy = "0.18"
saddle-camera-cinematic-camera = { git = "https://github.com/julien-blanchon/saddle-camera-cinematic-camera" }
[dependencies]
bevy = "0.18"
saddle-camera-cinematic-camera = { git = "https://github.com/julien-blanchon/saddle-camera-cinematic-camera" }
use bevy::prelude::*;
use saddle_camera_cinematic_camera::*;

#[derive(States, Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum DemoState {
    #[default]
    Gameplay,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_state::<DemoState>()
        .add_plugins(CinematicCameraPlugin::new(
            OnEnter(DemoState::Gameplay),
            OnExit(DemoState::Gameplay),
            Update,
        ))
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    let camera = commands
        .spawn((
            Name::new("Gameplay Camera"),
            Camera3d::default(),
            Transform::from_xyz(-4.0, 2.5, 12.0).looking_at(Vec3::ZERO, Vec3::Y),
        ))
        .id();

    let mut push_in = CinematicShot::rail(
        "Push In",
        3.0,
        CinematicRail {
            points: vec![
                Vec3::new(-4.0, 2.5, 10.0),
                Vec3::new(-1.0, 2.2, 7.5),
                Vec3::new(3.0, 2.0, 4.0),
                Vec3::new(6.0, 2.0, 3.0),
            ],
            kind: RailSplineKind::Linear,
            ..default()
        },
    );
    push_in.orientation = OrientationTrack::LookAt(LookAtTarget::Point {
        point: Vec3::new(0.0, 1.0, 0.0),
        up: UpVectorMode::WorldY,
    });

    commands.spawn((
        Name::new("Boss Reveal Rig"),
        CinematicCameraRig {
            auto_play: true,
            enabled: true,
        },
        CinematicCameraBinding {
            camera,
            ..default()
        },
        CinematicPlayback::default(),
        CinematicSequence {
            shots: vec![push_in],
            restore_camera_on_finish: true,
            entry_blend: CinematicBlend {
                duration_secs: 0.8,
                easing: CinematicEasing::CubicInOut,
            },
            exit_blend: CinematicBlend {
                duration_secs: 0.8,
                easing: CinematicEasing::SineInOut,
            },
            ..default()
        },
    ));
}
use bevy::prelude::*;
use saddle_camera_cinematic_camera::*;

#[derive(States, Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum DemoState {
    #[default]
    Gameplay,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_state::<DemoState>()
        .add_plugins(CinematicCameraPlugin::new(
            OnEnter(DemoState::Gameplay),
            OnExit(DemoState::Gameplay),
            Update,
        ))
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    let camera = commands
        .spawn((
            Name::new("Gameplay Camera"),
            Camera3d::default(),
            Transform::from_xyz(-4.0, 2.5, 12.0).looking_at(Vec3::ZERO, Vec3::Y),
        ))
        .id();

    let mut push_in = CinematicShot::rail(
        "Push In",
        3.0,
        CinematicRail {
            points: vec![
                Vec3::new(-4.0, 2.5, 10.0),
                Vec3::new(-1.0, 2.2, 7.5),
                Vec3::new(3.0, 2.0, 4.0),
                Vec3::new(6.0, 2.0, 3.0),
            ],
            kind: RailSplineKind::Linear,
            ..default()
        },
    );
    push_in.orientation = OrientationTrack::LookAt(LookAtTarget::Point {
        point: Vec3::new(0.0, 1.0, 0.0),
        up: UpVectorMode::WorldY,
    });

    commands.spawn((
        Name::new("Boss Reveal Rig"),
        CinematicCameraRig {
            auto_play: true,
            enabled: true,
        },
        CinematicCameraBinding {
            camera,
            ..default()
        },
        CinematicPlayback::default(),
        CinematicSequence {
            shots: vec![push_in],
            restore_camera_on_finish: true,
            entry_blend: CinematicBlend {
                duration_secs: 0.8,
                easing: CinematicEasing::CubicInOut,
            },
            exit_blend: CinematicBlend {
                duration_secs: 0.8,
                easing: CinematicEasing::SineInOut,
            },
            ..default()
        },
    ));
}

Public API

TypePurpose
CinematicCameraPluginRegisters the cinematic runtime with injectable activate, deactivate, and update schedules
CinematicCameraSystemsPublic ordering hooks: InputOrCommands, AdvanceTimeline, SolveRig, CollisionAvoidance, ApplyCamera, Debug
CinematicVirtualCameraAuthoring-facing virtual-camera surface that syncs into rig/binding data
CinematicCameraBrainOptional marker for a gameplay camera that receives virtual-camera output
CinematicCameraRigPer-rig runtime toggle and optional autoplay flag
CinematicCameraBindingBinds a rig to a concrete Bevy camera entity and controls transform / projection writeback
CinematicSequenceSequence of authored CinematicShots, plus entry/exit blends and sequence loop policy
CinematicShotPer-shot duration, rail or fixed position, orientation mode, blend-in, markers, shake, and lens track
CinematicRail / CinematicRailCacheCurve authoring and cache-backed arc-length sampling helpers
CinematicTargetGroupWeighted multi-target framing by centroid
CinematicPlaybackInspectable runtime playback state (Stopped, Playing, Paused, Exiting)
CinematicCameraStateSolved camera output: transform, look target, FOV, current shot, and blend summary
CinematicDrivenCameraOwnership marker written to the bound camera while the crate drives it
MessagesCinematicPlaybackCommand, ShotStarted, ShotFinished, ShotMarkerReached, SequenceFinished, CinematicBlendCompleted
CinematicOutputDampingPer-rig exponential smoothing on solved position and rotation to reduce jitter
CinematicCollisionAvoidancePer-rig camera collision avoidance via raycasting with configurable policy, padding, and retract/recover rates
CollisionPolicyCollision response strategy: PushCloser (default) or None
ResourcesCinematicCameraDebugSettings, CinematicCameraDiagnostics

Current Feature Scope

Supported in v0.1:

  • cache-backed rail sampling with normalized or world-distance traversal
  • open and closed rails with Once, Loop, and PingPong traversal
  • fixed, tangent-facing, point-look, entity-look, and weighted target-group orientation
  • per-shot lens interpolation and additive deterministic handheld shake
  • overlapped shot blends plus gameplay-camera blend-in / blend-out
  • message-driven playback commands (Play, Pause, Resume, Restart, Stop, Seek)
  • per-rig output damping (CinematicOutputDamping) for framerate-independent exponential smoothing of solved pose
  • camera collision avoidance (CinematicCollisionAvoidance) via mesh raycasting with asymmetric retract/recover smoothing
  • exponentially smoothed target velocity estimation for stable look-ahead prediction
  • smooth virtual-camera handoff blending when the winning rig changes on a brain camera
  • runtime solved-state components and opt-in gizmo debugging via CinematicCameraDebugSettings

Intentionally minimal in v0.1:

  • editor tooling and asset import pipelines
  • automatic dolly distance solve for group framing
  • orthographic lens blending
  • reverse-play lifecycle messages during ping-pong playback

Pipeline

The runtime is staged and orderable:

  1. InputOrCommands
  2. AdvanceTimeline
  3. SolveRig
  4. CollisionAvoidance
  5. ApplyCamera
  6. Debug

Sequence data is cached on Changed<CinematicSequence>. The solver operates on the cache and writes a CinematicCameraState component before any camera mutation happens. ApplyCamera is the only stage that writes Transform and Projection on the bound camera entity.

Examples

ExamplePurposeRun
basicMinimal looped flythrough on a closed railcargo run -p saddle-camera-cinematic-camera-example-basic
blend_between_shotsGameplay-camera handoff, shot-to-shot blends, and clean returncargo run -p saddle-camera-cinematic-camera-example-blend-between-shots
cinematic_orbit_handoffCross-crate demo: cinematic intro that hands off into an orbit-camera model viewercargo run -p saddle-camera-cinematic-camera-example-cinematic-orbit-handoff
moving_targetRail-driven camera that tracks a moving entity with look-aheadcargo run -p saddle-camera-cinematic-camera-example-moving-target
target_groupWeighted target-group framing over two moving subjectscargo run -p saddle-camera-cinematic-camera-example-target-group
handheld_railRail motion with deterministic additive handheld shakecargo run -p saddle-camera-cinematic-camera-example-handheld-rail
stress_previewOne active rig plus 100 passive preview rigs for perf smokecargo run -p saddle-camera-cinematic-camera-example-stress-preview
virtual_camera_brainTwo authored virtual cameras hand off through a shared brain cameracargo run -p saddle-camera-cinematic-camera-example-virtual-camera-brain

All showcase examples now include a saddle-pane panel for live tuning of playback speed, rig enablement, blend durations, and debug draw toggles.

Workspace Lab

The standalone examples verify the crate in isolation. The workspace also includes a crate-local lab app at shared/camera/saddle-camera-cinematic-camera/examples/lab:

cargo run -p saddle-camera-cinematic-camera-lab
cargo run -p saddle-camera-cinematic-camera-lab

More Docs

To preview rails and look targets in examples or tools, opt into debug gizmos explicitly:

app.insert_resource(CinematicCameraDebugSettings {
    enabled: true,
    ..default()
});
app.insert_resource(CinematicCameraDebugSettings {
    enabled: true,
    ..default()
});

Known Limitations

  • FOV blending is only applied to perspective cameras. Orthographic bindings keep their existing projection.
  • CinematicBlendCompleted is authored for forward-running shot transitions and gameplay handoff; reverse ping-pong transitions do not emit mirror-image lifecycle events yet.
  • Target groups solve a weighted centroid only. They do not yet auto-adjust distance or FOV to keep bounds tight.