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
  let paneExpanded = false

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

<Pane
  title="Camera Controls"
  position="fixed"
  bind:expanded={paneExpanded}
>
  <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"
>
  let installed = false
</script>

<script lang="ts">
  import { T, forwardEventHandlers, useTask, useParent, useThrelte } from '@threlte/core'
  import type {
    CameraControlsEvents,
    CameraControlsProps,
    CameraControlsSlots
  } from './CameraControls.svelte'

  type $$Props = CameraControlsProps
  type $$Events = CameraControlsEvents
  type $$Slots = CameraControlsSlots

  import CameraControls from 'camera-controls'
  import {
    Box3,
    Matrix4,
    Quaternion,
    Raycaster,
    Sphere,
    Spherical,
    Vector2,
    Vector3,
    Vector4,
    type PerspectiveCamera
  } from 'three'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'

  const subsetOfTHREE = {
    Vector2,
    Vector3,
    Vector4,
    Quaternion,
    Matrix4,
    Spherical,
    Box3,
    Sphere,
    Raycaster
  }

  if (!installed) {
    CameraControls.install({ THREE: subsetOfTHREE })
    installed = true
  }

  const parent = useParent()

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

  const { renderer, invalidate } = useThrelte()

  export let autoRotate = false
  export let autoRotateSpeed = 1

  export const ref = new CameraControls($parent as PerspectiveCamera, renderer?.domElement)

  const getControls = () => ref

  let disableAutoRotate = false

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

  const forwardingComponent = forwardEventHandlers()
</script>

<T
  is={ref}
  on:controlstart={(e) => {
    disableAutoRotate = true
  }}
  on:zoom={(e) => {
    console.log('zoomstart', e)
  }}
  on:controlend={() => {
    disableAutoRotate = false
  }}
  {...$$restProps}
  bind:this={$forwardingComponent}
>
  <slot {ref} />
</T>
import type { Events, Props, Slots } from '@threlte/core'
import CC from 'camera-controls'
import type { SvelteComponent } from 'svelte'

export type CameraControlsProps = Props<CC> & {
  autoRotate?: boolean
  autoRotateSpeed?: number
}
export type CameraControlsEvents = Events<CC>
export type CameraControlsSlots = Slots<CC>

export default class CameraControls extends SvelteComponent<
  CameraControlsProps,
  CameraControlsEvents,
  CameraControlsSlots
> {}
<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]}
  on:create={({ ref }) => {
    ref.lookAt(0, 1, 0)
  }}
>
  <CameraControls
    on:create={({ ref }) => {
      $cameraControls = ref
    }}
  />
</T.PerspectiveCamera>

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

<T.Mesh
  position.y={1}
  on:create={({ 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)
  })
}