Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cameras

Live Demo: Skybox

A camera defines the viewpoint and projection used to render the scene. Nightshade uses reversed-Z depth buffers for both perspective and orthographic projections, supports infinite far planes, frame-rate-independent input smoothing, and an arc-ball orbit controller for editor-style navigation.

Camera component

A camera entity needs a transform and the CAMERA component.

#![allow(unused)]
fn main() {
let camera = world.spawn_entities(
    LOCAL_TRANSFORM | GLOBAL_TRANSFORM | CAMERA,
    1
)[0];
}
#![allow(unused)]
fn main() {
pub struct Camera {
    pub projection: Projection,
    pub smoothing: Option<Smoothing>,
}

pub enum Projection {
    Perspective(PerspectiveCamera),
    Orthographic(OrthographicCamera),
}
}

The default Camera is perspective with a 45 degree FOV, an infinite far plane, a 0.01 near plane, and smoothing on.

Spawning cameras

Basic camera

#![allow(unused)]
fn main() {
let camera = spawn_camera(
    world,
    Vec3::new(0.0, 5.0, 10.0),
    "Main Camera".to_string(),
);
world.resources.active_camera = Some(camera);
}

Pan-orbit camera

For editor-style arc-ball controls.

#![allow(unused)]
fn main() {
use nightshade::ecs::camera::commands::spawn_pan_orbit_camera;

let camera = spawn_pan_orbit_camera(
    world,
    Vec3::new(0.0, 2.0, 0.0),  // focus point
    10.0,                       // radius (distance)
    0.5,                        // yaw (horizontal angle)
    0.4,                        // pitch (vertical angle)
    "Orbit Camera".to_string(),
);
}

Perspective projection

#![allow(unused)]
fn main() {
pub struct PerspectiveCamera {
    pub aspect_ratio: Option<f32>,
    pub y_fov_rad: f32,
    pub z_far: Option<f32>,
    pub z_near: f32,
}
}
FieldDefaultDescription
aspect_ratioNoneWidth/height ratio. None uses the viewport aspect ratio
y_fov_rad0.7854 (45 deg)Vertical field of view in radians
z_farNoneFar plane distance. None uses an infinite far plane
z_near0.01Near plane distance

Reversed-Z projection

Nightshade uses reversed-Z depth buffers. The near plane maps to depth 1.0 and the far plane maps to 0.0. The reason is precision. Floating-point depth has its highest precision near 0.0, and reversing the mapping puts that precision where the eye notices it most, on distant objects. Z-fighting at long range drops dramatically without changing anything else about the pipeline.

With an infinite far plane (z_far: None), the projection matrix is:

f = 1 / tan(fov / 2)

| f/aspect  0     0      0     |
| 0         f     0      0     |
| 0         0     0      z_near|
| 0         0    -1      0     |

With a finite far plane the matrix maps [z_near, z_far] to [1.0, 0.0]:

| f/aspect  0     0                          0                           |
| 0         f     0                          0                           |
| 0         0     z_near/(z_far - z_near)    z_near*z_far/(z_far-z_near) |
| 0         0    -1                          0                           |
#![allow(unused)]
fn main() {
world.core.set_camera(camera, Camera {
    projection: Projection::Perspective(PerspectiveCamera {
        y_fov_rad: 1.0,
        aspect_ratio: None,
        z_near: 0.1,
        z_far: Some(1000.0),
    }),
    smoothing: None,
});
}

Orthographic projection

#![allow(unused)]
fn main() {
pub struct OrthographicCamera {
    pub x_mag: f32,
    pub y_mag: f32,
    pub z_far: f32,
    pub z_near: f32,
}
}
FieldDefaultDescription
x_mag10.0Half-width of the view volume (horizontal extent is ±x_mag)
y_mag10.0Half-height of the view volume (vertical extent is ±y_mag)
z_far1000.0Far clipping plane distance
z_near0.01Near clipping plane distance

The orthographic projection uses reversed-Z too, mapping [z_near, z_far] to [1.0, 0.0].

#![allow(unused)]
fn main() {
world.core.set_camera(camera, Camera {
    projection: Projection::Orthographic(OrthographicCamera {
        x_mag: 10.0,
        y_mag: 10.0,
        z_near: 0.1,
        z_far: 100.0,
    }),
    smoothing: None,
});
}

Camera systems

Fly camera

A free-flying FPS-style camera with WASD movement.

#![allow(unused)]
fn main() {
fn run_systems(&mut self, world: &mut World) {
    fly_camera_system(world);
}
}

Pan-orbit camera

The arc-ball camera that orbits around a focus point.

#![allow(unused)]
fn main() {
use nightshade::ecs::camera::systems::pan_orbit_camera_system;

fn run_systems(&mut self, world: &mut World) {
    pan_orbit_camera_system(world);
}
}

Orthographic camera

For 2D or isometric views.

#![allow(unused)]
fn main() {
use nightshade::ecs::camera::systems::ortho_camera_system;

fn run_systems(&mut self, world: &mut World) {
    ortho_camera_system(world);
}
}

Input smoothing

The Smoothing component applies frame-rate-independent exponential smoothing to camera input. The smoothing factor is computed as:

smoothing_factor = 1.0 - smoothness^7 ^ delta_time

smoothness is the per-device smoothness parameter. A smoothness of 0 gives instant response. Values approaching 1 make the input progressively more sluggish. The powi(7) exponent is there to make the parameter feel linear when you adjust it in an inspector.

