Renderer · class

Camera

Perspective camera. Yaw/pitch convention: yaw 0 looks toward -Z, positive yaw turns right (+X); positive pitch looks up.

Explained in Hello world.

class Camera
import { Camera } from '@driftengine/core';

Properties

NameTypeDescription
positionreadonlyvec3
yawnumber
pitchnumber
fovYDegnumber
nearnumberNear plane, metres — and the single biggest control this engine has over z-fighting.
More

A conventional depth buffer spends its precision hyperbolically: the smallest separation it can resolve at distance z is about z² / (near · 2^bits). At the old 0.1 that is a quarter of a millimetre at twenty metres, 1.5 mm at fifty, and 6 mm at a hundred — so any two surfaces closer together than that are decided by float rounding, and which one a pixel shows changes as the camera moves — shown most clearly by a matched pair of screenshots of one seam from two angles, right in one and wrong in the other.

Precision scales linearly with this number, so four times the near plane is four times the resolution at every distance, and it costs nothing. Safe at 0.4 because a third-person boom never brings the eye closer than BOOM_MIN (0.9 m) to the character and keeps its own radius clear of surfaces — there is nothing legitimately within 40 cm of this camera to clip.

It is a mitigation and not the cure. A 0.2 mm seam, which the daily generator does produce where two decks meet, still fights past about forty metres. The cure is a reversed-Z float depth buffer; see docs/bugs/.

farnumber
forwardreadonlyvec3
viewreadonlymat4
projectionreadonlymat4
viewProjectionreadonlymat4
invViewProjectionreadonlymat4
rollnumberRoll about the view direction, radians. Positive drops the right side. Render-only: nothing about the camera reaches the simulation.

Methods

lookAt

lookAt(x: number, y: number, z: number): void

Point the camera at a world position from wherever it currently stands.

ParameterTypeDescription
xnumber
ynumber
znumber
More

Added because three callers had each derived this by hand and one of them got it wrong: a preview rig placed itself on a circle around its subject and computed a yaw that was exactly π out, so it rendered the inside of an empty box and the figure it was built to show was behind it. The trig is four lines and the sign conventions are this class's own — yaw 0 looks toward −Z, positive yaw turns toward +X — so leaving every caller to rediscover them is leaving them a trap.

Sets yaw and pitch only. Roll is a separate decision (it is a lean, not an aim), and updateMatrices still has to be called afterwards.

updateMatrices

updateMatrices(aspect: number): void
ParameterTypeDescription
aspectnumber

adoptView

adoptView(view: ReadonlyMat4, projection: ReadonlyMat4): void

Take a view and a projection that were computed somewhere else.

ParameterTypeDescription
viewReadonlyMat4
projectionReadonlyMat4
More

Because an XR eye cannot be described by this class, and this file already says why. updateMatrices derives the view from yaw and pitch and the projection from mat4.perspective, and the note above it records the corollary: only a centred crop of a symmetric frustum is itself a symmetric frustum; an off-centre one is sheared, which perspective cannot produce. A headset's eye projection is exactly that shear, off-axis by the distance from the pupil to the display's centre, and its view comes from a pose no pair of Euler angles was ever asked to describe.

So a supplied view is adopted rather than configured. Everything downstream reads view, projection, viewProjection and invViewProjection, so frustum culling, picking, the sky and every pass that samples the inverse keep working with nothing changed.

position and forward are decomposed back out, and that is not a convenience. A view matrix is the inverse of the camera's world transform, so the eye is -Rᵀt and the direction is the third row negated. A consumer reading camera.position to place a listener or to score a level of detail would otherwise get whatever the last non-XR frame left there, which is a defect that looks like an audio bug rather than a camera one.

yaw, pitch and roll are deliberately left alone, and reading them after this is reading the last angles somebody set. A supplied rotation may be one no Euler triple describes without a convention this class does not own, and inventing one here would put a second answer to "where is the camera looking" beside forward, which is derived from the matrix and is always right.

near and far are not read and not written: the supplied projection carries its own depth range, which the runtime chose.

project

project(out: Float32Array | number[], x: number, y: number, z: number, cssWidth: number, cssHeight: number): boolean

Where a world point lands on the canvas, in CSS pixels, or false if it is behind.

ParameterTypeDescription
outFloat32Array | number[]
xnumber
ynumber
znumber
cssWidthnumber
cssHeightnumber
More

For attaching DOM to a place in the world: a focus ring on a card, a label on a marker. The caller passes the CSS box rather than the drawing buffer, because the answer is for the document and the document does not know the device pixel ratio.

False rather than a coordinate when w <= 1e-6. The perspective divide by a negative w mirrors the point through the origin, so a thing behind the viewer reports a perfectly plausible position on the opposite side of the screen. A caller that placed an affordance there would be putting it on the wrong object, which is worse than putting it nowhere.

rayThrough

rayThrough(origin: Float32Array, direction: Float32Array, cssX: number, cssY: number, cssWidth: number, cssHeight: number): void

The ray a pixel looks along, in world space.

ParameterTypeDescription
originFloat32Array
directionFloat32Array
cssXnumber
cssYnumber
cssWidthnumber
cssHeightnumber
More

Two points are unprojected rather than one, because a direction cannot be recovered from a single unprojected point without knowing where the eye is in the same space — and for an orthographic camera there is no single eye at all. Unprojecting both ends of the depth range and subtracting is correct for either projection, which is why it is written this way for a camera that is currently only perspective.

cssX/cssY are in the canvas's CSS box, so a caller hands over event.clientX - rect.left and nothing has to know the device pixel ratio.