Renderer · class

ThirdPersonCamera

Generic camera mechanism. The caller supplies its already-resolved target height and desired FOV, keeping crouch rules and speed effects in the game.

Explained in Hello world.

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

Constructor

new

constructor(colliders: ColliderSet, options: Readonly<ThirdPersonCameraOptions>, surface?: GroundSurface | null, /** * A world query for the arm, instead of the collider set. * * Fourth and defaulted, so no existing call site changes — the same shape `surface` was added * in. When it is given, `colliders` is not consulted at all: a caller with one has the whole * world behind it and a set of boxes beside it would be a second, partial answer to the same * question. See `BoomObstruction` for why one consumer could not supply the set. */ obstruction?: BoomObstruction | null)
ParameterTypeDescription
collidersColliderSet
optionsReadonly<ThirdPersonCameraOptions>
surface?GroundSurface | nullThe walkable ground, where the caller has one. Not optional in practice, and the reason is worth stating: colliders is not the whole world. A consumer whose drivable surface is a banked ribbon cannot put it in the collider set — a world-axis box around a banked cross-section has a flat lid at its highest corner, which is floor where nothing is drawn and a wall where the next slab's lid stands above the character's feet. So that surface is invisible to segmentHit, and a boom tested only against boxes passes straight through the deck. Which is exactly what it did. On a descent, pitching the view down put the eye under the track, with the deck between the camera and the character: the rig sees the underside of the surface instead of the character, because dynamic surfaces are not part of what it tests against. Worse in replays, where the camera moves far more than a player ever moves it.
/** * A world query for the arm
instead of the collider set. * * Fourth and defaulted
so no existing call site changes — the same shape `surface` was added * in. When it is given
`colliders` is not consulted at all: a caller with one has the whole * world behind it and a set of boxes beside it would be a second
partial answer to the same * question. See `BoomObstruction` for why one consumer could not supply the set. */ obstruction?: BoomObstruction | null

Properties

NameTypeDescription
camerareadonlyCamera
boomUpXnumberThe frame the arm is built in. World axes unless a caller says otherwise.
More

The arm used to be a spherical boom about world Y — -sin(yaw) cos(pitch), -sin(pitch), cos(yaw) cos(pitch) — so a subject standing on a wall or a ceiling got an arm that still swung about world up, and the rig placed the eye through the surface the subject was standing on. groundClear cut the arm where the eye would go under the deck, which is the same assumption a second time.

The rig already anticipated this case and stopped one step short of it: targetRoll's comment says the rig knows nothing about banked surfaces, only that a camera can be tilted, and damps roll here so a subject snapping between surfaces cannot snap the view. The arm is carried the same distance now. Yaw and pitch keep their meaning and become angles inside this frame, so a caller that never sets one gets exactly the arm it always got.

boomUpYnumber
boomUpZnumber
boomForwardXnumberThe frame's forward, readable because a caller that sets one needs to know what it became.
More

Both setBoomUp and setBoomForward orthogonalise against the other axis, so what the rig holds is rarely the vector it was handed — and a caller computing a roll, or asking where its subject's heading ended up, needs the one the arm is actually built from. Assigning these directly does not rebuild the frame; the two setters are what keep the three axes orthonormal.

boomForwardYnumber
boomForwardZnumber

Methods

setBoomUp

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

Point the boom's up at a direction; the rest of the frame follows it.

ParameterTypeDescription
xnumber
ynumber
znumber
More

Forward is carried from the frame it had rather than derived from a world axis, which is what makes a body walking from a floor onto a wall and onto a ceiling continuous. Deriving it from a fixed reference would be one line shorter and would put a pole wherever up lined up with that reference: the arm would swing through a half turn as a subject crossed it, on a rig whose whole job is that the view does not snap.

The fallbacks are only reached when the carried forward has become parallel to the new up, which a continuous rotation cannot do in one step and a teleport can. World Z first and world X behind it, so there is always an answer and it is never a normalised zero.

A zero-length vector keeps the frame it had, for the reason setUp on the character controller gives: a NaN basis is a camera that is nowhere, and every number downstream of it reads as a rig that has failed rather than as an argument that was refused.

setBoomForward

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

Point the boom's forward at a direction; the up is kept and the arm is rebuilt around it.

ParameterTypeDescription
xnumber
ynumber
znumber
More

For a subject that has a heading of its own, which the carried forward cannot express. setBoomUp keeps whatever forward the frame had, re-projected — the right default, because it is what makes a body walking from a floor onto a wall continuous, and because most subjects have no heading to speak of. What it cannot say is "behind this": a yaw of π puts the eye opposite a forward the caller never chose and, until this existed, could not read either. A subject spawning face-on to a wall got a shot framed at random, and a caller trying to work out how far to roll so that the support surface sits at the bottom of the picture had to know where the arm was — an azimuth measured from an axis nothing reported.

This is not a reversal of the carried forward and does not change it for anyone. A caller that never calls this carries its forward exactly as before, and one that does still has it re-orthogonalised by every setBoomUp — the heading is a direction in the world, and the frame it lands in is the surface's.

A zero-length vector, or one parallel to the current up, keeps the frame it had. Deliberately not the fallback chain setBoomUp uses, and the asymmetry is the point: that method must produce some forward because the up is what changed and a frame needs one, so an arbitrary axis is better than none. Here the forward is the argument, and answering a direction the caller did not ask for is precisely the failure this exists to fix.

snap

snap(): void

update

update(frameDt: number, yaw: number, pitch: number, targetX: number, targetY: number, targetZ: number, targetFovYDeg: number, /** * Roll, radians. The caller's policy: this rig knows nothing about banked * surfaces, only that a camera can be tilted. Damped here rather than by * the caller so a subject snapping between surfaces cannot snap the view. */ targetRoll?: number): void
ParameterTypeDescription
frameDtnumber
yawnumber
pitchnumber
targetXnumber
targetYnumber
targetZnumber
targetFovYDegnumber
/** * Roll
radians. The caller's policy: this rig knows nothing about banked * surfaces
only that a camera can be tilted. Damped here rather than by * the caller so a subject snapping between surfaces cannot snap the view. */ targetRoll?: number