#![allow(unused)]
fn main() {
pub struct Smoothing {
    pub mouse_sensitivity: f32,
    pub mouse_smoothness: f32,
    pub mouse_dpi_scale: f32,
    pub keyboard_smoothness: f32,
    pub gamepad_sensitivity: f32,
    pub gamepad_smoothness: f32,
    pub gamepad_deadzone: f32,
}
}
FieldDefaultDescription
mouse_sensitivity0.5Mouse look speed multiplier
mouse_smoothness0.05Mouse input smoothing (0 = instant, 1 = no change)
mouse_dpi_scale1.0DPI scaling factor for mouse input
keyboard_smoothness0.08Keyboard movement smoothing
gamepad_sensitivity1.5Gamepad stick look speed
gamepad_smoothness0.06Gamepad input smoothing
gamepad_deadzone0.15Gamepad stick deadzone threshold
#![allow(unused)]
fn main() {
world.core.set_camera(camera, Camera {
    projection: Projection::Perspective(PerspectiveCamera::default()),
    smoothing: Some(Smoothing {
        mouse_sensitivity: 0.5,
        mouse_smoothness: 0.05,
        keyboard_smoothness: 0.08,
        ..Smoothing::default()
    }),
});
}

Pan-orbit camera configuration

The PanOrbitCamera component is a fully configurable arc-ball camera with Blender-style controls by default.

#![allow(unused)]
fn main() {
pub struct PanOrbitCamera {
    pub focus: Vec3,
    pub radius: f32,
    pub yaw: f32,
    pub pitch: f32,
    pub target_focus: Vec3,
    pub target_radius: f32,
    pub target_yaw: f32,
    pub target_pitch: f32,
    pub enabled: bool,
    // ... configuration fields
}
}

Default controls

ActionMouseGamepadTouch
OrbitMiddle buttonRight stickSingle finger drag
PanShift + Middle buttonLeft stickTwo finger drag
Zoom (drag)Ctrl + Middle buttonTriggersPinch
Zoom (step)Scroll wheel--

Builder API

#![allow(unused)]
fn main() {
let pan_orbit = PanOrbitCamera::new(focus, 10.0)
    .with_yaw_pitch(0.5, 0.4)
    .with_zoom_limits(1.0, Some(100.0))
    .with_pitch_limits(-1.5, 1.5)
    .with_smoothness(0.1, 0.02, 0.1)
    .with_buttons(PanOrbitButton::Middle, PanOrbitButton::Middle)
    .with_modifiers(None, Some(PanOrbitModifier::Shift))
    .with_upside_down(false);
}

Sensitivity and smoothness

Each action has independent sensitivity and smoothness parameters.

ParameterDefaultDescription
orbit_sensitivity1.0Mouse orbit speed
pan_sensitivity1.0Mouse pan speed
zoom_sensitivity1.0Scroll zoom speed
orbit_smoothness0.1Orbit interpolation smoothness
pan_smoothness0.02Pan interpolation smoothness
zoom_smoothness0.1Zoom interpolation smoothness
gamepad_orbit_sensitivity2.0Gamepad orbit speed
gamepad_pan_sensitivity10.0Gamepad pan speed
gamepad_zoom_sensitivity5.0Gamepad zoom speed
gamepad_deadzone0.15Stick deadzone
gamepad_smoothness0.06Gamepad smoothing

Target values (target_yaw, target_pitch, target_focus, target_radius) are set by user input. The current values interpolate toward the targets every frame using the smoothing formula. The system snaps to the target when the difference falls below 0.001 to avoid an infinite tail.

Zoom and pitch limits

#![allow(unused)]
fn main() {
if let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera) {
    pan_orbit.zoom_lower_limit = 1.0;
    pan_orbit.zoom_upper_limit = Some(50.0);
    pan_orbit.pitch_upper_limit = std::f32::consts::FRAC_PI_2 - 0.01;
    pan_orbit.pitch_lower_limit = -(std::f32::consts::FRAC_PI_2 - 0.01);
}
}

Upside-down handling

When allow_upside_down is true, the pitch can exceed plus or minus 90 degrees. When the camera goes upside down, the yaw direction is automatically reversed so mouse control still feels right.

Runtime control

#![allow(unused)]
fn main() {
if let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera) {
    pan_orbit.target_focus = Vec3::new(0.0, 2.0, 0.0);
    pan_orbit.target_radius = 5.0;
    pan_orbit.target_yaw += 0.1;
    pan_orbit.target_pitch += 0.05;
}
}

Computing camera transform

The pan-orbit camera position is computed from yaw, pitch, and radius.

#![allow(unused)]
fn main() {
let (position, rotation) = pan_orbit.compute_camera_transform();
}

The camera sits at focus + rotate(yaw, pitch) * (0, 0, radius). The rotation is composed as yaw around Y first and then pitch around X.

Screen-to-world conversion

Convert screen coordinates to a world-space ray.

#![allow(unused)]
fn main() {
use nightshade::ecs::picking::PickingRay;

let screen_pos = world.resources.input.mouse.position;
if let Some(ray) = PickingRay::from_screen_position(world, screen_pos) {
    let origin = ray.origin;
    let direction = ray.direction;
}
}

For perspective cameras, the ray origin is the camera position and the direction is computed by unprojecting through the inverse view-projection matrix. For orthographic cameras, the origin is the unprojected near-plane point and the direction is the camera's forward vector.

Multiple cameras

Switching between cameras is a single field write.

#![allow(unused)]
fn main() {
fn on_keyboard_input(&mut self, world: &mut World, key: KeyCode, state: ElementState) {
    if state == ElementState::Pressed && key == KeyCode::Tab {
        let current = world.resources.active_camera;
        world.resources.active_camera = if current == Some(self.main_camera) {
            Some(self.debug_camera)
        } else {
            Some(self.main_camera)
        };
    }
}
}