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 { 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 '../../rapier/world/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 { createEventDispatcher, onDestroy } from 'svelte'
import { Camera, Vector2, Vector3, Quaternion } from 'three'
import { useThrelte, useParent, useTask } from '@threlte/core'
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()
const isCamera = (p: any): p is Camera => {
return p.isCamera
}
if (!isCamera($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}
/>