threlte logo

camera-controls

You may have come up against limitations with Three.js’s <OrbitalControls/>. Camera Controls is an alternative which supports smooth transitions and a variety of features.

Thes example below demonstrates how to incorporate the camera controls library into threlte.

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

  const mesh = new Mesh()
  let controls: CameraControls
</script>

<Pane
  title="Camera Controls"
  position="fixed"
>
  <Button
    title="rotate theta 45deg"
    on:click={() => {
      controls?.rotate(45 * DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate theta -90deg"
    on:click={() => {
      controls?.rotate(-90 * DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate theta 360deg"
    on:click={() => {
      controls?.rotate(360 * DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate phi 20deg"
    on:click={() => {
      controls?.rotate(0, 20 * 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="move to ( 3, 5, 2)"
    on:click={() => {
      controls?.moveTo(3, 5, 2, true)
    }}
  />
  <Button
    title="fit to the bounding box of the mesh"
    on:click={() => {
      controls?.fitToBox(mesh, true)
    }}
  />
  <Separator />
  <Button
    title="set position to ( -5, 2, 1 )"
    on:click={() => {
      controls?.setPosition(-5, 2, 1, true)
    }}
  />
  <Button
    title="look at ( 3, 0, -3 )"
    on:click={() => {
      controls?.setTarget(3, 0, -3, true)
    }}
  />
  <Button
    title="move to ( 1, 2, 3 ), look at ( 1, 1, 0 )"
    on:click={() => {
      controls?.setLookAt(1, 2, 3, 1, 1, 0, true)
    }}
  />
  <Separator />
  <Button
    title="move to somewhere between ( -2, 0, 0 ) -> ( 1, 1, 0 ) and ( 0, 2, 5 ) -> ( -1, 0, 0 )"
    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 />
  {#if controls !== undefined}
    <Checkbox
      bind:value={controls.enabled}
      label="enabled"
    />
  {/if}
</Pane>

<div>
  <Canvas>
    <Scene
      {mesh}
      bind:controls
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
import { useTask, useThrelte } from '@threlte/core'
import CC from 'camera-controls'
import { onDestroy } from 'svelte'
import type { OrthographicCamera, PerspectiveCamera } from 'three'
import {
  Box3,
  Matrix4,
  Quaternion,
  Raycaster,
  Sphere,
  Spherical,
  Vector2,
  Vector3,
  Vector4
} from 'three'

export default class CameraControls extends CC {
  static installed = false
  constructor(camera: OrthographicCamera | PerspectiveCamera, element: HTMLElement) {
    if (!CameraControls.installed) {
      CC.install({
        THREE: {
          Box3,
          Matrix4,
          Quaternion,
          Raycaster,
          Sphere,
          Spherical,
          Vector2,
          Vector3,
          Vector4
        }
      })
      CameraControls.installed = true
    }

    super(camera)

    const { invalidate } = useThrelte()

    this.connect(element)

    onDestroy(() => {
      this.dispose()
    })

    useTask(
      (delta) => {
        if (this.update(delta)) {
          invalidate()
        }
      },
      { autoInvalidate: false }
    )
  }
}
<script lang="ts">
  import { T, useThrelte } from '@threlte/core'
  import { Grid } from '@threlte/extras'
  import CC from 'camera-controls'
  import { Mesh, PerspectiveCamera } from 'three'
  import CameraControls from './CameraControls.svelte'

  const { dom } = useThrelte()

  type Props = {
    controls: CC
    mesh: Mesh
  }

  let { controls = $bindable(), mesh }: Props = $props()

  const camera = new PerspectiveCamera()
  controls = new CameraControls(camera, dom)
  controls.setPosition(5, 5, 5)
</script>

<T
  is={camera}
  makeDefault
/>

<T.DirectionalLight position={[3, 10, 7]} />

<T
  is={mesh}
  position.y={0.5}
>
  <T.BoxGeometry />
  <T.MeshBasicMaterial
    color="#ff3e00"
    wireframe
  />
</T>

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

The CameraControls class in the example extends the camera controls library’s CameraControls class.

It automatically installs camera-controls if not already installed, connects and disconnects to the dom element that is sent into its constructor, updates the controls and potentially invalidates the scene and cleans up after itself when the component it is instantiated in is destroyed.

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