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)
})
}