ThirdPersonCamera
Inspired by SimonDev’s ThirdPersonCamera.
Use ‘W’ and ‘S’ to move forward and backwards, and ‘A’ and ‘D’ to rotate the camera.
<script lang="ts">
import { Pane, Text } from 'svelte-tweakpane-ui'
import { Canvas } from '@threlte/core'
import { World } from '@threlte/rapier'
import Scene from './Scene.svelte'
</script>
<Pane
position="fixed"
title="third-person"
>
<Text
value="Use the 'wasd' keys to move around"
disabled
/>
</Pane>
<div>
<Canvas>
<World>
<Scene />
</World>
</Canvas>
</div>
<style>
div {
position: relative;
height: 100%;
width: 100%;
}
</style>
<script lang="ts">
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
import { T } from '@threlte/core'
import { HTML } from '@threlte/extras'
import { AutoColliders, Collider, CollisionGroups, RigidBody } from '@threlte/rapier'
import { cubicIn, cubicOut } from 'svelte/easing'
import { tweened } from 'svelte/motion'
import { blur } from 'svelte/transition'
import {
BoxGeometry,
Euler,
type Group,
MeshStandardMaterial,
Quaternion,
MathUtils
} from 'three'
let open = false
let objectsInSensor = 0
$: open = objectsInSensor > 0
let group: Group
let doorRigidBody: RapierRigidBody
let doorRotationClosed = 0
let doorRotationOpen = -105 * MathUtils.DEG2RAD
let doorRotation = tweened(doorRotationClosed)
$: doorRotation.set(open ? doorRotationOpen : doorRotationClosed, {
easing: open ? cubicOut : cubicIn
})
const q = new Quaternion()
const e = new Euler()
const applyDoorRotation = (rotation: number) => {
if (!group || !doorRigidBody) return
group.getWorldQuaternion(q)
e.setFromQuaternion(q)
e.y += rotation
q.setFromEuler(e)
doorRigidBody.setNextKinematicRotation(q)
}
$: if (group && doorRigidBody) applyDoorRotation($doorRotation)
</script>
@@ -1,152 +0,0 @@
<T.Group bind:ref={group}>
<!-- FRAME -->
<AutoColliders shape={'cuboid'}>
<!-- SIDE FRAME A -->
<T.Mesh
receiveShadow
castShadow
position={[0.7, 1.125, 0]}
geometry={new BoxGeometry(0.3, 2.25, 0.3)}
material={new MeshStandardMaterial()}
/>
<!-- SIDE FRAME B -->
<T.Mesh
receiveShadow
castShadow
position={[-0.7, 1.125, 0]}
geometry={new BoxGeometry(0.3, 2.25, 0.3)}
material={new MeshStandardMaterial()}
/>
<!-- TOP FRAME -->
<T.Mesh
receiveShadow
castShadow
position.y={2.4}
geometry={new BoxGeometry(1.4 + 0.3, 0.3, 0.3)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
<HTML
transform
position.y={3}
pointerEvents={'none'}
>
{#key open}
<small
in:blur={{
amount: 15,
duration: 300
}}
out:blur={{
amount: 15,
duration: 300
}}
class="door"
class:closed={!open}
class:open
>
{open ? 'UNLOCKED' : 'LOCKED'}
</small>
{/key}
</HTML>
<!-- DOOR -->
<T.Group position={[-0.5, 1.125, 0]}>
<RigidBody
bind:rigidBody={doorRigidBody}
type={'kinematicPosition'}
>
<AutoColliders shape={'cuboid'}>
<T.Mesh
receiveShadow
castShadow
position.x={0.5}
geometry={new BoxGeometry(1, 2.25, 0.1)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
</RigidBody>
</T.Group>
<CollisionGroups groups={[15]}>
<T.Group position={[0, 1.5, 0]}>
<Collider
shape={'cuboid'}
args={[1, 1.35, 1.5]}
sensor
onsensorenter={() => (objectsInSensor += 1)}
onsensorexit={() => (objectsInSensor -= 1)}
/>
</T.Group>
</CollisionGroups>
</T.Group>
<style>
.door {
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
color: rgb(255, 255, 255);
border-radius: 0.375rem;
position: absolute;
transform: translateX(-50%) translateY(-50%);
}
.closed {
background-color: rgb(239, 68, 68);
}
.open {
background-color: rgb(34, 197, 94);
}
</style>
<script lang="ts">
import { CapsuleGeometry, Euler, Vector3 } from 'three'
import { T, useTask, useThrelte } from '@threlte/core'
import { RigidBody, CollisionGroups, Collider } from '@threlte/rapier'
import { createEventDispatcher } from 'svelte'
import Controller from './ThirdPersonControls.svelte'
export let position = [0, 3, 5]
export let radius = 0.3
export let height = 1.7
export let speed = 6
let capsule
let capRef
$: if (capsule) {
capRef = capsule
}
let rigidBody
let forward = 0
let backward = 0
let left = 0
let right = 0
const temp = new Vector3()
const dispatch = createEventDispatcher()
let grounded = false
$: grounded ? dispatch('groundenter') : dispatch('groundexit')
useTask(() => {
if (!rigidBody || !capsule) return
// get direction
const velVec = temp.fromArray([0, 0, forward - backward]) // left - right
// sort rotate and multiply by speed
velVec.applyEuler(new Euler().copy(capsule.rotation)).multiplyScalar(speed)
// don't override falling velocity
const linVel = rigidBody.linvel()
temp.y = linVel.y
// finally set the velocities and wake up the body
rigidBody.setLinvel(temp, true)
// when body position changes update camera position
const pos = rigidBody.translation()
position = [pos.x, pos.y, pos.z]
})
function onKeyDown(e: KeyboardEvent) {
switch (e.key) {
case 's':
backward = 1
break
case 'w':
forward = 1
break
case 'a':
left = 1
break
case 'd':
right = 1
break
default:
break
}
}
function onKeyUp(e: KeyboardEvent) {
switch (e.key) {
case 's':
backward = 0
break
case 'w':
forward = 0
break
case 'a':
left = 0
break
case 'd':
right = 0
break
default:
break
}
}
</script>
<svelte:window
on:keydown|preventDefault={onKeyDown}
on:keyup={onKeyUp}
/>
<T.PerspectiveCamera
makeDefault
fov={90}
>
<Controller bind:object={capRef} />
</T.PerspectiveCamera>
<T.Group
bind:ref={capsule}
{position}
rotation.y={Math.PI}
>
<RigidBody
bind:rigidBody
enabledRotations={[false, false, false]}
>
<CollisionGroups groups={[0]}>
<Collider
shape={'capsule'}
args={[height / 2 - radius, radius]}
/>
<T.Mesh geometry={new CapsuleGeometry(0.3, 1.8 - 0.3 * 2)} />
</CollisionGroups>
<CollisionGroups groups={[15]}>
<Collider
sensor
shape={'ball'}
args={[radius * 1.2]}
position={[0, -height / 2 + radius, 0]}
/>
</CollisionGroups>
</RigidBody>
</T.Group>
<script>
import { T } from '@threlte/core'
import { AutoColliders, CollisionGroups, Debug } from '@threlte/rapier'
import { BoxGeometry, MeshStandardMaterial } from 'three'
import Door from './Door.svelte'
import Player from './Player.svelte'
</script>
<T.DirectionalLight
castShadow
position={[8, 20, -3]}
/>
<T.AmbientLight intensity={0.2} />
<Debug />
<T.GridHelper
args={[50]}
position.y={0.01}
/>
<CollisionGroups groups={[0, 15]}>
<AutoColliders
shape={'cuboid'}
position={[0, -0.5, 0]}
>
<T.Mesh
receiveShadow
geometry={new BoxGeometry(100, 1, 100)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
</CollisionGroups>
<CollisionGroups groups={[0]}>
<!-- position={{ x: 2 }} -->
<Player />
<Door />
<!-- WALLS -->
<AutoColliders shape={'cuboid'}>
<T.Mesh
receiveShadow
castShadow
position.x={30 + 0.7 + 0.15}
position.y={1.275}
geometry={new BoxGeometry(60, 2.55, 0.15)}
material={new MeshStandardMaterial({
transparent: true,
opacity: 0.5,
color: 0x333333
})}
/>
<T.Mesh
receiveShadow
castShadow
position.x={-30 - 0.7 - 0.15}
position.y={1.275}
geometry={new BoxGeometry(60, 2.55, 0.15)}
material={new MeshStandardMaterial({
transparent: true,
opacity: 0.5,
color: 0x333333
})}
/>
</AutoColliders>
</CollisionGroups>
<script lang="ts">
import { isInstanceOf, useParent, useTask, useThrelte } from '@threlte/core'
import { createEventDispatcher, onDestroy } from 'svelte'
import { Quaternion, Vector2, Vector3 } from 'three'
export let object
export let rotateSpeed = 1.0
$: if (object) {
// console.log(object)
// object.position.y = 10
// // Calculate the direction vector towards (0, 0, 0)
// const target = new Vector3(0, 0, 0)
// const direction = target.clone().sub(object.position).normalize()
// // Extract the forward direction from the object's current rotation matrix
// const currentDirection = new Vector3(0, 1, 0)
// currentDirection.applyQuaternion(object.quaternion)
// // Calculate the axis and angle to rotate the object
// const rotationAxis = currentDirection.clone().cross(direction).normalize()
// const rotationAngle = Math.acos(currentDirection.dot(direction))
// // Rotate the object using rotateOnAxis()
// object.rotateOnAxis(rotationAxis, rotationAngle)
}
export let idealOffset = { x: -0.5, y: 2, z: -3 }
export let idealLookAt = { x: 0, y: 1, z: 5 }
const currentPosition = new Vector3()
const currentLookAt = new Vector3()
let isOrbiting = false
let pointerDown = false
const rotateStart = new Vector2()
const rotateEnd = new Vector2()
const rotateDelta = new Vector2()
const axis = new Vector3(0, 1, 0)
const rotationQuat = new Quaternion()
const { renderer, invalidate } = useThrelte()
const domElement = renderer.domElement
const camera = useParent()
const dispatch = createEventDispatcher()
if (!isInstanceOf($camera, 'Camera')) {
throw new Error('Parent missing: <PointerLockControls> need to be a child of a <Camera>')
}
domElement.addEventListener('pointerdown', onPointerDown)
domElement.addEventListener('pointermove', onPointerMove)
domElement.addEventListener('pointerleave', onPointerLeave)
domElement.addEventListener('pointerup', onPointerUp)
onDestroy(() => {
domElement.removeEventListener('pointerdown', onPointerDown)
domElement.removeEventListener('pointermove', onPointerMove)
domElement.removeEventListener('pointerleave', onPointerLeave)
domElement.removeEventListener('pointerup', onPointerUp)
})
// This is basically your update function
useTask((delta) => {
// the object's position is bound to the prop
if (!object) return
// camera is based on character so we rotation character first
rotationQuat.setFromAxisAngle(axis, -rotateDelta.x * rotateSpeed * delta)
object.quaternion.multiply(rotationQuat)
// then we calculate our ideal's
const offset = vectorFromObject(idealOffset)
const lookAt = vectorFromObject(idealLookAt)
// and how far we should move towards them
const t = 1.0 - Math.pow(0.001, delta)
currentPosition.lerp(offset, t)
currentLookAt.lerp(lookAt, t)
// then finally set the camera
$camera.position.copy(currentPosition)
$camera.lookAt(currentLookAt)
})
function onPointerMove(event: PointerEvent) {
const { x, y } = event
if (pointerDown && !isOrbiting) {
// calculate distance from init down
const distCheck =
Math.sqrt(Math.pow(x - rotateStart.x, 2) + Math.pow(y - rotateStart.y, 2)) > 10
if (distCheck) {
isOrbiting = true
}
}
if (!isOrbiting) return
rotateEnd.set(x, y)
rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(rotateSpeed)
rotateStart.copy(rotateEnd)
invalidate()
dispatch('change')
}
function onPointerDown(event: PointerEvent) {
const { x, y } = event
rotateStart.set(x, y)
pointerDown = true
}
function onPointerUp() {
rotateDelta.set(0, 0)
pointerDown = false
isOrbiting = false
}
function onPointerLeave() {
rotateDelta.set(0, 0)
pointerDown = false
isOrbiting = false
}
function vectorFromObject(vec: { x: number; y: number; z: number }) {
const { x, y, z } = vec
const ideal = new Vector3(x, y, z)
ideal.applyQuaternion(object.quaternion)
ideal.add(new Vector3(object.position.x, object.position.y, object.position.z))
return ideal
}
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'a':
rotateDelta.x = -2 * rotateSpeed
break
case 'd':
rotateDelta.x = 2 * rotateSpeed
break
default:
break
}
}
function onKeyUp(event: KeyboardEvent) {
switch (event.key) {
case 'a':
rotateDelta.x = 0
break
case 'd':
rotateDelta.x = 0
break
default:
break
}
}
</script>
<svelte:window
on:keydown={onKeyDown}
on:keyup={onKeyUp}
/>