Skip to content

Camera

A camera turns your 3D scene into a 2D image; Awaken derives the view and projection from a Camera component in Play, or the editor's orbit camera while you edit, and packs both into one per-frame uniform.

The Camera component

Add a Camera component (packages/render/src/cameraComponent.ts) to a GameObject to define a viewpoint. It has five fields:

FieldDefaultMeaning
projection"perspective""perspective" (a 3D lens, uses fov) or "orthographic" (a flat, size-preserving projection with no foreshortening, uses orthoSize) - see Orthographic and 2D
fov60Perspective only. Vertical field of view, in degrees
orthoSize5Orthographic only. Half the view height in world units (5 shows 10 units top to bottom); matches Unity's orthographicSize
near0.1Near clip distance (metres)
far1000Far clip distance (metres)

The camera's position and orientation come from its GameObject's Transform - the view matrix is the inverse of the camera's world matrix. Parent a camera under a character to make a follow-cam; rotate the GameObject to aim it.

Active-camera selection

A scene can hold many cameras, but only one renders. activeCamera(world) returns the first GameObject that is active-in-hierarchy, has both Transform and Camera, and whose Camera is enabled:

ts
export function activeCamera(world: World): Entity | null {
  const q = world.query(Transform, Camera)
    .filter((e) => world.isActiveInHierarchy(e) && world.get(e, Camera)!.enabled !== false);
  return q.length > 0 ? q[0] : null;
}

There is no "main camera" tag - selection is purely first-enabled in query order. To switch cameras at runtime, disable the current Camera and enable another (see Scripting). If no camera is active, the Game view has nothing to render from.

View and projection maths

Both matrices are column-major (packages/render/src/camera.ts):

  • View - lookAt(eye, target, up) builds a right-handed view matrix (world → view). The camera looks down its local −Z. In the editor this comes from the OrbitCamera; in Play it is inverse(cameraWorldMatrix) via cameraMatrices().
  • Projection - for a perspective camera, perspectiveZO(fovyRad, aspect, near, far) produces a perspective matrix for WebGPU's clip space, where depth z ∈ [0, 1] - near maps to 0, far maps to 1 (the native WebGPU range, not OpenGL's [-1, 1]). For an orthographic camera, orthographicZO(left, right, bottom, top, near, far) builds the matrix from orthoSize (half-height) and the viewport aspect, with the same z ∈ [0, 1] depth direction so the depth test is unchanged. cameraMatrices() picks whichever matches projection. The renderer clears depth to 1.0 and tests with depthCompare: "less", so nearer fragments win.

The camera's world position, needed for specular and fog, is recovered as the translation of the inverted view matrix (cameraPositionFromView).

Orthographic and 2D

Set projection to orthographic and the camera drops perspective foreshortening: a sprite keeps the same on-screen size at any depth, which is the projection a 2D, isometric, or top-down game uses. orthoSize is the zoom - half the visible height in world units, with the width following the viewport aspect (Unity's orthographicSize convention). fov is ignored in this mode, and orthoSize is ignored in perspective.

Only a scene Camera can be orthographic. It applies in the Game view, in Play, in a shipped game, and in the camera-preview inset. The editor's edit-mode OrbitCamera is always perspective, so switching a camera to orthographic changes the framed shot, not how you navigate while editing.

The editor's Add ▸ 2D menu wires the two halves of a 2D setup:

  • 2D Camera - a GameObject with an orthographic Camera (orthoSize 5, near 0.1, far 100) placed at z = 10 looking down −Z.
  • Sprite - a MeshRenderer on the built-in quad mesh, transparent: true and castShadow: false, so a textured card blends its alpha and casts no shadow. Assign its albedo texture in the Inspector.

The quad is a builtin mesh id alongside cube/sphere/plane (packages/render/src/primitives.ts, quadMesh): a unit square in the XY plane facing +Z, so it faces a 2D camera looking down −Z. The plane builtin lies flat on XZ facing up and is edge-on to that camera, so use quad for sprites. There is no separate 2D renderer or sprite pass - a sprite is an ordinary transparent mesh drawn by the same forward transparent pass.

The editor OrbitCamera

While you edit (not Play), the Viewport uses an OrbitCamera (packages/render/src/orbit.ts) instead of a scene Camera. It maintains a target, distance, yaw, and pitch and exposes:

  • orbit - swing the eye around a fixed target (drag the world).
  • look - first-person free-look that rotates about the eye (turn the view, not the world).
  • reframe - re-anchor the orbit pivot onto a point (e.g. the selection) without moving the eye, so subsequent orbiting swings around it.
  • zoom - dolly in/out along the view direction.

Pitch is clamped just short of straight up/down to avoid a degenerate up vector. See The Viewport for the navigation bindings.

The 116-float camera uniform

Every frame the renderer packs one uniform buffer that all scene shaders read (group 0, binding 0). It is the most layout-sensitive buffer in the engine, so its packing lives in its own unit-tested module (packages/render/src/cameraUniform.ts, CAMERA_FLOATS = 116, 464 bytes) rather than inline, so it cannot silently drift from the WGSL Camera struct:

FloatsContents
0..15viewProj (mat4)
16..18camera world position
19cascade count (camPos.w)
20..35inverse viewProj (mat4) - used by the grid and sky to unproject
36..99lightViewProj[4] - one shadow cascade matrix each
100..103cascade split distances (radial)
104..106sky zenith RGB
107SSAO-on flag (skyZenith.w)
108..110sky horizon RGB
111tonemap mode (skyHorizon.w; 0 linear, 1 ACES)
112..114sky ground RGB

So one buffer carries the camera, the shadow cascades, the sky gradient, and the SSAO/tonemap flags - which is why sky colours and the tonemap choice ride on the camera uniform rather than a separate one.

Game view and the camera preview

  • Game view renders through the active Camera exactly as the shipped game will. Toggle it from the Toolbar.
  • Camera-preview inset - when a camera is selected in the editor, the renderer re-renders the scene from that camera into a small swapchain sub-rect (single-sample, reusing the frame's shadow maps) so you can frame a shot without leaving the editor view.

📸 Screenshot - save as img/render-camera-preview.png

The Viewport with a Camera GameObject selected, its frustum gizmo visible, and the camera-preview inset showing that camera's view in the corner.

See also

Awaken — browser-native WebGPU game engine.