R3F Hero Camera
One rig, one discipline: every cinematic move is a single damped
setLookAt(..., true) on one drei <CameraControls>; the only per-frame camera
code is one exponential smoother feeding moveTo(..., false) for following. No
scripted per-frame paths, no timeline library — camera-controls' own damping is
the animator, and the user keeps orbit/zoom through every shot.
Built for React Three Fiber 8 + drei 9 + three 0.169 (camera-controls under
drei). Copy-paste building blocks live in REFERENCE.md.
Sibling skills this composes with:
interactive-3d-object — hover/select FX on the objects the camera frames.
guided-placement — delegates its grab-framing stage to this rig.
The shot vocabulary
Every shot is a pure recipe → one setLookAt. d = the scene's long side —
recipes scale with content, never hardcode world units.
| Shot |
Recipe |
Use for |
| Overview |
eye (d·0.8, d·0.6, d·1.1) → scene center |
establishing, reset, "complete" beat |
| 3/4 object frame |
eye = center + normalize(dir)·max(span·2.2, min) from [1, 0.85, 1.25] |
click-to-focus on any object |
| Screen punch-in |
eye = surface point + normal·((h/2)/tan(fov/2) + margin) |
reading a diegetic screen full-frame |
| Screen context |
eye = point + normal·far + cross(normal, up)·side |
screen + its stand/surroundings |
| First-person standpoint |
eye at ~1.6 world-height near a work point, slight lateral offset so the shot isn't dead-on |
operator/visitor vantage |
| Chase engage |
eye = object + (2.4, 2.0, 3.0) → object |
entering follow mode |
| Dolly beat |
controls.dollyTo(dist, true) only |
tension change without re-aiming |
The writer-priority ladder
Multiple systems want the camera. Gate every pose effect on everything above it
so exactly one writer acts per event — the follow smoother is the only
per-frame writer and yields to all of them:
manual user override (explicit view pick)
> tour / scripted beat shots
> selection framing (click-to-focus)
> follow cam (per-frame)
> idle attract orbit
Workflow (checklist)
- Mount the rig — one
<HeroCameraRig> (REFERENCE §1): default camera +
<CameraControls makeDefault> + scale-aware maxDistance (≈2.5× scene long
side + a small flat term so tiny scenes don't clamp tight).
- Add shots as effects — each trigger (selection, beat, button nonce) is a
useEffect that computes one recipe and calls setLookAt(..., true)
(REFERENCE §2–3). Gate each on the ladder above it.
- Follow cam (moving objects) — the single-smoothing-stage
useFrame
(REFERENCE §4). Engage with a chase-pose setLookAt; seed the smoother from
controls.getTarget() so the view glides over instead of snapping.
- Attract orbit —
azimuthAngle += rate·dt in useFrame, broken by a
controlstart listener, re-armed on beat change (REFERENCE §5).
- Mode switch (optional ortho top view) — swap
makeDefault cameras; guard
every pose effect with camera.isPerspectiveCamera / isOrthographicCamera
and keep camera in the deps (REFERENCE §6).
- ESC ladder — one keydown handler releasing the most specific state first:
tour → beat step-back → stop follow (REFERENCE §7).
Core principles
- Poses, not paths. One damped
setLookAt per beat. Sequenced shots =
sequenced state changes, each firing its own pose. If a shot needs a curve,
you almost always actually need two poses.
- Guided vantage, never lockout. Every shot leaves rotate/zoom live; ESC
always releases. Lockouts read as broken, not cinematic.
- One smoothing stage. Per-frame
moveTo(..., smooth=true) restarts the
damped transition every frame on top of the target's own motion — double lag
that reads as stutter. Smooth the target yourself
(k = 1 − e^(−rate·dt), ~12/s) and hand it over with smooth=false.
- User input cancels ambient motion, never intent.
controlstart breaks
attract/auto-orbit for the rest of that cycle; it never interrupts a pose the
user themselves triggered.
- Frame from live world transforms.
getWorldPosition / getWorldQuaternion
on the actual object (via a ref registry), never from layout math that can
drift from what's rendered.
- Recipes scale with content. Distances derive from scene size, object
span, or fov math — a rig full of magic constants breaks on the next scene.
Pitfalls
- Per-frame smooth
moveTo = stutter (principle 3). The #1 follow-cam bug.
- Follow sampling order. The follow
useFrame must run AFTER whatever moves
the target (movers at priority 0 → camera at priority 1), or it intermittently
samples last frame's position — a one-frame lag that reads as jitter and
churns with subscription order.
- Pose fired at the wrong camera. After a
makeDefault swap there's a render
where the old camera is still default. Guard with
camera.isPerspectiveCamera and depend on camera so the effect re-fires
once the right one is live.
- "Focus" button pressed from the wrong mode. Store a pending flag (or
nonce +
pendingRef), force the mode switch, and let the effect land the pose
when the right camera arrives — don't setLookAt immediately and lose it.
- Runaway zoom-out. Derive
maxDistance from scene size; a fixed constant
lets a trackpad fling dolly into deep space (or clamps a big scene too tight).
- Ortho top view must not rotate. Zero
azimuthRotateSpeed/polarRotateSpeed
(tilt breaks click accuracy) and remap left-drag / one-finger to TRUCK so
navigation stays one-handed.
- Ortho frustum is yours. With a
manual OrthographicCamera, set
left/right/top/bottom from content + margin, call updateProjectionMatrix()
before framing, and zoomTo(1) — stale zoom from the last session skews the fit.
- Two writers, one frame. A pose effect and the follow smoother both writing
= the pose loses silently. Gate the pose on
followId == null or make
engaging follow the pose (chase engage).
- Punch-in distance is fov math, not taste:
dist ≈ (h/2)/tan(fov/2) for a
screen of height h to fill the frame; add ~10% margin.
- Never move the camera mid-drag of a scene object — it shifts the
pointer→world mapping under a stationary pointer (see
guided-placement).
- Timers for rides need a hard cap. A "linger then return" ride (following a
handed-off object) must also time out absolutely, or a blocked target parks
the camera forever.
1---2name: r3f-hero-camera3description: Cinematic camera rig for a React Three Fiber scene, built on drei CameraControls — a reusable shot vocabulary (overview, 3/4 object framing, screen punch-in, first-person standpoint, chase engage), a stutter-free follow cam, an idle attract orbit that breaks on user input, perspective↔orthographic mode switching, and a writer-priority ladder so competing camera drivers never fight. Use when adding camera fly-to / framing on selection, a follow or chase camera for a moving object, cinematic tour or demo beats, a "focus on X" button, an idle attract loop, or a top-down ortho editor view to a three.js / R3F project.4---56# R3F Hero Camera78One rig, one discipline: **every cinematic move is a single damped9`setLookAt(..., true)`** on one drei `<CameraControls>`; the only per-frame camera10code is one exponential smoother feeding `moveTo(..., false)` for following. No11scripted per-frame paths, no timeline library — camera-controls' own damping is12the animator, and the user keeps orbit/zoom through every shot.1314Built for **React Three Fiber 8 + drei 9 + three 0.169** (`camera-controls` under15drei). Copy-paste building blocks live in [REFERENCE.md](REFERENCE.md).1617**Sibling skills this composes with:**18- `interactive-3d-object` — hover/select FX on the objects the camera frames.19- `guided-placement` — delegates its grab-framing stage to this rig.2021## The shot vocabulary2223Every shot is a pure recipe → one `setLookAt`. `d` = the scene's long side —24recipes scale with content, never hardcode world units.2526| Shot | Recipe | Use for |27|---|---|---|28| **Overview** | eye `(d·0.8, d·0.6, d·1.1)` → scene center | establishing, reset, "complete" beat |29| **3/4 object frame** | eye = center + normalize(dir)·max(span·2.2, min) from `[1, 0.85, 1.25]` | click-to-focus on any object |30| **Screen punch-in** | eye = surface point + normal·((h/2)/tan(fov/2) + margin) | reading a diegetic screen full-frame |31| **Screen context** | eye = point + normal·far + cross(normal, up)·side | screen + its stand/surroundings |32| **First-person standpoint** | eye at ~1.6 world-height near a work point, slight lateral offset so the shot isn't dead-on | operator/visitor vantage |33| **Chase engage** | eye = object + `(2.4, 2.0, 3.0)` → object | entering follow mode |34| **Dolly beat** | `controls.dollyTo(dist, true)` only | tension change without re-aiming |3536## The writer-priority ladder3738Multiple systems want the camera. Gate every pose effect on everything above it39so exactly **one writer acts per event** — the follow smoother is the only40per-frame writer and yields to all of them:4142```43manual user override (explicit view pick)44 > tour / scripted beat shots45 > selection framing (click-to-focus)46 > follow cam (per-frame)47 > idle attract orbit48```4950## Workflow (checklist)51521. **Mount the rig** — one `<HeroCameraRig>` (REFERENCE §1): default camera +53 `<CameraControls makeDefault>` + scale-aware `maxDistance` (≈2.5× scene long54 side + a small flat term so tiny scenes don't clamp tight).552. **Add shots as effects** — each trigger (selection, beat, button nonce) is a56 `useEffect` that computes one recipe and calls `setLookAt(..., true)`57 (REFERENCE §2–3). Gate each on the ladder above it.583. **Follow cam** (moving objects) — the single-smoothing-stage `useFrame`59 (REFERENCE §4). Engage with a chase-pose `setLookAt`; seed the smoother from60 `controls.getTarget()` so the view glides over instead of snapping.614. **Attract orbit** — `azimuthAngle += rate·dt` in `useFrame`, broken by a62 `controlstart` listener, re-armed on beat change (REFERENCE §5).635. **Mode switch** (optional ortho top view) — swap `makeDefault` cameras; guard64 every pose effect with `camera.isPerspectiveCamera` / `isOrthographicCamera`65 and keep `camera` in the deps (REFERENCE §6).666. **ESC ladder** — one keydown handler releasing the most specific state first:67 tour → beat step-back → stop follow (REFERENCE §7).6869## Core principles70711. **Poses, not paths.** One damped `setLookAt` per beat. Sequenced shots =72 sequenced state changes, each firing its own pose. If a shot needs a curve,73 you almost always actually need two poses.742. **Guided vantage, never lockout.** Every shot leaves rotate/zoom live; ESC75 always releases. Lockouts read as broken, not cinematic.763. **One smoothing stage.** Per-frame `moveTo(..., smooth=true)` restarts the77 damped transition every frame on top of the target's own motion — double lag78 that reads as stutter. Smooth the target yourself79 (`k = 1 − e^(−rate·dt)`, ~12/s) and hand it over with `smooth=false`.804. **User input cancels ambient motion, never intent.** `controlstart` breaks81 attract/auto-orbit for the rest of that cycle; it never interrupts a pose the82 user themselves triggered.835. **Frame from live world transforms.** `getWorldPosition` / `getWorldQuaternion`84 on the actual object (via a ref registry), never from layout math that can85 drift from what's rendered.866. **Recipes scale with content.** Distances derive from scene size, object87 span, or fov math — a rig full of magic constants breaks on the next scene.8889## Pitfalls9091- **Per-frame smooth `moveTo` = stutter** (principle 3). The #1 follow-cam bug.92- **Follow sampling order.** The follow `useFrame` must run AFTER whatever moves93 the target (movers at priority 0 → camera at priority 1), or it intermittently94 samples last frame's position — a one-frame lag that reads as jitter and95 churns with subscription order.96- **Pose fired at the wrong camera.** After a `makeDefault` swap there's a render97 where the old camera is still default. Guard with98 `camera.isPerspectiveCamera` and depend on `camera` so the effect re-fires99 once the right one is live.100- **"Focus" button pressed from the wrong mode.** Store a pending flag (or101 nonce + `pendingRef`), force the mode switch, and let the effect land the pose102 when the right camera arrives — don't `setLookAt` immediately and lose it.103- **Runaway zoom-out.** Derive `maxDistance` from scene size; a fixed constant104 lets a trackpad fling dolly into deep space (or clamps a big scene too tight).105- **Ortho top view must not rotate.** Zero `azimuthRotateSpeed`/`polarRotateSpeed`106 (tilt breaks click accuracy) and remap left-drag / one-finger to TRUCK so107 navigation stays one-handed.108- **Ortho frustum is yours.** With a `manual` OrthographicCamera, set109 left/right/top/bottom from content + margin, call `updateProjectionMatrix()`110 before framing, and `zoomTo(1)` — stale zoom from the last session skews the fit.111- **Two writers, one frame.** A pose effect and the follow smoother both writing112 = the pose loses silently. Gate the pose on `followId == null` or make113 engaging follow the pose (chase engage).114- **Punch-in distance is fov math**, not taste: `dist ≈ (h/2)/tan(fov/2)` for a115 screen of height `h` to fill the frame; add ~10% margin.116- **Never move the camera mid-drag** of a scene object — it shifts the117 pointer→world mapping under a stationary pointer (see `guided-placement`).118- **Timers for rides need a hard cap.** A "linger then return" ride (following a119 handed-off object) must also time out absolutely, or a blocked target parks120 the camera forever.