threlte logo
@threlte/extras

<CameraControls>

webgl <CameraControls> works with WebGL and is available from the @threlte/extras export. webgpu <CameraControls> works with WebGPU and is available from the @threlte/extras/webgpu export.

This component is a declarative implementation of the popular camera-controls library.

<script lang="ts">
  import Scene from './Scene.svelte'
  import { Button, Checkbox, Pane, Separator } from 'svelte-tweakpane-ui'
  import { Canvas } from '@threlte/core'
  import type { CameraControlsRef } from '@threlte/extras'
  import { type Mesh, MathUtils } from 'three'

  let controls = $state.raw<CameraControlsRef>()
  let mesh = $state.raw<Mesh>()

  /**
   * controls.enabled can not be bound to since its not reactive
   */
  let enabled = $state(true)
  $effect(() => {
    if (controls !== undefined) {
      controls.enabled = enabled
    }
  })
</script>

<Pane
  title="Camera Controls"
  position="fixed"
>
  <Button
    title="rotate(45deg, 0)"
    on:click={() => {
      controls?.rotate(45 * MathUtils.DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate(-90deg, 0)"
    on:click={() => {
      controls?.rotate(-90 * MathUtils.DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate(360deg, 0)"
    on:click={() => {
      controls?.rotate(360 * MathUtils.DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate(0, 20deg)"
    on:click={() => {
      controls?.rotate(0, 20 * MathUtils.DEG2RAD, true)
    }}
  />
  <Separator />
  <Button
    title="truck(1, 0)"
    on:click={() => {
      controls?.truck(1, 0, true)
    }}
  />
  <Button
    title="truck(0, 1)"
    on:click={() => {
      controls?.truck(0, 1, true)
    }}
  />
  <Button
    title="truck(-1, -1)"
    on:click={() => {
      controls?.truck(-1, -1, true)
    }}
  />
  <Separator />
  <Button
    title="dolly(1)"
    on:click={() => {
      controls?.dolly(1, true)
    }}
  />
  <Button
    title="dolly(-1)"
    on:click={() => {
      controls?.dolly(-1, true)
    }}
  />
  <Separator />
  <Button
    title="zoom(camera.zoom / 2)"
    on:click={() => {
      controls?.zoom(controls.camera.zoom / 2, true)
    }}
  />
  <Button
    title="zoom(-camera.zoom / 2)"
    on:click={() => {
      controls?.zoom(-controls.camera.zoom / 2, true)
    }}
  />
  <Separator />
  <Button
    title="moveTo(3, 5, 2)"
    on:click={() => {
      controls?.moveTo(3, 5, 2, true)
    }}
  />
  <Button
    title="fitToBox(mesh)"
    on:click={() => {
      if (mesh !== undefined) {
        controls?.fitToBox(mesh, true)
      }
    }}
  />
  <Separator />
  <Button
    title="setPosition(-5, 2, 1)"
    on:click={() => {
      controls?.setPosition(-5, 2, 1, true)
    }}
  />
  <Button
    title="setTarget(3, 0, -3)"
    on:click={() => {
      controls?.setTarget(3, 0, -3, true)
    }}
  />
  <Button
    title="setLookAt(1, 2, 3, 1, 1, 0)"
    on:click={() => {
      controls?.setLookAt(1, 2, 3, 1, 1, 0, true)
    }}
  />
  <Separator />
  <Button
    title="lerpLookAt(-2,0,0,1,1,0,0,2,5,-1,0,0,random())"
    on:click={() => {
      controls?.lerpLookAt(-2, 0, 0, 1, 1, 0, 0, 2, 5, -1, 0, 0, Math.random(), true)
    }}
  />
  <Separator />
  <Button
    title="reset()"
    on:click={() => {
      controls?.reset(true)
    }}
  />
  <Button
    title="saveState()"
    on:click={() => {
      controls?.saveState()
    }}
  />
  <Separator />
  <Checkbox
    bind:value={enabled}
    label="enabled"
  />
</Pane>

<Canvas>
  <Scene
    bind:controls
    bind:mesh
  />
</Canvas>
<script lang="ts">
  import { Mesh } from 'three'
  import { T } from '@threlte/core'
  import { Grid, CameraControls, type CameraControlsRef } from '@threlte/extras'

  interface Props {
    controls?: CameraControlsRef
    mesh?: Mesh
  }

  let { controls = $bindable(), mesh = $bindable() }: Props = $props()
</script>

<CameraControls
  bind:ref={controls}
  oncreate={(ref) => {
    ref.setPosition(5, 5, 5)
  }}
/>

<T.Mesh
  bind:ref={mesh}
  position.y={0.5}
>
  <T.BoxGeometry />
  <T.MeshBasicMaterial
    color="#ff3e00"
    wireframe
  />
</T.Mesh>

<Grid
  sectionColor="#ff3e00"
  sectionThickness={1}
  cellColor="#cccccc"
  gridSize={40}
/>

If the controls are set as a child component of a camera, they will attach to that camera.

<T.PerspectiveCamera makeDefault>
  <CameraControls />
</T.PerspectiveCamera>

A camera can also optionally be passed to the controls as a prop.

<CameraControls camera={myPerspectiveCamera} />

Finally, if the component is created without an attached camera it will use the scene’s default camera as provided by useThrelte.

useCameraControls

To access the underlying CameraControls instance from another component — without a bind:ref — use the useCameraControls hook. It returns a reactive accessor whose current is the instance registered by the nearest <CameraControls> within the <Canvas>, or undefined until it has mounted.

FrameButton.svelte
<script lang="ts">
  import { useCameraControls } from '@threlte/extras'

  const controls = useCameraControls()
</script>

<button onclick={() => controls.current?.setLookAt(5, 5, 5, 0, 0, 0, true)}> Frame scene </button>

Examples

Basic Example

CameraControls.svelte
<script lang="ts">
  import { CameraControls, type CameraControlsRef } from '@threlte/extras'

  let controls = $state<CameraControlsRef>()

  $effect.pre(() => {
    controls?.truck(1, 0, true)
  })
</script>

<CameraControls
  bind:ref={controls}
  oncreate={(ref) => ref.setPosition(5, 5, 5)}
/>

Prevent SSR Externalization

If you are using SvelteKit or Vite for building your app, you may need to externalize the camera-controls library.

To externalize the camera-controls library put the following in your vite.config.js or vite.config.ts.

// vite.config.ts
export default defineConfig({
  plugins: [sveltekit()],
  ssr: {
    noExternal: ['camera-controls']
  }
})

The camera-controls package features include first-person, third-person, pointer-lock, fit-to-bounding-sphere and much more!

Component Signature

<CameraControls> extends < T . CameraControls > and supports all its props, snippets, bindings and events.

Props

name
type
required
default
description

camera
PerspectiveCamera | OrthographicCamera
no
The camera to control. Falls back to the parent camera when nested inside one, then to Threlte's default camera.

pointerLock
boolean
no
false
Enables pointer-lock rotation. Clicking the canvas requests a pointer lock, after which pointer movement rotates the camera directly, without holding a button. The left, middle and right mouseButtons actions are set to NONE while it's on so drag gestures don't fire, and the wheel still dollies.

pointerLockSensitivity
number
no
0.003
Radians of camera rotation per pixel of pointer movement while the pointer is locked.

Bindings

name
type
description

ref
CameraControls
The underlying CameraControls instance.