Klipp Full documentation content. \[!WARNING] Early-stage and experimental. The API may still change. > \[!TIP] See it in action on the [examples site](https://pmndrs.github.io/klipp/examples/). ## Why Klipp? Most 3D projects rebuild the same camera code: an offset that follows the player, damping, a dead zone, a cutscene camera and a smooth way back. Klipp brings the virtual camera model from engines like Unity to the web: each shot is described on its own, the one with the highest priority is on screen, and switching between shots is a blend. ## One system, any renderer The core is a complete camera system with no renderer or framework attached. Integrations are thin layers on top of it: three.js and React Three Fiber today, with more to come, like TresJS. Each page shows the core and every integration in tabs. ## What it looks like A follow camera, and a cutscene camera that takes over when it turns on. Turning it off blends back. **Core** **three.js** **React Three Fiber** ```ts import { vec3 } from 'math'; import { Klipp, FollowBody, HardLockToTargetBody, HardLookAtAim, targetPose, VirtualCamera } from '@pmndrs/klipp'; const klipp = new Klipp(); const player = targetPose.create(); // keep player.position up to date const spot = targetPose.create(); vec3.set(spot.position, 10, 2, 0); const follow = new VirtualCamera('follow', { priority: 10 }); klipp.add(follow); follow.body = new FollowBody(player, { offset: [0, 3, 8], damping: 0.5 }); follow.aim = new HardLookAtAim(player); const cutscene = new VirtualCamera('cutscene', { priority: 20, active: false }); klipp.add(cutscene); cutscene.body = new HardLockToTargetBody(spot); cutscene.aim = new HardLookAtAim(player); // every frame klipp.update(dt); engineCamera.setPosition(klipp.shot.position); engineCamera.setRotation(klipp.shot.quaternion); ``` ```ts import { Vector3 } from 'three'; import { KlippThree, FollowBodyThree, HardLockToTargetBodyThree, HardLookAtAimThree, VirtualCameraThree } from '@pmndrs/klipp/three'; const klipp = new KlippThree(camera); const follow = new VirtualCameraThree('follow', { priority: 10 }); klipp.add(follow); follow.body = new FollowBodyThree(player, { offset: new Vector3(0, 3, 8), damping: 0.5 }); follow.aim = new HardLookAtAimThree(player); const cutscene = new VirtualCameraThree('cutscene', { priority: 20, active: false }); klipp.add(cutscene); cutscene.body = new HardLockToTargetBodyThree(new Vector3(10, 2, 0)); cutscene.aim = new HardLookAtAimThree(player); // every frame, before rendering klipp.update(dt); ``` ```tsx import { Klipp, VirtualCamera, Body, Aim } from '@pmndrs/klipp/react'; ``` ## Next steps 1. [Installation](/klipp/docs/getting-started/installation) 2. [Core concepts](/klipp/docs/getting-started/core-concepts) 3. [Your first camera](/klipp/docs/getting-started/your-first-camera) 4. [Building on the core](/klipp/docs/getting-started/building-on-the-core), for your own integration or for data and functions ]]> ` creates one for you. Settings have the same names everywhere. ## Targets Pieces follow and look at targets: - **Core:** the core doesn't know about your scene. A target is a `TargetPose` you keep up to date from wherever your data lives: `position`, and `rotation` with `hasRotation`. Pieces that frame by size, like the composers, also read its `extent`. - **three.js and React Three Fiber:** a target is an `Object3D`, a ref to one, or a fixed point: a `Vector3` or `[x, y, z]`. Objects are tracked live, including their rotation. A target of `null`, or a ref that is still empty, is ignored until it resolves. ## Damping Most pieces take a `damping` setting that makes them ease toward their goal instead of snapping to it. It's a time in seconds: `0` is instant, `0.5` feels smooth, `2` feels heavy. Pass `{ into, from }` to ease differently while the gap is growing (`into`) and while it's closing again (`from`). Next to `damping` you'll usually find `maxSpeed`, which caps how fast the camera may move while catching up. ## Changing settings while running Settings are plain fields, so you can change them at any time, even every frame. Vector fields are always `[x, y, z]` arrays. In React, `ref` gives you the piece itself, so you can change it without re-rendering. ## Usage Feeding a target: **Core** **three.js** **React Three Fiber** ```ts import { quat, vec3 } from 'math'; import { targetPose } from '@pmndrs/klipp'; const player = targetPose.create(); player.extent.radius = 0.5; // for pieces that frame by size // every frame, before klipp.update vec3.copy(player.position, playerPosition); quat.copy(player.rotation, playerRotation); player.hasRotation = true; ``` ```ts new HardLookAtAimThree(player); // an Object3D new HardLookAtAimThree({ current: player }); // a ref to one new HardLookAtAimThree(new Vector3(0, 1, 0)); // a fixed point, or [0, 1, 0] ``` ```tsx // a ref to an Object3D // an Object3D // a fixed point, like ``` Ready to build one? Continue with [Your first camera](/klipp/docs/getting-started/your-first-camera). ]]> { overview.active = !overview.active; }; ``` The highest-priority active camera is the one on screen. Toggle `overview` and Klipp blends between the two shots. ### Change the blend By default a blend takes 2 seconds with an ease-in-out curve: ```ts import { BlendCurves } from '@pmndrs/klipp'; const klipp = new Klipp({ defaultBlend: { curve: BlendCurves.easeInOut, time: 0.8 } }); ``` ### Create Klipp `KlippThree` drives a three.js camera: ```ts title="main.ts" import { GridHelper, PerspectiveCamera, Scene, WebGLRenderer } from 'three'; import { KlippThree } from '@pmndrs/klipp/three'; const renderer = new WebGLRenderer(); renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement); const scene = new Scene(); scene.add(new GridHelper(20, 20)); const camera = new PerspectiveCamera(50, innerWidth / innerHeight); const klipp = new KlippThree(camera); klipp.setSize(innerWidth, innerHeight); ``` `setSize` matters for pieces that frame by screen position, like the composers. Call it again whenever the viewport resizes. ### Add a moving box Any `Object3D` can be a target. `klipp.update` writes the shot to the camera: ```ts import { BoxGeometry, Mesh, MeshNormalMaterial } from 'three'; const player = new Mesh(new BoxGeometry(), new MeshNormalMaterial()); scene.add(player); let last = 0; renderer.setAnimationLoop((time) => { const t = time / 1000; player.position.set(Math.sin(t) * 4, 0.5, Math.cos(t) * 4); klipp.update(t - last); last = t; renderer.render(scene, camera); }); ``` ### Add a virtual camera A virtual camera is one shot, described by a **Body**, which decides where the camera is, and an **Aim**, which decides where it looks: ```ts import { Vector3 } from 'three'; import { FollowBodyThree, HardLookAtAimThree, VirtualCameraThree } from '@pmndrs/klipp/three'; const follow = new VirtualCameraThree('follow', { priority: 10 }); klipp.add(follow); follow.body = new FollowBodyThree(player, { offset: new Vector3(0, 3, 8) }); follow.aim = new HardLookAtAimThree(player); ``` The camera now stays 8 units behind and 3 units above the box, looking straight at it. ### Smooth it out `damping` lets the camera lag behind a little instead of being glued to the box. The value is roughly how many seconds it takes to catch up: ```ts follow.body = new FollowBodyThree(player, { offset: new Vector3(0, 3, 8), damping: 0.5 }); ``` ### Add a second shot A wide overview camera with a higher `priority`, turned on and off with `active`. A target can also be a fixed point: ```ts import { Vector3 } from 'three'; import { HardLockToTargetBodyThree, VirtualCameraThree } from '@pmndrs/klipp/three'; const overview = new VirtualCameraThree('overview', { priority: 20, active: false }); klipp.add(overview); overview.body = new HardLockToTargetBodyThree(new Vector3(0, 15, 15)); overview.aim = new HardLookAtAimThree(new Vector3(0, 0, 0)); button.onclick = () => { overview.active = !overview.active; }; ``` The highest-priority active camera is the one on screen. Toggle `overview` and Klipp blends between the two shots. ### Change the blend By default a blend takes 2 seconds with an ease-in-out curve: ```ts import { BlendCurves } from '@pmndrs/klipp'; const klipp = new KlippThree(camera, { defaultBlend: { curve: BlendCurves.easeInOut, time: 0.8 } }); ``` ### Start with a scene A box that moves in a circle. A ref to it will be the target: ```tsx title="App.tsx" import { useRef } from 'react'; import { Canvas, useFrame } from '@react-three/fiber'; import type { Mesh } from 'three'; function Player({ ref }) { useFrame(({ clock }) => { const t = clock.getElapsedTime(); ref.current.position.set(Math.sin(t) * 4, 0.5, Math.cos(t) * 4); }); return ( ); } export default function App() { const playerRef = useRef(null); return ( ); } ``` ### Add `` `` drives the camera. Put it anywhere inside ``: ```tsx import { Klipp } from '@pmndrs/klipp/react'; {/* cameras go here */} ``` ### Add a virtual camera A `` is one shot, described by a **Body**, which decides where the camera is, and an **Aim**, which decides where it looks: ```tsx import { Klipp, VirtualCamera, Body, Aim } from '@pmndrs/klipp/react'; ``` The camera now stays 8 units behind and 3 units above the box, looking straight at it. ### Smooth it out `damping` lets the camera lag behind a little instead of being glued to the box. The value is roughly how many seconds it takes to catch up: ```tsx ``` ### Add a second shot A wide overview camera with a higher `priority`, turned on and off with `active`. A target can also be a fixed point, `[x, y, z]`: ```tsx title="App.tsx" collapsible import { useRef, useState } from 'react'; import { Canvas } from '@react-three/fiber'; import type { Mesh } from 'three'; import { Klipp, VirtualCamera, Body, Aim } from '@pmndrs/klipp/react'; export default function App() { const playerRef = useRef(null); const [overview, setOverview] = useState(false); return ( <> ); } ``` The highest-priority active camera is the one on screen. Toggle `overview` and Klipp blends between the two shots. ### Change the blend By default a blend takes 2 seconds with an ease-in-out curve: ```tsx import { BlendCurves } from '@pmndrs/klipp'; ``` ## Where to go next - [Body](/klipp/docs/body/introduction) and [Aim](/klipp/docs/aim/introduction) for every way to position and aim a camera. - [Blending](/klipp/docs/blending/introduction) for curves, per-camera transitions and blend hints. - [Building on the core](/klipp/docs/getting-started/building-on-the-core) to read your engine's objects directly, or to use plain data and functions. ]]> { private readonly pose = targetPose.create(); protected override readTarget(): TargetPose | null { if (!this.target) return null; vec3.copy(this.pose.position, this.target.worldPosition); quat.copy(this.pose.rotation, this.target.worldRotation); this.pose.hasRotation = true; return this.pose; } } camera.body = new EngineFollow(player, { offset: [0, 3, 8] }); ``` The same works for `HardLockToTargetBody`, `OrbitFollowBody`, `PositionComposerBody`, `HardLookAtAim`, `RotateWithFollowTargetAim`, `RotationComposerAim` and `PanTiltAim`. `FollowZoomExtension` reads its target the same way, `ConfinerExtension` its bounds through `readBounds` and `GroupFramingExtension` its members through `readMembers`. Name your classes after the core one with a suffix of your own, like `FollowBodyThree`, which is `FollowBody` reading an `Object3D`. For logic of your own, write a [custom piece](/klipp/docs/advanced/custom-components). ## Data and functions Under its classes, every piece is plain data and functions on [`math`](https://github.com/pmndrs/math), in a namespace named after it, with the same three parts. For example, `FollowBody` has: - `FollowParams`, and `follow.createParams(settings)` for the defaults: its settings (`offset`, `damping`, ...). - `FollowState` and `follow.createState()`: the memory it keeps between frames, like damping velocity. - `follow.update(out, state, params, ...)`: advances one frame and writes into a `CameraState`. A class is a thin wrapper that passes itself as `params`. Calling the functions directly gives you full control, for example from an ECS or a simulation: ```ts import { cameraState, follow, targetPose, hardLookAt } from '@pmndrs/klipp'; const camera = cameraState.create(); const state = follow.createState(); const params = follow.createParams({ offset: [0, 3, 8], damping: 0.5 }); const player = targetPose.create(); function frame(dt: number, justActivated: boolean) { follow.update(camera, state, params, player, dt, justActivated); hardLookAt.update(camera, player.position); } ``` `justActivated` is `true` on a camera's first frame, and again when it turns back on, so smoothing starts from the right place. Virtual cameras pass it for you. Picking the camera and blending works the same way. `klippState.tick` returns the shot, and pushes transitions to `state.events` as data: ```ts import { klippState, cameraState } from '@pmndrs/klipp'; const klipp = klippState.create(); const klippParams = klippState.createParams(); klippState.register(klipp, { name: 'follow', priority: 10, state: camera }); function frame(dt: number) { const shot = klippState.tick(klipp, klippParams, dt); klipp.events.length = 0; // write shot to your camera, for example only when cameraState.transformEquals says it moved } ``` ### With an ECS Because state is plain data, it can live in components. A sketch with [Koota](https://github.com/pmndrs/koota): ```ts import { createWorld, trait } from 'koota'; import { cameraState, follow, targetPose } from '@pmndrs/klipp'; const Camera = trait(() => cameraState.create()); const Target = trait(() => targetPose.create()); const Follow = trait(() => ({ state: follow.createState(), params: follow.createParams({ offset: [0, 3, 8] }) })); const world = createWorld(); world.spawn(Camera, Target, Follow); function frame(dt: number) { world.query(Camera, Target, Follow).updateEach(([camera, target, { state, params }]) => { follow.update(camera, state, params, target, dt, false); }); } ``` ### What's available Each piece is a namespace with the same names: `update` runs one frame, `createState()` makes its memory and `createParams()` its settings. The other namespaces are the parts they share. | Area | Namespaces | | ------------------- | ------------------------------------------------------------------------------------------- | | Body | `follow`, `hardLockToTarget`, `orbitFollow`, `positionComposer` | | Aim | `hardLookAt`, `panTilt`, `rotateWithFollowTarget`, `rotationComposer` | | Extension | `cameraOffset`, `confiner`, `followZoom`, `groupFraming`, `lens`, `recomposer` | | Noise | `basicMultiChannelPerlin`, `impulseListener`, `impulses` | | Following a target | `tracker` (shared by `follow` and `orbitFollow`), `threeRing` (the `threeRing` orbit style) | | Targets | `targetExtent`, `targetPose` | | Input | `consumedInput`, `inputAxis`, `inputMapping` | | Choosing the camera | `klippState` | | Blending | `blend`, `cameraState.lerp` | | Damping | `damping`, `predictor` | | Debugging | `debugZones` | | Output | `cameraState.lensEquals`, `cameraState.transformEquals` | ]]> ` for React Three Fiber. ## Members | Member | Description | | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `add(camera)`, `remove(camera)` | Adds or removes a [virtual camera](/klipp/docs/api/virtual-camera). Adding moves it from any other klipp. | | `update(dt)` | Runs one frame. Returns `true` while something is still moving or the shot changed. | | `shot` | The current shot, see below. | | `setSize(width, height)` | The viewport size in pixels, for pieces that frame by screen position. | | `orthographic` | Whether the camera being driven is orthographic. See [Orthographic cameras](#orthographic-cameras). | | `defaultBlend`, `customBlends` | The blends, changeable at any time. | | `registerUpdate(fn)` | Runs `fn` every frame before the cameras, for input. Returns a function that stops it. | | `whileOnScreen(camera, start, { waitForBlend })` | Calls `start` while `camera` is on screen, and the function it returns once it leaves, for example to connect input. A camera in a [group](/klipp/docs/groups/introduction) is on screen only while the group is too. With `waitForBlend` (the default), only once the blend into it has finished. Returns a function that stops. | | `mode` | `'enabled'`, `'standby'` or `'disabled'`. See below. | | `activeCameraName`, `isActive(name)` | The winner, the camera the shot is heading to. | | `activeCameraState` | The winner's own state, before blending. | | `liveCameraName`, `isLive(name)` | The camera fully on screen, once the blend into it has finished. | | `isBlending` | Whether a blend is in progress. | Klipp is also an event target, see [Events](/klipp/docs/api/events). ## `mode` | Mode | Cameras update | Camera is written | | ------------ | -------------- | ----------------- | | `'enabled'` | yes | yes | | `'standby'` | yes | no | | `'disabled'` | no | no | Use `'standby'` to hand the camera to something else for a while (an editor, a photo mode). Klipp keeps its cameras updated in the background, so they're ready the moment you switch back. ## The shot `shot` is a `CameraState`: | Field | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------- | | `position`, `quaternion` | Where the camera is and how it's turned: `[x, y, z]` and `[x, y, z, w]`. | | `fov`, `near`, `far` | Lens settings, `fov` in degrees. | | `orthographicSize` | Half the view's height in world units, used by an orthographic camera instead of `fov`. | | `viewOffset` | Shifts the view without moving the camera, as a fraction of half the viewport. `[0, 0]` is centered. | `shot` changes only once the first camera goes live, and stays put in `'standby'`. To react only when it changes, override `write`, see [Building on the core](/klipp/docs/getting-started/building-on-the-core#your-own-integration). ## Orthographic cameras An orthographic camera shows `orthographicSize` world units above and below the center of the view, at any distance. Each virtual camera has its own size, like its `fov`: set it with [`Lens`](/klipp/docs/extension/lens) or in `initialState`, and blends move between sizes. `orthographic` tells the pieces whether the camera being driven is orthographic. `KlippThree` and `` set it from their camera, also when it is swapped, for example for drei's ``. On the core, set it yourself. `KlippThree` keeps the camera's `left`, `right`, `top` and `bottom` and writes the size through `zoom`, so after a resize the same part of the world stays in view. New virtual cameras start from the size of the camera `KlippThree` was created with, like ``. A camera swapped in later leaves the size of existing virtual cameras alone. ## Usage **Core** **three.js** **React Three Fiber** `Klipp` works with any renderer. Call `update` every frame and put `shot` on your camera: ```ts import { BlendCurves, Klipp, VirtualCamera } from '@pmndrs/klipp'; const klipp = new Klipp({ defaultBlend: { curve: BlendCurves.easeInOut, time: 1 } }); klipp.setSize(canvas.width, canvas.height); klipp.add(new VirtualCamera('main', { priority: 10 })); function frame(dt: number) { if (klipp.update(dt)) { engine.camera.setTransform(klipp.shot.position, klipp.shot.quaternion); engine.camera.setFov(klipp.shot.fov); engine.render(); } } ``` New cameras start from `initialCameraState`, an option of `Klipp`. Pass your camera's starting pose there. For an orthographic camera, set `orthographic` to `true` and use `shot.orthographicSize` instead of `fov`. `KlippThree` is `Klipp` driving a three.js camera. `update` writes the shot to it: ```ts import { BlendCurves } from '@pmndrs/klipp'; import { KlippThree, VirtualCameraThree } from '@pmndrs/klipp/three'; const klipp = new KlippThree(camera, { defaultBlend: { curve: BlendCurves.easeInOut, time: 1 } }); klipp.setSize(innerWidth, innerHeight); klipp.add(new VirtualCameraThree('main', { priority: 10 })); renderer.setAnimationLoop(() => { klipp.update(dt); renderer.render(scene, camera); }); ``` New cameras start from the camera's lens, position and rotation at the time `KlippThree` is created. It drives a `PerspectiveCamera` or an [`OrthographicCamera`](#orthographic-cameras), and `klipp.camera` can be swapped at any time: the new camera gets the shot on the next frame. `camera.aspect`, or an orthographic camera's `left`, `right`, `top` and `bottom`, stay yours to update on resize, next to `setSize`. `` creates a `KlippThree` on the React Three Fiber camera and runs it every frame. It has to be inside ``. ```tsx import { BlendCurves } from '@pmndrs/klipp'; import { Klipp } from '@pmndrs/klipp/react'; ... ``` | Prop | Description | Default | | -------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | `defaultBlend` | How to blend between cameras. See [Blending](/klipp/docs/blending/introduction). | `{ curve: BlendCurves.easeInOut, time: 2 }` | | `customBlends` | Blends for specific camera pairs. See [Custom blends](/klipp/docs/blending/introduction#custom-blends). | `[]` | | `camera` | The camera to drive. | the r3f default camera | | `mode` | `'enabled'`, `'standby'` or `'disabled'`. See [`mode`](#mode). | `'enabled'` | Virtual cameras start from the real camera's lens, position and rotation, so whatever you set on `` is the starting point. `` works too, see [Orthographic cameras](#orthographic-cameras). It keeps the viewport size in sync for you. [`useKlipp()`](/klipp/docs/api/hooks#useklipp) gives you the `KlippThree` behind ``, with the members above, and [``](/klipp/docs/api/events) listens to every transition. ]]> ... ``` `ref` gives you the camera, with the members above. **`initialState`** ```tsx ``` **``** Listens to transitions involving this camera only. See [Events](/klipp/docs/api/events). ```tsx startBossMusic()} /> ``` **`standbyUpdate`** ```tsx ... ``` ]]> ` is the current winner. Turns `true` as soon as the blend into it starts. ```tsx function Hud() { const isActive = useIsActiveVirtualCamera(); return isActive ? : null; } ``` ## `useIsLiveVirtualCamera` Whether the nearest `` is fully on screen. Turns `true` only once the blend into it has finished. ```tsx const isLive = useIsLiveVirtualCamera(); ``` ## `useVirtualCamera` The camera behind the nearest ``, with its pieces and raw `state`. Mostly needed for [custom pieces](/klipp/docs/advanced/custom-components). It has the same members as any [virtual camera](/klipp/docs/api/virtual-camera#members). ```tsx const camera = useVirtualCamera(); camera.state; // the camera's raw state ``` ## `useKlipp` The `KlippThree` behind the nearest ``, which knows about every camera. It has the same [members](/klipp/docs/api/klipp#members) as `Klipp`. ```tsx const klipp = useKlipp(); klipp.activeCameraName; // the current winner klipp.liveCameraName; // the camera fully on screen klipp.isBlending; // whether a blend is in progress ``` ]]> console.log(`${e.outgoing} -> ${e.incoming}`)); const follow = new VirtualCamera('follow', { priority: 10 }); klipp.add(follow); follow.addEventListener('deactivated', () => console.log('follow is off screen')); ``` ```ts klipp.addEventListener('activated', (e) => console.log(`${e.outgoing} -> ${e.incoming}`)); const follow = new VirtualCameraThree('follow', { priority: 10 }); klipp.add(follow); follow.addEventListener('deactivated', () => console.log('follow is off screen')); ``` `` hears every transition, and `` only its own camera's. Event names become props: `onActivated`, `onDeactivated`, `onBlendCreated`, `onBlendFinished` and `onCut`. ```tsx console.log(`${e.outgoing} -> ${e.incoming}`)} /> console.log('follow is off screen')} /> ``` A ``'s `ref` is the camera, and has `addEventListener` too. ]]> ``` ]]> ``` `ref` gives you the `HardLockToTargetBodyThree`. ]]> ``` `ref` gives you the `FollowBodyThree`. **`bindingMode`** ```tsx ``` ]]> { controller.connect(canvas); return () => controller.disconnect(); }); klipp.registerUpdate(() => controller.update()); ``` Scrolling up and spreading a pinch are positive, while zooming in makes `radial` smaller, hence `invert`. **Recentering** ```ts const recentering = { enabled: true, wait: 1, time: 0.5 }; body.horizontal.recentering = recentering; body.vertical.recentering = recentering; ``` ```ts import { Vector3 } from 'three'; import { HardLookAtAimThree, OrbitFollowBodyThree } from '@pmndrs/klipp/three'; const body = new OrbitFollowBodyThree(player, { radius: 8, targetOffset: new Vector3(0, 1.5, 0), damping: 0.3, radial: { damping: 0.15 }, }); camera.body = body; camera.aim = new HardLookAtAimThree(player); ``` Map the input as on the core. ```tsx import { Aim, Body, InputController, VirtualCamera } from '@pmndrs/klipp/react'; const orbit = { axes: { x: 'horizontal', y: 'vertical' }, gain: 0.3 }; ``` `ref` gives you the `OrbitFollowBodyThree`. Removing an axis setting brings back its default without moving the axis. **Recentering** ```tsx const recentering = { enabled: true, wait: 1, time: 0.5 }; ``` ]]> ``` For meshes, the target's size is measured from its geometry. `ref` gives you the `PositionComposerBodyThree`: if the shape changes (an animated skinned mesh, for example), call `recalculateSize()` to measure it again. **Lookahead** ```tsx ``` ]]> \[!NOTE] Use `initialPosition` to set the starting point. The camera's `initialState` has no effect on `CameraControls`. ## Settings | Setting | Description | Default | | ------------------ | --------------------------------------------------------------------------------------------------------------- | ------- | | `target` | What to orbit around. See [Targets](/klipp/docs/getting-started/core-concepts#targets). Omit for free controls. | | | `initialPosition` | Where the camera starts. | | | `enableTransition` | Animate the internal `camera-controls` calls. | `false` | | `impl` | A custom `CameraControls` subclass to use instead. | | | `waitForBlend` | Ignore input until the blend into this camera has finished. An option of `klipp.whileOnScreen` in three.js. | `true` | | `makeDefault` | React only. Set as r3f's `state.controls` while it accepts input. | `false` | | `regress` | React only. Lower render quality while moving (`performance.regress()`). | `false` | `controls` is the `camera-controls` instance, for its own options like `minDistance` or `dollySpeed`, and methods like `rotateTo` or `setLookAt`. ## Usage **three.js** **React Three Fiber** ```ts import { CameraControlsBodyThree } from '@pmndrs/klipp/three/camera-controls'; import { VirtualCameraThree } from '@pmndrs/klipp/three'; const body = new CameraControlsBodyThree(ball); body.controls.minDistance = 2; const orbit = new VirtualCameraThree('orbit', { priority: 10 }); klipp.add(orbit); orbit.body = body; klipp.whileOnScreen(orbit, () => { body.controls.connect(renderer.domElement); return () => body.controls.disconnect(); }); ``` `whileOnScreen` connects the controls only while their camera is on screen, so several orbit cameras don't fight over the mouse. ```tsx import { CameraControls } from '@pmndrs/klipp/react/camera-controls'; ``` Any option of a `camera-controls` instance can be passed as a prop, and its events too: `onControlStart`, `onControl`, `onControlEnd`, `onTransitionStart`, `onUpdate`, `onWake`, `onRest` and `onSleep`. Input only works while this camera is on screen, so you can mount several orbit cameras without them fighting over the mouse. `ref` gives you the `CameraControlsBodyThree`. ]]> ``` ]]> ``` `ref` gives you the `HardLookAtAimThree`. ]]> ``` For meshes, the target's size is measured from its geometry. `ref` gives you the `RotationComposerAimThree`: if the shape changes (an animated skinned mesh, for example), call `recalculateSize()` to measure it again. ]]> ``` `ref` gives you the `RotateWithFollowTargetAimThree`. ]]> { controller.connect(canvas); return () => controller.disconnect(); }); klipp.registerUpdate(() => controller.update()); // reads input before the cameras run ``` Pass a target to make `pan` and `tilt` relative to its rotation: `new PanTiltAim(vehicle)`. **From code** ```ts aim.pan.applyDelta(5); // turn right by 5° aim.pan.setValue(0); // face forward at once aim.tilt.value; // current tilt ``` More in [Input from data](/klipp/docs/input/input-from-data). ```ts import { PanTiltAimThree } from '@pmndrs/klipp/three'; import { InputControllerDom } from '@pmndrs/klipp/dom'; const aim = new PanTiltAimThree(null, { tilt: { range: [-60, 60] } }); camera.aim = aim; const look = { axes: { x: aim.pan, y: aim.tilt }, gain: 0.15 }; const controller = new InputControllerDom({ mouseButtons: { left: look }, touches: { one: look }, }); klipp.whileOnScreen(camera, () => { controller.connect(renderer.domElement); return () => controller.disconnect(); }); klipp.registerUpdate(() => controller.update()); // reads input before the cameras run ``` Pass a target to make `pan` and `tilt` relative to its rotation: `new PanTiltAimThree(vehicle)`. **From code** ```ts aim.pan.applyDelta(5); // turn right by 5° aim.pan.setValue(0); // face forward at once aim.tilt.value; // current tilt ``` More in [Input from data](/klipp/docs/input/input-from-data). Add an [``](/klipp/docs/input/input-controller) inside it to drive it with the mouse or touch: ```tsx import { Aim, Body, InputController } from '@pmndrs/klipp/react'; ``` `ref` gives you the `PanTiltAimThree`. **From code** ```tsx const aim = useRef(null); aim.current.pan.applyDelta(5); // turn right by 5° aim.current.pan.setValue(0); // face forward at once aim.current.tilt.value; // current tilt ``` More in [Input from data](/klipp/docs/input/input-from-data). ]]> ``` ]]> ``` To move the point of view and keep the framing, pair it with a composer: ```tsx ``` `ref` gives you the `CameraOffsetExtensionThree`. ]]> ``` `ref` gives you the `ConfinerExtensionThree`. ]]> ``` `ref` gives you the `FollowZoomExtensionThree`. ]]> ``` For meshes, member sizes are measured from their geometry. `ref` gives you the `GroupFramingExtensionThree`: if a member's shape changes, call `recalculateSize()` to measure it again. ]]> ``` `ref` gives you the `LensExtension`. To change the lens every frame without re-rendering, set its fields directly: ```tsx const lens = useRef(null); useFrame(() => { lens.current.fov = 50 + speed * 0.5; }); ``` ]]> ``` `ref` gives you the `RecomposerExtension`. ]]> ``` ]]> ``` `ref` gives you the `BasicMultiChannelPerlinNoiseThree`. **Fading in and out** ```tsx ``` **Layering** ```tsx ``` ]]> number`. | `ImpulseShapes.bump` | | `duration` | Length in seconds. | `0.4` | | `radius` | Distance from `position` where the impulse is felt at full strength. | `0` | | `dissipationDistance` | Distance beyond `radius` over which it fades out. `0` means it's felt everywhere. | `0` | | `propagationSpeed` | How fast it travels, in units per second, so faraway cameras feel it later. | `Infinity` | | `channel` | Bitmask to target specific listeners. See `channelMask` below. | `1` | ## Listener settings | Setting | Description | Default | | ------------- | ---------------------------------------------------------------------------- | -------------- | | `gain` | Multiplies the impulse strength. `0` mutes this listener. | `1` | | `shake` | Adds a rattle on top of the push. See below. | | | `cameraSpace` | Treat `direction` relative to the camera instead of the world. See below. | `false` | | `channelMask` | Only react to impulses where `channel & channelMask` is not `0`. | `1` | | `field` | A separate `ImpulseField` to listen to instead of the shared `impulseField`. | `impulseField` | ## Adding a rattle An impulse on its own only pushes the camera. `shake` adds rotation and jitter that grows and fades with it. It's a [`BasicMultiChannelPerlin`](/klipp/docs/noise/basic-multi-channel-perlin) whose `amplitudeGain` follows the impulse strength. ## Camera-relative impulses By default `direction` is in world space, which suits explosions: the camera gets pushed away from where it happened. For recoil or a landing thump, use `cameraSpace` so the same `direction` always kicks the same way on screen. ## Usage **Core** **three.js** **React Three Fiber** ```ts import { ImpulseListenerNoise } from '@pmndrs/klipp'; camera.addNoise(new ImpulseListenerNoise()); ``` **Adding a rattle** ```ts import { BasicMultiChannelPerlinNoise } from '@pmndrs/klipp'; const shake = new BasicMultiChannelPerlinNoise({ positionAmplitude: [0.1, 0.1, 0.1], rotationAmplitude: [4, 4, 4] }); camera.addNoise(new ImpulseListenerNoise({ shake })); ``` **Camera-relative** ```ts impulseField.generate({ position: [0, 0, 0], direction: [0, 0.3, 0.1], shape: ImpulseShapes.recoil }); camera.addNoise(new ImpulseListenerNoise({ cameraSpace: true })); ``` ```ts import { ImpulseListenerNoise } from '@pmndrs/klipp'; camera.addNoise(new ImpulseListenerNoise()); ``` **Adding a rattle** ```ts import { Vector3 } from 'three'; import { BasicMultiChannelPerlinNoiseThree } from '@pmndrs/klipp/three'; const shake = new BasicMultiChannelPerlinNoiseThree({ positionAmplitude: new Vector3(0.1, 0.1, 0.1), rotationAmplitude: new Vector3(4, 4, 4) }); camera.addNoise(new ImpulseListenerNoise({ shake })); ``` **Camera-relative** ```ts impulseField.generate({ position: [0, 0, 0], direction: [0, 0.3, 0.1], shape: ImpulseShapes.recoil }); camera.addNoise(new ImpulseListenerNoise({ cameraSpace: true })); ``` ```tsx import { Noise } from '@pmndrs/klipp/react'; ``` `ref` gives you the `ImpulseListenerNoise`. > \[!NOTE] With `frameloop="demand"`, call `invalidate()` after `generate()`. See [On-demand rendering](/klipp/docs/advanced/on-demand-rendering). **Adding a rattle** `shake` takes the noise's settings instead of the noise itself: ```tsx ``` **Camera-relative** ```tsx ``` ]]> { controller.connect(canvas); return () => controller.disconnect(); }); klipp.registerUpdate(() => controller.update()); // reads input before the cameras run ``` Without a DOM, feed the axes from any source, see [Input from data](/klipp/docs/input/input-from-data). `InputControllerDom` from `@pmndrs/klipp/dom` reads the mouse, touch and wheel, and takes the axes themselves: ```ts import { InputControllerDom } from '@pmndrs/klipp/dom'; const look = { axes: { x: aim.pan, y: aim.tilt } }; const controller = new InputControllerDom({ mouseButtons: { right: look }, touches: { one: look }, }); klipp.whileOnScreen(camera, () => { controller.connect(renderer.domElement); return () => controller.disconnect(); }); klipp.registerUpdate(() => controller.update()); // reads input before the cameras run ``` An `` placed inside the piece finds its axes by name: ```tsx import { Aim, InputController } from '@pmndrs/klipp/react'; ``` ]]> ` finds them by name in the piece it's placed in. Placed directly in a ``, it finds them in any of the camera's pieces. ## Settings | Setting | Description | Default | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- | | `mouseButtons` | What each mouse button drives: `{ left, right, middle }`. | | | `touches` | What each finger count drives: `{ one, two, three }`. | | | `wheel` | What the mouse wheel drives. Scrolling up is positive, like spreading a pinch. | | | `pinch` | What a pinch drives, with two fingers or on a trackpad, as the change in `ln(scale)`. Spreading is positive. | | | `enabled` | Turns input on or off. | `true` | | `suppressContextMenu` | Disable the right-click menu on the canvas. On `inputSystem` on the core and in three.js. | `false` | | `interactiveArea` | Only take drags, scrolls and pinches that start inside part of the canvas: `{ x, y, width, height }` from `0` to `1`, measured from the top-left corner. On `inputSystem` on the core and in three.js. | whole canvas | | `lockTouchAxis` | For two-finger gestures, pick either pinch or rotate instead of both. On `inputSystem` on the core and in three.js. | `false` | | `waitForBlend` | Ignore input until the blend into this camera has finished. An option of `klipp.whileOnScreen` on the core and in three.js. | `true` | | `target` | React only. Something else with axes to control, or a ref to it (for example a `PanTiltAimThree`), instead of the piece it's placed in. | parent piece, or the camera | ## Mapping a source List only the sources you use. Each button and finger count takes the same config: ```ts { axes: { x: 'pan', y: 'tilt' }, // which axis receives the horizontal and vertical drag: names in React, the axes themselves elsewhere gain: 0.15, // multiplier from pixels to axis units invert: { y: true }, // flip one or both directions, or `true` for both } ``` Leave `x` or `y` out to ignore that direction: `axes: { x: 'pan' }` turns only horizontal drags into input. ## Wheel and pinch `wheel` and `pinch` each drive a single axis: ```ts { axis: 'radial', // a name in React, the axis itself elsewhere gain: 0.002, invert: true, } ``` A pinch feels the same on a touch screen and on a trackpad, and adds up to the same amount however far you are zoomed in. That suits a zoom stored as `ln` of its scale. The wheel moves in pixels, so it needs its own `gain`. The page keeps scrolling over the canvas until `wheel` is mapped, and a trackpad pinch keeps zooming the page until `pinch` is mapped. ## Pointer lock For mouse look without holding a button, request pointer lock on the controller's `inputSystem`. While locked, mouse movement goes to `mouseButtons.left`. ## Raw input `input` holds this frame's raw deltas, for example `wheelDeltaY`, `pinchDelta` or `leftDx`, if you want to handle something yourself. ## Usage **Core** **three.js** **React Three Fiber** ```ts import { InputControllerDom } from '@pmndrs/klipp/dom'; const look = { axes: { x: aim.pan, y: aim.tilt }, gain: 0.15 }; const controller = new InputControllerDom({ mouseButtons: { right: look }, touches: { one: look }, }); controller.inputSystem.suppressContextMenu = true; klipp.whileOnScreen(camera, () => { controller.connect(canvas); return () => controller.disconnect(); }); klipp.registerUpdate(() => controller.update()); ``` `whileOnScreen` connects it only while its camera is on screen, so several cameras don't react to the same drag. `controller.enabled = false` keeps it connected but ignores input. **Pointer lock** ```ts // in a click handler controller.inputSystem.requestPointerLock(); controller.inputSystem.exitPointerLock(); ``` **Raw input** ```ts klipp.registerUpdate(() => { controller.update(); roll += controller.input.twistDelta; }); ``` ```ts import { InputControllerDom } from '@pmndrs/klipp/dom'; const look = { axes: { x: aim.pan, y: aim.tilt }, gain: 0.15 }; const controller = new InputControllerDom({ mouseButtons: { right: look }, touches: { one: look }, }); controller.inputSystem.suppressContextMenu = true; klipp.whileOnScreen(camera, () => { controller.connect(renderer.domElement); return () => controller.disconnect(); }); klipp.registerUpdate(() => controller.update()); ``` `whileOnScreen` connects it only while its camera is on screen, so several cameras don't react to the same drag. `controller.enabled = false` keeps it connected but ignores input. **Pointer lock** ```ts // in a click handler controller.inputSystem.requestPointerLock(); controller.inputSystem.exitPointerLock(); ``` **Raw input** ```ts klipp.registerUpdate(() => { controller.update(); roll += controller.input.twistDelta; }); ``` ```tsx ``` Input is only read while its camera is on screen. `enabled={false}` keeps it listening but ignores input; unmount it to stop listening, for example to let the page scroll over the canvas. `ref` gives you the `InputControllerDom`. **Pointer lock** ```tsx const controller = useRef(null); ; // in a click handler controller.current.inputSystem.requestPointerLock(); controller.current.inputSystem.exitPointerLock(); ``` **Raw input** ```tsx useFrame(() => { roll += controller.current.input.twistDelta; }); ``` ]]> (null); aim.current.pan.applyDelta(5); // turn by 5° aim.current.pan.setValue(0); // jump straight to 0° aim.current.pan.value; // read the current value ``` ]]> { aim.pan.applyDelta(remote.pan); aim.tilt.applyDelta(remote.tilt); remote.pan = 0; remote.tilt = 0; }); aim.pan.setValue(0); // face forward again ``` **Recording and replay** An `InputController` from `@pmndrs/klipp` reads its input from its `input` field. Copy what a live controller read each frame, then play it back into another controller with the same mapping. Here `live` is an `InputControllerDom` set up as usual, updated in the recording function: ```ts import { InputController, type ConsumedInput } from '@pmndrs/klipp'; const frames: ConsumedInput[] = []; klipp.registerUpdate(() => { live.update(); frames.push({ ...live.input }); }); // later const replay = new InputController(live.config); let frame = 0; klipp.registerUpdate(() => { if (frame >= frames.length) return false; Object.assign(replay.input, frames[frame++]); replay.update(); return true; }); ``` The replay plays one recorded frame per frame. ```ts const remote = { pan: 0, tilt: 0 }; // filled by your network code klipp.registerUpdate(() => { aim.pan.applyDelta(remote.pan); aim.tilt.applyDelta(remote.tilt); remote.pan = 0; remote.tilt = 0; }); aim.pan.setValue(0); // face forward again ``` **Recording and replay** An `InputController` from `@pmndrs/klipp` reads its input from its `input` field. Copy what a live controller read each frame, then play it back into another controller with the same mapping. Here `live` is an `InputControllerDom` set up as usual, updated in the recording function: ```ts import { InputController, type ConsumedInput } from '@pmndrs/klipp'; const frames: ConsumedInput[] = []; klipp.registerUpdate(() => { live.update(); frames.push({ ...live.input }); }); // later const replay = new InputController(live.config); let frame = 0; klipp.registerUpdate(() => { if (frame >= frames.length) return false; Object.assign(replay.input, frames[frame++]); replay.update(); return true; }); ``` The replay plays one recorded frame per frame. Register the update from a component inside ``, and reach the axes through the piece's `ref`: ```tsx function RemoteLook({ aim, remote }) { const klipp = useKlipp(); useEffect( () => klipp.registerUpdate(() => { aim.current?.pan.applyDelta(remote.pan); aim.current?.tilt.applyDelta(remote.tilt); remote.pan = 0; remote.tilt = 0; }), [klipp, aim, remote], ); return null; } ``` With `frameloop="demand"`, data that arrives between frames, for example in a WebSocket message, needs a nudge: call `invalidate()` after storing it. ```tsx socket.onmessage = (message) => { remote.pan += message.pan; invalidate(); }; ``` Jumps work the same way: `aim.current.pan.setValue(0)`. ]]> number` function works as a curve too: ```ts { curve: (t) => t * t * t, time: 1 } ``` ## Custom blends `customBlends` gives specific camera pairs their own blend. `from` and `to` are camera `name`s; leave one out to match any camera. When several entries match, the most specific one wins: `from` and `to` together, then `to` only, then `from` only, then `defaultBlend`. Change them later on `klipp.customBlends`. ## Blend hints By default the camera moves in a straight line between the two shots. If both shots follow the same target from different sides, that line can pass right through it. Blend hints change the path: | Hint | Effect | | -------------------------------- | ---------------------------------------------------------------------------------------------- | | `BlendHints.sphericalPosition` | Arcs around the target instead of cutting through it. | | `BlendHints.cylindricalPosition` | Arcs around the target horizontally, moves straight vertically. | | `BlendHints.ignoreTarget` | Rotates directly between the two shots instead of keeping the target in view during the blend. | A hint is a setting of the camera, and applies to every blend into or out of it. Combine hints with `|`. ## Usage **Core** **three.js** **React Three Fiber** ```ts import { BlendCurves, BlendHints, Klipp, VirtualCamera } from '@pmndrs/klipp'; const klipp = new Klipp({ defaultBlend: { curve: BlendCurves.easeInOut, time: 1 }, customBlends: [ { to: 'closeup', blend: { curve: BlendCurves.linear, time: 0 } }, { from: 'intro', to: 'gameplay', blend: { damping: 0.8 } }, ], }); klipp.defaultBlend = { curve: BlendCurves.linear, time: 0.5 }; ``` **Blend hints** ```ts const orbit = new VirtualCamera('orbit', { priority: 10, hints: BlendHints.sphericalPosition }); klipp.add(orbit); orbit.hints = BlendHints.sphericalPosition | BlendHints.ignoreTarget; ``` ```ts import { BlendCurves, BlendHints } from '@pmndrs/klipp'; import { KlippThree, VirtualCameraThree } from '@pmndrs/klipp/three'; const klipp = new KlippThree(camera, { defaultBlend: { curve: BlendCurves.easeInOut, time: 1 }, customBlends: [ { to: 'closeup', blend: { curve: BlendCurves.linear, time: 0 } }, { from: 'intro', to: 'gameplay', blend: { damping: 0.8 } }, ], }); klipp.defaultBlend = { curve: BlendCurves.linear, time: 0.5 }; ``` **Blend hints** ```ts const orbit = new VirtualCameraThree('orbit', { priority: 10, hints: BlendHints.sphericalPosition }); klipp.add(orbit); orbit.hints = BlendHints.sphericalPosition | BlendHints.ignoreTarget; ``` ```tsx import { BlendCurves, BlendHints } from '@pmndrs/klipp'; ``` **Blend hints** ```tsx ``` ]]> `s inside the group: ```tsx import { SequencerCamera, VirtualCamera } from '@pmndrs/klipp/react'; ... ... ``` Inside a group, `useIsActiveVirtualCamera` and `useIsLiveVirtualCamera` are `true` only while the group is active or live too. ## Events Transitions between the children are dispatched on `group.children`, with the same events as klipp. The group's own transitions, into and out of it, are dispatched on the group. Each child also gets its own, like any virtual camera. ]]> ... ... ; ``` `ref` gives you the `SequencerCamera`. ]]> ... ... ; ``` To change the weights every frame without re-rendering, pass an object that lives across renders and set them on it: ```tsx const [weights] = useState(() => ({ calm: 1, fast: 0 })); useFrame(() => { weights.fast = speed / maxSpeed; weights.calm = 1 - weights.fast; }); ... ; ``` `ref` gives you the `MixingCamera`. ]]> ``` **Camera frustums** Place `` inside the `` you want to see: ```tsx import { CameraFrustumHelper } from '@pmndrs/klipp/react'; ``` | Prop | Description | Default | | -------------- | --------------------------------------- | ----------------- | | `color` | Color of the helper. | three.js defaults | | `maxDistance` | How far the frustum is drawn. | `1` | | `hideWhenLive` | Hide it while this camera is on screen. | `true` | `ref` gives you the `CameraFrustumHelperThree`. **Orbits** Add the `debug` prop: ```tsx ``` ]]> { if (klipp.update(dt) || sceneChanged) renderer.render(scene, camera); }); ``` Klipp works with ``. It keeps requesting frames on its own while anything is still moving. Once the camera has settled, nothing renders until something asks for a frame. Mounting, changing or unmounting Klipp's components, `` and input read by `` do that for you. For anything else, call `invalidate()` yourself, for example after: - moving a target imperatively, for example `target.position.set(...)` in a click handler, - firing an impulse with `impulseField.generate()`, - changing a component's fields through its `ref`. ```tsx const invalidate = useThree((state) => state.invalidate); function onClick() { targetRef.current.position.set(0, 0, 5); invalidate(); } ``` Outside a component, import `invalidate` from `@react-three/fiber` directly. ]]> 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](/klipp/docs/advanced/on-demand-rendering) keeps going. ## Optional members | Member | What it does | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `primeFrom` | Starts the piece from the camera's `initialState` on its first activation: `(position)` on a Body, `(rotation, referenceUp)` on an Aim, `(initialState)` on an Extension. | | `aspect` | Kept up to date with the viewport's width divided by its height. | | `orthographic` | Kept up to date with whether the camera being driven is [orthographic](/klipp/docs/api/klipp#orthographic-cameras). | | `inputAxes` | Axes an [input controller](/klipp/docs/input/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`: ```ts title="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` | Field | Type | Description | | --------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `position` | `Vec3` | Camera position. | | `quaternion` | `Quat` | Camera rotation. | | `fov`, `near`, `far` | `number` | Lens settings. | | `orthographicSize` | `number` | Half 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`, `hasTarget` | `Vec3`, `boolean` | The point the Body follows, if any. Used by [blend hints](/klipp/docs/blending/introduction#blend-hints). | | `lookAtTarget`, `hasLookAtTarget` | `Vec3`, `boolean` | The point the Aim looks at, if any. | | `referenceUp` | `Vec3` | Which way is up for Aims. Defaults to `[0, 1, 0]`. | | `rotationDampingBypass` | `Quat` | How 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`](https://github.com/pmndrs/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`: | Helper | What it does | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Damper` | Smooths a number with a `damping` value. Call `reset()` to snap on the next update. | | `damping.damp`, `damping.dampVector3`, `damping.dampQuaternion` | The same as functions over state you keep yourself (`damping.createState()`, ...). | | `BlendDriver` | Blends 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.tick` | The same as functions over state you keep yourself (`blend.create()`). | | `predictor.addPosition`, `predictor.predictDelta` | Track a target's velocity and predict how far it moves in the next `time` seconds, over state from `predictor.create()`. | From `@pmndrs/klipp/three`: | Helper | What it does | | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Vector3Damper`, `QuaternionDamper` | `Damper` for a three.js `Vector3` or `Quaternion`. | | `Predictor` | The `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](/klipp/docs/getting-started/core-concepts#targets) 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 **Core** **three.js** **React Three Fiber** ```ts camera.addExtension(pointerDrift(pointer)); // keep pointer.x and pointer.y up to date ``` ```ts 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()`](/klipp/docs/api/hooks#usevirtualcamera) and add the piece in `useEffect`: the returned function is its cleanup. r3f's `state.pointer` already has the right shape: ```tsx title="PointerDrift.tsx" 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; } ; ``` ]]>