Custom pieces

Write your own Body, Aim, Extension or Noise.

A piece is any object with an update function that runs every frame. Add your own to a camera and it runs alongside the built-in ones:

camera.body = { update };        // replaces any other Body
camera.aim = { update };         // replaces any other Aim
camera.addExtension({ update }); // stacks
camera.addNoise({ update });     // stacks

addExtension and addNoise return a function that removes the piece. For a Body or Aim, setBody and setAim do the same, and warn if the camera already has one.

A class with an update field works just as well as an object.

The update function

(out: CameraState, dt: number, justActivated: boolean) => boolean | void
  • out is the camera's state. Change it in place. Each frame starts from the state the Body and Aim left the frame before, with the camera's own lens: what Extensions and Noise change doesn't carry over to the next frame.
  • dt is the frame time in seconds.
  • justActivated is true on the camera's first frame, and again when it turns back on. Reset any smoothing here so the camera starts from the right place.
  • Return true while your piece is still moving, so on-demand rendering keeps going.

Optional members

MemberWhat it does
primeFromStarts the piece from the camera's initialState on its first activation: (position) on a Body, (rotation, referenceUp) on an Aim, (initialState) on an Extension.
aspectKept up to date with the viewport's width divided by its height.
orthographicKept up to date with whether the camera being driven is orthographic.
inputAxesAxes an input controller can drive, by name.
applyAfter'start' runs an Extension before the Body, for the lens the Body and Aim frame with. 'body' runs it right after the Body, before the Aim, for what depends on where the Body put the camera, like a limit on where it can go. In both, only set the lens or limit the position: an offset would carry over to the next frame. 'aim', the default, runs it after the Aim.

Example: drift toward the pointer

An Extension that turns the shot slightly toward the pointer, on top of any Body and Aim. It's plain core code, so it works with every integration. pointer is { x, y } from -1 to 1:

pointerDrift.ts
import { quat, type Euler, type Quat } from 'math';
import { Damper, type CameraPiece } from '@pmndrs/klipp';

const euler: Euler = [0, 0, 0, 'xyz'];
const rotation: Quat = [0, 0, 0, 1];

export function pointerDrift(pointer: { x: number; y: number }, strength = 0.1, damping = 0.4): CameraPiece {
  const yawDamper = new Damper();
  const pitchDamper = new Damper();
  let yaw = 0;
  let pitch = 0;

  return {
    update: (out, dt, justActivated) => {
      if (justActivated) {
        yawDamper.reset();
        pitchDamper.reset();
      }

      const targetYaw = -pointer.x * strength;
      const targetPitch = pointer.y * strength;
      yaw = yawDamper.update(yaw, targetYaw, damping, dt);
      pitch = pitchDamper.update(pitch, targetPitch, damping, dt);

      euler[0] = pitch;
      euler[1] = yaw;
      quat.multiply(out.quaternion, out.quaternion, quat.fromEuler(rotation, euler));
      return yaw !== targetYaw || pitch !== targetPitch;
    },
  };
}

Tip

Avoid creating objects inside the update function. Allocate scratch objects once, like euler and rotation above.

CameraState

FieldTypeDescription
positionVec3Camera position.
quaternionQuatCamera rotation.
fov, near, farnumberLens settings.
orthographicSizenumberHalf the view's height in world units, used by an orthographic camera instead of fov.
viewOffset[number, number]Shifts the view without moving the camera. [0, 0] is centered.
target, hasTargetVec3, booleanThe point the Body follows, if any. Used by blend hints.
lookAtTarget, hasLookAtTargetVec3, booleanThe point the Aim looks at, if any.
referenceUpVec3Which way is up for Aims. Defaults to [0, 1, 0].
rotationDampingBypassQuatHow far a Body turned the camera this frame, for example around an orbit. Aims with damping follow this turn right away. Reset to identity every frame.

Vectors are plain arrays ([x, y, z], quaternions [x, y, z, w]) from math, which Klipp depends on. Use its vec3 and quat functions to work with them, or copy to three.js with vector.fromArray(out.position).

Helpers

These let your pieces behave like the built-in ones. From @pmndrs/klipp:

HelperWhat it does
DamperSmooths a number with a damping value. Call reset() to snap on the next update.
damping.damp, damping.dampVector3, damping.dampQuaternionThe same as functions over state you keep yourself (damping.createState(), ...).
BlendDriverBlends between camera states you look up by name, for a piece that switches between shots: new BlendDriver((name) => states[name]), then setTarget(name, { curve, time }) to start a blend and tick(dt) for the result.
blend.setTarget, blend.tickThe same as functions over state you keep yourself (blend.create()).
predictor.addPosition, predictor.predictDeltaTrack a target's velocity and predict how far it moves in the next time seconds, over state from predictor.create().

From @pmndrs/klipp/three:

HelperWhat it does
Vector3Damper, QuaternionDamperDamper for a three.js Vector3 or Quaternion.
PredictorThe predictor functions for a three.js Vector3: addPosition(position, dt, smoothing) every frame, then predictDelta(out, time).
readTargetPose(out, target, withRotation)Reads the world position, and optionally rotation, of any target into a TargetPose. Returns false if there isn't one.
resolveTargetPosition(out, target), resolveTargetRotation(out, target)The same into a three.js Vector3 or Quaternion.
resolveVector3(out, value)Turns [x, y, z], a number or a Vector3 into a Vector3.

Usage

camera.addExtension(pointerDrift(pointer)); // keep pointer.x and pointer.y up to date