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. These fields frame the picture:

The Main Camera in the Inspector: projection, fov, orthoSize, turn, near, far, targetTexture (screen) and cullingMask (Everything).

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
fit"height"Orthographic only. height, contain or cover: how orthoSize meets the window - see Fitting a 2D view to any screen
fitAspect16/9Fit contain or cover. The fitted rect's width over its height
turn"none""portrait" turns the picture a quarter clockwise when the window is taller than it is wide - see Portrait turn
near0.1Near clip distance (metres)
far1000Far clip distance (metres)

Six more fields (targetTexture, cullingMask, clear, clearColor, order, overrideMaterial) make a camera draw into a render texture instead of the screen; a camera with a targetTexture is never the screen camera. fit, fitAspect, turn and the target fields are left out of a saved scene while they hold their defaults, so a scene saved before them loads unchanged.

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 draws to the screen. activeCamera(world) returns the first GameObject that is active-in-hierarchy, has both Transform and Camera, whose Camera is enabled, and that has no targetTexture:

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 && !world.get(e, Camera)!.targetTexture);
  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 Hierarchy's create menu has a 2D group that wires the two halves of a 2D setup:

  • 2D Camera (Orthographic) - a GameObject with an orthographic Camera (orthoSize 5, near 0.1, far 100, Fit height, no turn) placed at z = 10 looking down −Z. Set Fit to contain yourself for a playfield that must stay whole on every screen.
  • 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.

See 2D Games for the 2D Scene view, Shape 2D and 2D physics.

Fitting a 2D view to any screen ​

With Fit height, orthoSize is half the view's height and the width follows the window, so a wide phone shows more of the world at the sides than a 4:3 tablet does. A puzzle board or a playfield that must always be seen whole wants Fit contain:

FitWhat is shown
heightorthoSize is half the view's height, centred on the camera. The default, and how every camera worked before Fit existed.
containA rect orthoSize × 2 high and fitAspect times as wide, centred on the camera, shown whole and as large as fits inside the game area, centred there. The world carries on past it on the sides that have room.
coverThe same rect fills the whole view edge to edge, its long side cropped. For a backdrop or a menu's background. Insets are ignored.

For a board 1.6 units wide and 1 high, set Fit contain, fitAspect 1.6, orthoSize 0.5.

The game area is the view less two kinds of edge (gameAreaInsets in @awaken/runtime):

  1. The safe area: what the device reserves, a phone's notch, Dynamic Island, rounded corners and home indicator. The shipped player reads it from the mobile shell or the browser's env(safe-area-inset-*). The editor has none, so notch behaviour cannot be previewed there.
  2. The edges your UI holds: per edge, the largest insetTop/insetRight/insetBottom/insetLeft of the UI screens showing at this window size, times that screen's scale. A status bar along the top with insetTop 62 keeps the board below it.

Any screen with insets reframes the board while it shows, a pause menu included, so put insets only on the screens that stay up during play (a pinned HUD and its layout variants).

The shipped player hands every camera its view size and game area each frame; the editor's Game view does the same, without a safe area.

Portrait turn ​

turn: "portrait" turns the camera's picture a quarter clockwise when the view is taller than it is wide, so a landscape game takes the whole width of an upright phone instead of a thin band across it. The camera's top edge goes to the screen's right. It works for perspective and orthographic cameras alike: the turn is applied in clip space after either projection (turnClockwise).

Everything that frames or reads the picture works in the turned frame:

  • Fit (contain or cover) fits the rect to the turned view.
  • The game-area insets are moved to the edge of the picture that lies against them (turnedView).
  • api.screenToWorld(px, py) and api.worldToScreen(p) use the same matrices, so a tap lands on what is drawn under it. Use them rather than your own orthographic arithmetic: api.input.pointerX/Y stay raw canvas pixels.

The turn moves only the camera's picture. The DOM UI is not turned: lay a portrait HUD out as a layout variant with Max aspect 1. On a phone, pair the turn with App Settings orientation any or portrait, since an app locked to landscape never sees a portrait window (an iPad always gets every orientation).

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 140-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 = 140, 560 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
116..118sunDir.xyz - the direction towards the body lighting the scene
119that body's brightness
120..122sunTint.rgb - its colour, normalised to a hue
123how square it is (0 a disc, 1 a hard-edged square)
124..127star params: cells across, lit-cell threshold, brightness, on/off
128..131star spin: axis xyz and angle in radians (the sky's own rotation)
132..134moonDir.xyz
135the moon's brightness (0 = no second body)
136..138moonTint.rgb
139its phase (0 new, 0.5 full)

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

There is one entry for the lit body rather than two, because the sun and the moon are the same directional light seen at different times: whichever is up is the one lighting you, and its own colour and brightness are what tell a warm midday disc from a dim blue 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.

The Scene view with the Main Camera selected and the camera-preview inset showing that camera's view in the corner.

See also ​

  • The Viewport - editor navigation and the orbit camera controls
  • Transforms - how a camera's position and aim are set
  • Render Pipeline Overview - where the camera uniform is written each frame
  • Shadows - the cascade matrices packed alongside the camera
  • Scripting API - switching the active camera at runtime, screenToWorld and worldToScreen
  • In-Game UI - UI screens, layout variants and the insets Fit contain keeps clear of
  • 2D Games - the 2D Scene view, Shape 2D and 2D physics

Awaken: browser-native WebGPU game engine.