Custom pieces
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
outis 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.dtis the frame time in seconds.justActivatedistrueon 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
truewhile your piece is still moving, so on-demand rendering keeps going.
Optional members
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:
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;
},
};
}
Avoid creating objects inside the update function. Allocate scratch objects once, like euler and rotation above.
CameraState
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:
From @pmndrs/klipp/three:
Usage
camera.addExtension(pointerDrift(pointer)); // keep pointer.x and pointer.y up to date
const pointer = { x: 0, y: 0 };
renderer.domElement.addEventListener('pointermove', (e) => {
pointer.x = (e.offsetX / renderer.domElement.clientWidth) * 2 - 1;
pointer.y = -(e.offsetY / renderer.domElement.clientHeight) * 2 + 1;
});
camera.addExtension(pointerDrift(pointer));
Get the camera with useVirtualCamera() and add the piece in useEffect: the returned function is its cleanup. r3f's state.pointer already has the right shape:
import { useEffect } from 'react';
import { useThree } from '@react-three/fiber';
import { useVirtualCamera } from '@pmndrs/klipp/react';
export function PointerDrift({ strength = 0.1, damping = 0.4 }) {
const camera = useVirtualCamera();
const pointer = useThree((state) => state.pointer);
useEffect(() => camera.addExtension(pointerDrift(pointer, strength, damping)), [camera, pointer, strength, damping]);
return null;
}
<VirtualCamera name="menu" priority={10}>
<Body.HardLockToTarget target={[0, 2, 8]} />
<Aim.HardLookAt target={[0, 1, 0]} />
<PointerDrift />
</VirtualCamera>;