Skip to content

Camera

A Camera packages the two things every 3D render needs into one object: a projection (how the view frustum maps to clip space) and a view (where the camera sits and what it looks at). Pass it to Pass::add to wire its camera.view_proj and camera.position into every shader the pass renders; the Camera holds Arc-shared state, so subsequent look_at calls propagate to every shader the Camera has been wired into.

Internally a Camera carries:

  • A proj matrix built by Camera::perspective or Camera::orthographic. Both use glam’s right-handed builders (Mat4::perspective_rh, Mat4::orthographic_rh), which match wgpu’s NDC depth range [0, 1].
  • A view matrix initialized to identity (eye at origin, looking down -Z, with +Y up). Call look_at to position the camera in world space.
  • The world-space position of the eye, kept alongside the view matrix so shaders that need it (specular highlights, fresnel) don’t have to invert the view matrix on every frame.

The Camera is the user’s domain, not the Material’s: a Material is “what the surface looks like under any light from any viewpoint”, a Camera is “which viewpoint we’re using right now”.


Construct a Camera with a perspective projection. fovy_radians is the vertical field of view (use degrees.to_radians() if you’re starting from degrees); aspect is width / height; near and far clip the depth range.

Built on glam::Mat4::perspective_rh, which targets wgpu’s NDC depth range [0, 1]. Pair with a depth attachment configured for that range when you add the Camera to a Pass that does depth testing.

The view component starts at identity: eye at the world origin, looking down -Z, with +Y up. Chain look_at to position the camera before binding it.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Camera;
let camera = Camera::perspective(1.047, 16.0 / 9.0, 0.1, 100.0);
6 collapsed lines
// Perspective collapses the depth axis into a non-trivial matrix; the
// [2][3] term encodes the -1 wgpu uses for the homogeneous w divide.
let m = camera.view_proj();
assert!((m[2][3] + 1.0).abs() < 1.0e-5);
Ok(())
}

Construct a Camera with an orthographic projection. The six arguments are the frustum planes in view space: left, right, bottom, top, near, far. Use this when you need a flat 2D look (UI overlays, 2D-in-3D gameplay, isometric scenes) or for shadow-map style passes where you want parallel projection.

Built on glam::Mat4::orthographic_rh, which targets wgpu’s NDC depth range [0, 1]. Pair with a depth attachment configured for that range when you add the Camera to a Pass that does depth testing.

The view component starts at identity: eye at the world origin, looking down -Z, with +Y up. Chain look_at to position the camera before binding it.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Camera;
// A 16:9 viewport, 10 world units tall, depth range 0.1..100.
let camera = Camera::orthographic(-8.0, 8.0, -4.5, 4.5, 0.1, 100.0);
6 collapsed lines
let m = camera.view_proj();
// Orthographic preserves parallel lines: the bottom-right corner is the
// projection-only [3][3] term, which is 1.0 (unlike perspective's 0).
assert!((m[3][3] - 1.0).abs() < 1.0e-5);
Ok(())
}

Update the camera’s aspect ratio (width / height) in place. The projection matrix recomputes and propagates to every shader the Camera was added to, plus the Pass-level camera snapshot the renderer reads for transparency depth-sorting. No need to drop and recreate the Camera handle.

Typical use: window-resize handler. On WindowEvent::Resized, call camera.set_aspect(width as f32 / height as f32) and the next frame renders without distortion.

Returns a handle to the same Camera (Arc-shared backing) for chaining.

Behaviour by projection kind:

  • Perspective: rebuilds from fovy_radians / near / far with the new aspect. The vertical FOV is preserved, horizontal grows or shrinks.
  • Orthographic: keeps the current vertical extent and rescales the horizontal extents so (right - left) / (top - bottom) matches the new aspect, centred on the existing horizontal midpoint. The frustum height stays put; the width tracks the window.
1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Camera;
let camera = Camera::perspective(1.047, 1.0, 0.1, 100.0);
// Window resize: 1920×1080 → wide-screen aspect.
camera.set_aspect(1920.0 / 1080.0);
2 collapsed lines
Ok(())
}

Position the camera in world space. position is where the camera sits, target is the point it aims at, and up is the world-space up vector that orients the roll (almost always [0, 1, 0]). Matches Light’s position vocabulary so both scene types describe “where this object is in the world” the same way.

Returns a handle to the same Camera (Arc-shared backing) so it chains cleanly off a perspective or orthographic constructor and can be called again after the Camera has been added to a Pass. The new view propagates live to every Material the Pass renders.

Internally builds the view matrix with glam::Mat4::look_at_rh. The result is a right-handed view matrix that pairs with the right-handed projection produced by perspective and orthographic.

The world-space position is cached on the Camera and exposed via position so shaders that need it (specular highlights, fresnel) don’t have to invert the view matrix on every frame.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Camera;
let camera = Camera::perspective(1.047, 16.0 / 9.0, 0.1, 100.0)
.look_at([0.0, 1.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
3 collapsed lines
assert_eq!(camera.position(), [0.0, 1.0, 5.0]);
Ok(())
}

Read the combined proj * view matrix as a column-major 4x4. Column-major matches WGSL’s mat4x4<f32> storage and glam’s to_cols_array_2d(), so the result is ready to feed directly into a Shader’s camera.view_proj uniform via Shader::set(...) if you need direct control. For the common case, pass the Camera to Pass::add. The Pass seeds camera.view_proj + camera.position on every Material attached to it and keeps them in sync with later updates.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Camera;
let camera = Camera::perspective(1.047, 16.0 / 9.0, 0.1, 100.0)
.look_at([0.0, 0.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
let view_proj = camera.view_proj();
5 collapsed lines
// Column 3 (translation) reflects the eye offset baked into the view matrix.
assert!(view_proj[3][2] != 0.0);
let _view_proj = view_proj;
Ok(())
}

Read the world-space eye position as [x, y, z]. This is the value set by the most recent look_at call, or [0, 0, 0] if the camera has only been constructed (the default view is identity, with the eye at the origin).

Shaders that need the eye position (specular highlights, fresnel, parallax) typically pull it from the camera.position uniform seeded by Pass::add when the Camera is absorbed. Caching it here keeps every frame cheap (no view-matrix inversion on the GPU side).

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Camera;
let camera = Camera::perspective(1.047, 16.0 / 9.0, 0.1, 100.0)
.look_at([3.0, 2.0, 8.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
let eye = camera.position();
3 collapsed lines
assert_eq!(eye, [3.0, 2.0, 8.0]);
Ok(())
}