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, } }
| Field | Default | Description |
|---|---|---|
aspect_ratio | None | Width/height ratio. None uses the viewport aspect ratio |
y_fov_rad | 0.7854 (45 deg) | Vertical field of view in radians |
z_far | None | Far plane distance. None uses an infinite far plane |
z_near | 0.01 | Near 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, } }
| Field | Default | Description |
|---|---|---|
x_mag | 10.0 | Half-width of the view volume (horizontal extent is ±x_mag) |
y_mag | 10.0 | Half-height of the view volume (vertical extent is ±y_mag) |
z_far | 1000.0 | Far clipping plane distance |
z_near | 0.01 | Near 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, } }
| Field | Default | Description |
|---|---|---|
mouse_sensitivity | 0.5 | Mouse look speed multiplier |
mouse_smoothness | 0.05 | Mouse input smoothing (0 = instant, 1 = no change) |
mouse_dpi_scale | 1.0 | DPI scaling factor for mouse input |
keyboard_smoothness | 0.08 | Keyboard movement smoothing |
gamepad_sensitivity | 1.5 | Gamepad stick look speed |
gamepad_smoothness | 0.06 | Gamepad input smoothing |
gamepad_deadzone | 0.15 | Gamepad 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
| Action | Mouse | Gamepad | Touch |
|---|---|---|---|
| Orbit | Middle button | Right stick | Single finger drag |
| Pan | Shift + Middle button | Left stick | Two finger drag |
| Zoom (drag) | Ctrl + Middle button | Triggers | Pinch |
| 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.
| Parameter | Default | Description |
|---|---|---|
orbit_sensitivity | 1.0 | Mouse orbit speed |
pan_sensitivity | 1.0 | Mouse pan speed |
zoom_sensitivity | 1.0 | Scroll zoom speed |
orbit_smoothness | 0.1 | Orbit interpolation smoothness |
pan_smoothness | 0.02 | Pan interpolation smoothness |
zoom_smoothness | 0.1 | Zoom interpolation smoothness |
gamepad_orbit_sensitivity | 2.0 | Gamepad orbit speed |
gamepad_pan_sensitivity | 10.0 | Gamepad pan speed |
gamepad_zoom_sensitivity | 5.0 | Gamepad zoom speed |
gamepad_deadzone | 0.15 | Stick deadzone |
gamepad_smoothness | 0.06 | Gamepad 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) }; } } }