threlte logo

camera-controls

You may have come up against limitations with <OrbitalControls/> from three.js. Camera Controls is an existing project which supports smooth transitions and has many more features.

The example below has a component with a basic implementation of camera-controls and functions equivelant to this camera-controls doc example. Your project may need specific features in which case, visit their docs and adjust the component to suit.

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

<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Pane, Button, Separator } from 'svelte-tweakpane-ui'
  import { cameraControls, mesh } from './stores'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'

  let camera

  $: if ($cameraControls) {
    camera = $cameraControls._camera
  }
</script>

<Pane
  title="Camera Controls"
  position="fixed"
>
  <Button
    title="rotate theta 45deg"
    on:click={() => {
      $cameraControls.rotate(45 * DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate theta -90deg"
    on:click={() => {
      $cameraControls.rotate(-90 * DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate theta 360deg"
    on:click={() => {
      $cameraControls.rotate(360 * DEG2RAD, 0, true)
    }}
  />
  <Button
    title="rotate phi 20deg"
    on:click={() => {
      $cameraControls.rotate(0, 20 * DEG2RAD, true)
    }}
  />
  <Separator />
  <Button
    title="truck(1, 0)"
    on:click={() => {
      $cameraControls.truck(1, 0, true)
    }}
  />
  <Button
    title="truck(0, 1)"
    on:click={() => {
      $cameraControls.truck(0, 1, true)
    }}
  />
  <Button
    title="truck(-1, -1)"
    on:click={() => {
      $cameraControls.truck(-1, -1, true)
    }}
  />
  <Separator />
  <Button
    title="dolly 1"
    on:click={() => {
      $cameraControls.dolly(1, true)
    }}
  />
  <Button
    title="dolly -1"
    on:click={() => {
      $cameraControls.dolly(-1, true)
    }}
  />
  <Separator />
  <Button
    title="zoom `camera.zoom / 2`"
    on:click={() => {
      $cameraControls.zoom(camera.zoom / 2, true)
    }}
  />
  <Button
    title="zoom `- camera.zoom / 2`"
    on:click={() => {
      $cameraControls.zoom(-camera.zoom / 2, true)
    }}
  />
  <Separator />
  <Button
    title="move to ( 3, 5, 2)"
    on:click={() => {
      $cameraControls.moveTo(3, 5, 2, true)
    }}
  />
  <Button
    title="fit to the bounding box of the mesh"
    on:click={() => {
      $cameraControls.fitToBox($mesh, true)
    }}
  />
  <Separator />
  <Button
    title="move to ( -5, 2, 1 )"
    on:click={() => {
      $cameraControls.setPosition(-5, 2, 1, true)
    }}
  />
  <Button
    title="look at ( 3, 0, -3 )"
    on:click={() => {
      $cameraControls.setTarget(3, 0, -3, true)
    }}
  />
  <Button
    title="move to ( 1, 2, 3 ), look at ( 1, 1, 0 )"
    on:click={() => {
      $cameraControls.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={() => {
      $cameraControls.lerpLookAt(-2, 0, 0, 1, 1, 0, 0, 2, 5, -1, 0, 0, Math.random(), true)
    }}
  />
  <Separator />
  <Button
    title="reset"
    on:click={() => {
      $cameraControls.reset(true)
    }}
  />
  <Button
    title="saveState"
    on:click={() => {
      $cameraControls.saveState(true)
    }}
  />
  <Separator />
  <Button
    title="disable mouse/touch controls"
    on:click={() => {
      $cameraControls.enabled = false
    }}
  />
  <Button
    title="enable mouse/touch controls"
    on:click={() => {
      $cameraControls.enabled = true
    }}
  />
</Pane>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script
  context="module"
  lang="ts"
>
  import { T, useTask, useParent, useThrelte, type Props } from '@threlte/core'
  import {
    Box3,
    Matrix4,
    Quaternion,
    Raycaster,
    Sphere,
    Spherical,
    Vector2,
    Vector3,
    Vector4,
    type PerspectiveCamera,
    MathUtils
  } from 'three'
  import CameraControls from 'camera-controls'

  CameraControls.install({
    THREE: {
      Vector2,
      Vector3,
      Vector4,
      Quaternion,
      Matrix4,
      Spherical,
      Box3,
      Sphere,
      Raycaster
    }
  })
</script>

<script lang="ts">
  interface CameraControlsProps extends Props<CameraControls> {
    ref: CameraControls
    autoRotate?: boolean
    autoRotateSpeed?: number
  }

  let {
    autoRotate = false,
    autoRotateSpeed = 1,
    ref = $bindable(),
    ...props
  }: CameraControlsProps = $props()

  const parent = useParent()

  if (!$parent) {
    throw new Error('CameraControls must be a child of a ThreeJS camera')
  }

  const { renderer, invalidate } = useThrelte()

  const controls = new CameraControls($parent as PerspectiveCamera, renderer.domElement)

  let disableAutoRotate = false

  useTask(
    (delta) => {
      if (autoRotate && !disableAutoRotate) {
        controls.azimuthAngle += 4 * delta * MathUtils.DEG2RAD * autoRotateSpeed
      }
      const updated = controls.update(delta)
      if (updated) invalidate()
    },
    {
      autoInvalidate: false
    }
  )
</script>

<T
  is={controls}
  bind:ref
  oncontrolstart={() => {
    disableAutoRotate = true
  }}
  onzoom={(event) => {
    console.log('zoomstart', event)
  }}
  oncontrolend={() => {
    disableAutoRotate = false
  }}
  {...props}
>
  <slot {ref} />
</T>
<script>
  import { T, useTask } from '@threlte/core'
  import { Grid } from '@threlte/extras'
  import CameraControls from './CameraControls.svelte'
  import { cameraControls, mesh } from './stores'
</script>

<T.PerspectiveCamera
  makeDefault
  position={[10, 10, 10]}
  oncreate={(ref) => {
    ref.lookAt(0, 1, 0)
  }}
>
  <CameraControls
    oncreate={(ref) => {
      $cameraControls = ref
    }}
  />
</T.PerspectiveCamera>

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

<T.Mesh
  position.y={1}
  oncreate={(ref) => {
    $mesh = ref
  }}
>
  <T.BoxGeometry args={[1, 1, 1]} />
  <T.MeshBasicMaterial
    color="red"
    wireframe={true}
  />
</T.Mesh>

<Grid
  sectionColor={'#ff3e00'}
  sectionThickness={1}
  cellColor={'#cccccc'}
  gridSize={40}
/>
import { writable } from 'svelte/store'

export const cameraControls = writable(undefined)
export const mesh = writable(undefined)
import { useThrelteUserContext } from '@threlte/core'
import { writable, type Writable } from 'svelte/store'
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

type ControlsContext = {
  orbitControls: Writable<OrbitControls | undefined>
}

/**
 * ### `useControlsContext`
 *
 * This hook is used to register the `OrbitControls` instance with the
 * `ControlsContext`. We're using this context to enable and disable the
 * controls when the user is interacting with the TransformControls.
 */
export const useControlsContext = (): ControlsContext => {
  return useThrelteUserContext<ControlsContext>('threlte-controls', {
    orbitControls: writable<OrbitControls | undefined>(undefined)
  })
}