Camera
NEO's camera system: the director, camera sources, smooth blends, modifiers such as shake and collision, and the Studio's viewport camera.
On this page
Games and the Studio share one camera system, the neo_camera crate. A director decides what each rendering camera shows, blends smoothly from one camera source to another, and layers modifiers such as shake on top. The Studio's own viewport camera is just another source, which is why the Studio can fly away from the game camera and back without the game noticing.
This page is for creators who write Rust game code.
The pieces#
| Type | What it is |
|---|---|
CameraPose | A position, a rotation and a CameraLens. Everything blends as poses. |
CameraLens | Perspective { fov } or Orthographic { height }, plus near, far and focus distance. Optional on a source: without it, the source's Bevy Projection is used. |
NeoCameraSource { priority, enabled, blend_in, focus_distance } | Makes an entity something the director can show. |
CameraOutput | Goes on a rendering camera and holds the director's state for it. Read active_source(), is_blending(), blend_progress(), pose() and source_weight(e). |
SetViewTarget { output, source, blend } | A message: show a source. SetViewTarget::new(source, blend) applies to every output; a source of None goes back to picking by priority. CameraOutput::set_view_target does the same on one output. |
CameraBlend { duration, curve, lock_outgoing } | A transition. CameraBlend::CUT switches instantly; CameraBlend::ease_in_out(seconds) and CameraBlend::linear(seconds) are the common ones. |
BlendCurve | Linear, Cubic, SmootherStep, EaseIn(exp), EaseOut(exp), EaseInOut(exp), or any Bevy EaseFunction. |
CameraBlendFinished | A message sent when a transition completes. |
CameraShake, FovPunch, CameraCollision | Modifiers on an output, each with ModifierSettings { enabled, weight, source }. |
Set up a camera#
Spawn a rendering camera with CameraOutput, spawn sources, and tell the director which one to show:
use bevy::prelude::*;
use neo_camera::*;
fn setup(mut commands: Commands, mut view: MessageWriter<SetViewTarget>) {
// the rendering camera
commands.spawn((Camera3d::default(), CameraOutput::default()));
// two sources
let editor = commands
.spawn(EditorCameraController::looking_at(Vec3::new(6.0, 4.0, 8.0), Vec3::ZERO))
.id();
let cinematic = commands
.spawn((
NeoCameraSource::with_priority(10),
CameraLens::perspective(40f32.to_radians()),
Transform::from_xyz(0.0, 2.0, 12.0).looking_at(Vec3::ZERO, Vec3::Y),
))
.id();
// show the first one now; later, blend to the other over 0.6 s:
// view.write(SetViewTarget::new(cinematic, CameraBlend::ease_in_out(0.6)));
view.write(SetViewTarget::new(editor, CameraBlend::CUT));
}Without an explicit view target, an output shows the enabled source with the highest priority (ties go to the most recently spawned source). A disabled source is never picked by priority, but an explicit view target still shows it.
A source is any entity with a transform and a lens: your game's third-person rig, a cutscene camera, a camera on a moving vehicle. Your systems move sources as usual. The director samples them after transforms are propagated each frame, so a camera parented to a moving character is followed with no frame of lag, and the pose it writes is the one that's rendered.
Blends#
Each output keeps a stack of transitions. A new view target blends in over the live result of everything under it, not a frozen picture:
- Interrupting a blend starts from exactly what is on screen, and the interrupted transition keeps running underneath, so the motion stays smooth, speed included.
- When a transition reaches full weight, everything under it is dropped.
lock_outgoing(CameraBlend::ease_in_out(0.6).locking_outgoing()) freezes the outgoing view at its last pose. Use it when the source you're leaving is about to teleport, such as a game reset.- A source that is despawned keeps its last pose until a transition covers it.
How poses blend:
| Between | How |
|---|---|
| Rotations | Yaw takes the short way round, with pitch and roll blended separately, so the horizon never rolls mid-blend. Near straight up or down it fades to a shortest-arc blend. |
| Perspective and perspective | Position and field of view interpolate. |
| Orthographic and orthographic | Position interpolates, and the view height interpolates so zooming feels even. |
| Perspective and orthographic | A dolly zoom around the focus plane: the eye backs away as the field of view narrows, so the focus plane keeps its framing. |
Game cameras#
A game's own camera works with the director without any changes to game code. By default, every GameCamera becomes both an output and a source of itself:
- The director reads the game's own
Transformas that source's pose and writes the rendered pose only to the camera's global transform and projection. The game never sees another camera's pose in itsTransform. - When the game camera is the only thing shown and no modifier is active, it passes straight through unchanged.
- The game's look (exposure, bloom, post-processing), the camera its UI draws to and the render target all stay on that one camera, so other sources render the world exactly as the game does.
This is how the Studio works: Play shows the game camera, eject (F8) blends to the Studio's viewport camera at the game camera's pose while the game keeps running, and F8 again goes back.
You can also use a dedicated output camera with separate, non-rendering source entities. The director supports both.
Modifiers#
Modifiers are components on the output entity, applied after the blend in a fixed order. Each is weighted by ModifierSettings.weight, and can be scoped to one source with source: Some(entity), in which case it fades in and out with that source's share of the blend. The game camera's collision, for example, fades out as another camera blends in.
| Modifier | What it does |
|---|---|
CameraShake | Add trauma with add_trauma(amount) (up to 1). The shake's strength is trauma squared, so small knocks barely shake and big ones do; trauma decays over time. It shakes pitch, yaw and roll plus a small offset, with smooth noise. |
FovPunch | punch(radians) kicks the field of view; a spring brings it back. |
CameraCollision | A spring arm from a pivot (the focus point, a point, or an entity plus an offset) to the camera, probed with five ray casts against meshes. It pulls in instantly and recovers at recover_speed, ignoring listed entities and their children. |
To write your own, add a system in NeoCameraSystems::Modify, ordered with ModifierStage (Geometry, then Lens, then Noise), that edits CameraOutput::working_pose_mut().
The Studio's viewport camera#
EditorCameraController is the source the Studio's viewport uses. It only takes input while it's the active source of an output, while the host allows it (CameraInputScope), and when a drag starts inside the viewport. Movement is smoothed, and jumps (framing, bookmarks, view changes) ease over a short time.
| Input | Perspective | Orthographic |
|---|---|---|
| Right mouse drag | Look around; W, A, S, D and Q, E fly (E up, Q down) | Pan |
| Right mouse + wheel | Speed step 1 to 8 | |
| Shift / Ctrl or Alt | x3 / x0.25 | Same |
| Alt + left mouse drag | Orbit the pivot | Orbit (switches to perspective) |
| Middle mouse drag | Pan (the point under the cursor follows it) | Pan |
| Alt + right mouse drag, or the wheel | Dolly | Zoom; the wheel zooms at the cursor |
| F | Frame the selection | |
| Alt+G / J / H / K | Perspective / Top / Front / Left (Shift+J, H, K for Bottom, Back, Right) | |
| Numpad 7 / 1 / 3 (Ctrl for the opposite side), 5 | Top / Front / Right, toggle perspective | |
| Ctrl (or Cmd) + 1 to 9, then 1 to 9 | Save a bookmark, then recall it |
Switching between perspective and orthographic views keeps the framing.
Commands#
| Command | What it does |
|---|---|
camera.status | The cameras' state. |
camera.view {view} | Switches the viewport's view (perspective, top, front and so on). |
camera.frame | Frames the selection. |