Your first camera
This guide builds a camera that follows a moving box, then adds a second shot and switches between the two.
The core works with any renderer. Where the code touches yours, it says engine.
Create Klipp
import { Klipp } from '@pmndrs/klipp';
const klipp = new Klipp();
klipp.setSize(canvas.width, canvas.height);
setSize matters for pieces that frame by screen position, like the composers. Call it again whenever the viewport resizes.
Add a moving target
The core doesn't read your scene. A target is a TargetPose you keep up to date. After update, klipp.shot holds the shot to put on your camera:
import { vec3 } from 'math';
import { targetPose } from '@pmndrs/klipp';
const player = targetPose.create();
function frame(dt: number) {
const t = engine.time;
vec3.set(player.position, Math.sin(t) * 4, 0.5, Math.cos(t) * 4);
engine.box.setPosition(player.position);
klipp.update(dt);
engine.camera.setPosition(klipp.shot.position); // [x, y, z]
engine.camera.setRotation(klipp.shot.quaternion); // [x, y, z, w]
engine.camera.setFov(klipp.shot.fov); // degrees
engine.render();
}
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:
import { FollowBody, HardLookAtAim, VirtualCamera } from '@pmndrs/klipp';
const follow = new VirtualCamera('follow', { priority: 10 });
klipp.add(follow);
follow.body = new FollowBody(player, { offset: [0, 3, 8] });
follow.aim = new HardLookAtAim(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:
follow.body = new FollowBody(player, { offset: [0, 3, 8], damping: 0.5 });
Add a second shot
A wide overview camera with a higher priority, turned on and off with active:
import { HardLockToTargetBody, VirtualCamera } from '@pmndrs/klipp';
const above = targetPose.create();
vec3.set(above.position, 0, 15, 15);
const center = targetPose.create();
const overview = new VirtualCamera('overview', { priority: 20, active: false });
klipp.add(overview);
overview.body = new HardLockToTargetBody(above);
overview.aim = new HardLookAtAim(center);
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:
import { BlendCurves } from '@pmndrs/klipp';
const klipp = new Klipp({ defaultBlend: { curve: BlendCurves.easeInOut, time: 0.8 } });
Create Klipp
KlippThree drives a three.js camera:
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:
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:
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:
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:
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:
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:
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 (
<mesh ref={ref}>
<boxGeometry />
<meshNormalMaterial />
</mesh>
);
}
export default function App() {
const playerRef = useRef<Mesh>(null);
return (
<Canvas>
<Player ref={playerRef} />
<gridHelper args={[20, 20]} />
</Canvas>
);
}
Add <Klipp>
<Klipp> drives the camera. Put it anywhere inside <Canvas>:
import { Klipp } from '@pmndrs/klipp/react';
<Canvas>
<Klipp>{/* cameras go here */}</Klipp>
<Player ref={playerRef} />
</Canvas>
Add a virtual camera
A <VirtualCamera> is one shot, described by a Body, which decides where the camera is, and an Aim, which decides where it looks:
import { Klipp, VirtualCamera, Body, Aim } from '@pmndrs/klipp/react';
<Klipp>
<VirtualCamera name="follow" priority={10}>
<Body.Follow target={playerRef} offset={[0, 3, 8]} />
<Aim.HardLookAt target={playerRef} />
</VirtualCamera>
</Klipp>
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:
<Body.Follow target={playerRef} offset={[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, [x, y, z]:
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<Mesh>(null);
const [overview, setOverview] = useState(false);
return (
<>
<button onClick={() => setOverview(!overview)}>Toggle overview</button>
<Canvas>
<Klipp>
<VirtualCamera name="follow" priority={10}>
<Body.Follow target={playerRef} offset={[0, 3, 8]} damping={0.5} />
<Aim.HardLookAt target={playerRef} />
</VirtualCamera>
<VirtualCamera name="overview" priority={20} active={overview}>
<Body.HardLockToTarget target={[0, 15, 15]} />
<Aim.HardLookAt target={[0, 0, 0]} />
</VirtualCamera>
</Klipp>
<Player ref={playerRef} />
<gridHelper args={[20, 20]} />
</Canvas>
</>
);
}
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:
import { BlendCurves } from '@pmndrs/klipp';
<Klipp defaultBlend={{ curve: BlendCurves.easeInOut, time: 0.8 }}>
Where to go next
- Body and Aim for every way to position and aim a camera.
- Blending for curves, per-camera transitions and blend hints.
- Building on the core to read your engine's objects directly, or to use plain data and functions.