Camera
Description
Section titled “Description”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
projmatrix built byCamera::perspectiveorCamera::orthographic. Both use glam’s right-handed builders (Mat4::perspective_rh,Mat4::orthographic_rh), which match wgpu’s NDC depth range[0, 1]. - A
viewmatrix initialized to identity (eye at origin, looking down-Z, with+Yup). Calllook_atto position the camera in world space. - The world-space
positionof 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”.
Methods
Section titled “Methods”Camera::perspective
Section titled “Camera::perspective”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.
Example
Section titled “Example”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(())}import { Camera } from "fragmentcolor";
const camera = Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0);from fragmentcolor import Camera
camera = Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0)import FragmentColor
let camera = Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0)import org.fragmentcolor.*
val camera = Camera.perspective(1.047f, 16.0f / 9.0f, 0.1f, 100.0f)Camera::orthographic
Section titled “Camera::orthographic”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.
Example
Section titled “Example”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(())}import { Camera } from "fragmentcolor";
// A 16:9 viewport, 10 world units tall, depth range 0.1..100.const camera = Camera.orthographic(-8.0, 8.0, -4.5, 4.5, 0.1, 100.0);from fragmentcolor import Camera
# A 16:9 viewport, 10 world units tall, depth range 0.1..100.camera = Camera.orthographic(-8.0, 8.0, -4.5, 4.5, 0.1, 100.0)import FragmentColor
// 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)import org.fragmentcolor.*
// A 16:9 viewport, 10 world units tall, depth range 0.1..100.val camera = Camera.orthographic(-8.0f, 8.0f, -4.5f, 4.5f, 0.1f, 100.0f)Camera::set_aspect
Section titled “Camera::set_aspect”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 / farwith 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.
Example
Section titled “Example”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(())}import { Camera } from "fragmentcolor";
const camera = Camera.perspective(1.047, 1.0, 0.1, 100.0);
// Window resize: 1920×1080 → wide-screen aspect.camera.setAspect(1920.0 / 1080.0);from fragmentcolor import Camera
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)import FragmentColor
let camera = Camera.perspective(1.047, 1.0, 0.1, 100.0)
// Window resize: 1920×1080 → wide-screen aspect.camera.setAspect(1920.0 / 1080.0)import org.fragmentcolor.*
val camera = Camera.perspective(1.047f, 1.0f, 0.1f, 100.0f)
// Window resize: 1920×1080 → wide-screen aspect.camera.setAspect(1920.0f / 1080.0f)Camera::look_at
Section titled “Camera::look_at”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.
Example
Section titled “Example”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(())}import { Camera } from "fragmentcolor";
const camera = Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0).lookAt([0.0, 1.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);from fragmentcolor import Camera
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])import FragmentColor
let camera = try Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0).lookAt([0.0, 1.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0])import org.fragmentcolor.*
val camera = Camera.perspective(1.047f, 16.0f / 9.0f, 0.1f, 100.0f).lookAt(listOf(0.0f, 1.0f, 5.0f), listOf(0.0f, 0.0f, 0.0f), listOf(0.0f, 1.0f, 0.0f))Camera::view_proj
Section titled “Camera::view_proj”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.
Example
Section titled “Example”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(())}import { Camera } from "fragmentcolor";
const camera = Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0).lookAt([0.0, 0.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
const view_proj = camera.viewProj();from fragmentcolor import Camera
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])
view_proj = camera.view_proj()import FragmentColor
let camera = try Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0).lookAt([0.0, 0.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0])
let view_proj = camera.viewProj()import org.fragmentcolor.*
val camera = Camera.perspective(1.047f, 16.0f / 9.0f, 0.1f, 100.0f).lookAt(listOf(0.0f, 0.0f, 5.0f), listOf(0.0f, 0.0f, 0.0f), listOf(0.0f, 1.0f, 0.0f))
val view_proj = camera.viewProj()Camera::position
Section titled “Camera::position”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).
Example
Section titled “Example”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(())}import { Camera } from "fragmentcolor";
const camera = Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0).lookAt([3.0, 2.0, 8.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
const eye = camera.position();from fragmentcolor import Camera
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])
eye = camera.position()import FragmentColor
let camera = try Camera.perspective(1.047, 16.0 / 9.0, 0.1, 100.0).lookAt([3.0, 2.0, 8.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0])
let eye = camera.position()import org.fragmentcolor.*
val camera = Camera.perspective(1.047f, 16.0f / 9.0f, 0.1f, 100.0f).lookAt(listOf(3.0f, 2.0f, 8.0f), listOf(0.0f, 0.0f, 0.0f), listOf(0.0f, 1.0f, 0.0f))
val eye = camera.position()