Remove physics engine. Use raycasters for collisions

master
Ian Mancini 4 years ago
parent 4167ecd248
commit a143c7b1e3

@ -0,0 +1,231 @@
import React, { useEffect, useRef, useState } from 'react'
import { useThree, useFrame } from 'react-three-fiber'
import {
Object3D,
Geometry,
Event,
Group,
PerspectiveCamera,
Vector3,
Raycaster,
Mesh,
CircleGeometry,
} from 'three'
import WorldCollisions from './models/WorldCollisions'
import useStore from '../store'
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial'
const SPEED = 1
const HEIGHT = 1.5
const CIRCLE_RADIUS = 0.9
const CIRCLE_SEGMENTS = 8
const keys: Record<string, string> = {
KeyW: 'forward',
KeyS: 'backward',
KeyA: 'left',
KeyD: 'right',
ArrowUp: 'forward',
ArrowDown: 'backward',
ArrowLeft: 'left',
ArrowRight: 'right',
ShiftLeft: 'run',
Space: 'jump',
}
const moveFieldByKey = (key: string) => keys[key]
function FirstPersonCamera(props: JSX.IntrinsicElements['perspectiveCamera']) {
const ref = useRef<PerspectiveCamera>()
const { setDefaultCamera } = useThree()
// Make the camera known to the system
useEffect(() => {
if (ref.current) setDefaultCamera(ref.current)
}, [ref.current])
// Update it every frame
useFrame(() => ref.current?.updateMatrixWorld())
return <perspectiveCamera ref={ref} {...props} />
}
const usePlayerControls = () => {
const [movement, setMovement] = useState({
forward: false,
backward: false,
left: false,
right: false,
run: false,
jump: false,
})
useEffect(() => {
const handleKeyDown = (e: Event) => {
setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: true }))
}
const handleKeyUp = (e: Event) => {
setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: false }))
}
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [])
return movement
}
// TODO Improve physics in player
const Player = () => {
const socket = useStore((state) => state.socket)
const pointerLocked = useStore((state) => state.pointerLocked)
const { forward, backward, left, right, run } = usePlayerControls()
const { camera } = useThree()
const groupRef = useRef<Group>()
const collisionsRef = useRef<Mesh<CircleGeometry, MeshBasicMaterial>>(null)
const bottomRaycaster = useRef(
new Raycaster(new Vector3(), new Vector3(0, -1, 0), 0, HEIGHT + 0.5),
)
const collisionCircle = useRef<Mesh>()
const rotationObject = new Object3D()
rotationObject.rotation.x = Math.PI / 2
rotationObject.updateMatrix()
const collisionCircleGeometry = new CircleGeometry(CIRCLE_RADIUS, CIRCLE_SEGMENTS)
collisionCircleGeometry.applyMatrix4(rotationObject.matrix)
const collisionCircleMaterial = new MeshBasicMaterial({
color: 0xff0000,
wireframe: true,
})
const wallRaycasters = useRef<Raycaster[]>(
Array(CIRCLE_SEGMENTS)
.fill(undefined)
.map(() => new Raycaster(new Vector3(), new Vector3(0, 0, -1), 0, CIRCLE_RADIUS)),
)
useEffect(() => {
const socketEmitTransformInterval = setInterval(() => {
if (socket && groupRef.current && camera) {
const cameraRotation = camera.rotation.toArray()
socket.emit('transform', {
position: [
groupRef.current?.position.x,
groupRef.current?.position.y,
groupRef.current?.position.z,
],
rotation: [cameraRotation[0], cameraRotation[1], cameraRotation[2]],
})
}
}, 50)
return () => {
clearInterval(socketEmitTransformInterval)
}
}, [])
const velocity = useRef<Vector3>(new Vector3())
const direction = useRef<Vector3>(new Vector3())
const worldOrigin = useRef<Vector3>(new Vector3())
const moveForward = (distance: number) => {
if (groupRef.current) {
worldOrigin.current.setFromMatrixColumn(camera.matrix, 0)
worldOrigin.current.crossVectors(camera.up, worldOrigin.current)
groupRef.current.position.addScaledVector(worldOrigin.current, distance)
}
}
const moveRight = (distance: number) => {
if (groupRef.current) {
worldOrigin.current.setFromMatrixColumn(camera.matrix, 0)
groupRef.current.position.addScaledVector(worldOrigin.current, distance)
}
}
useFrame((_, delta) => {
if (!groupRef.current || !collisionsRef.current || !collisionCircle.current) return
if (pointerLocked) {
// Slowdown
velocity.current.x -= velocity.current.x * 10.0 * delta
velocity.current.z -= velocity.current.z * 10.0 * delta
// Fall
bottomRaycaster.current.ray.origin.copy(groupRef.current.position)
let intersections = []
intersections = bottomRaycaster.current.intersectObject(
collisionsRef.current,
false,
)
if (intersections.length < 1) {
velocity.current.y -= 9.8 * 10 * delta
} else {
velocity.current.y = 0
groupRef.current.position.y = intersections[0].point.y + HEIGHT
}
// Direction
direction.current.z = Number(forward) - Number(backward)
direction.current.x = Number(left) - Number(right)
direction.current.normalize()
// Running
const speed = run ? 1.5 : 1
// Move
if (forward || backward)
velocity.current.z -= direction.current.z * SPEED * delta * speed
if (left || right) velocity.current.x -= direction.current.x * SPEED * delta * speed
// Wall collisions
const collisionCircleGeometry = collisionCircle.current.geometry as Geometry
if (collisionCircleGeometry.vertices) {
for (let i = 0; i < CIRCLE_SEGMENTS; i++) {
const localVertex = collisionCircleGeometry.vertices[i + 1].clone()
console.log(i + 1, localVertex)
const globalVertex = localVertex.applyMatrix4(groupRef.current.matrix)
const directionVector = globalVertex.sub(collisionCircle.current.position)
wallRaycasters.current[i].ray.origin.copy(collisionCircle.current.position)
wallRaycasters.current[i].ray.direction.copy(directionVector.normalize())
let wallIntersections = []
wallIntersections = bottomRaycaster.current.intersectObject(
collisionsRef.current,
false,
)
if (wallIntersections.length > 1) console.log(`${i} Hit`)
}
}
// Apply speed
moveForward(-velocity.current.x * speed)
moveRight(-velocity.current.z * speed)
groupRef.current.position.y += velocity.current.y * delta
}
// TODO enable jump if cheating
//if (jump && Math.abs(parseFloat(velocity.current[1].toFixed(2))) < 0.05)
// api.velocity.set(velocity.current[0], 10, velocity.current[2])
})
return (
<>
<group ref={groupRef} position={[0, 5, 0]} rotation={[0, -Math.PI / 2, 0]}>
<FirstPersonCamera rotation={[0, Math.PI / 2, 0]} />
<mesh
ref={collisionCircle}
position={[0, -HEIGHT / 2, 0]}
geometry={collisionCircleGeometry}
material={collisionCircleMaterial}
/>
</group>
<WorldCollisions ref={collisionsRef} />
</>
)
}
export default Player

@ -1,12 +1,25 @@
import * as THREE from 'three'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { SphereProps, useSphere } from '@react-three/cannon'
import { useThree, useFrame } from 'react-three-fiber' import { useThree, useFrame } from 'react-three-fiber'
import { Event } from 'three' import {
Geometry,
Event,
Group,
PerspectiveCamera,
Vector3,
Raycaster,
Mesh,
CircleGeometry,
} from 'three'
import WorldCollisions from './models/WorldCollisions'
import useStore from '../store' import useStore from '../store'
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial'
const SPEED = 1
const HEIGHT = 1.5
const CIRCLE_RADIUS = 1.0
const CIRCLE_SEGMENTS = 8
const SPEED = 5
const keys: Record<string, string> = { const keys: Record<string, string> = {
KeyW: 'forward', KeyW: 'forward',
KeyS: 'backward', KeyS: 'backward',
@ -20,9 +33,18 @@ const keys: Record<string, string> = {
Space: 'jump', Space: 'jump',
} }
const moveFieldByKey = (key: string) => keys[key] const moveFieldByKey = (key: string) => keys[key]
const direction = new THREE.Vector3()
const frontVector = new THREE.Vector3() function FirstPersonCamera(props: JSX.IntrinsicElements['perspectiveCamera']) {
const sideVector = new THREE.Vector3() const ref = useRef<PerspectiveCamera>()
const { setDefaultCamera } = useThree()
// Make the camera known to the system
useEffect(() => {
if (ref.current) setDefaultCamera(ref.current)
}, [ref.current])
// Update it every frame
useFrame(() => ref.current?.updateMatrixWorld())
return <perspectiveCamera ref={ref} {...props} />
}
const usePlayerControls = () => { const usePlayerControls = () => {
const [movement, setMovement] = useState({ const [movement, setMovement] = useState({
@ -51,71 +73,169 @@ const usePlayerControls = () => {
} }
// TODO Improve physics in player // TODO Improve physics in player
const Player = (props: SphereProps) => { const Player = () => {
const socket = useStore((state) => state.socket) const socket = useStore((state) => state.socket)
const pointerLocked = useStore((state) => state.pointerLocked) const pointerLocked = useStore((state) => state.pointerLocked)
const { forward, backward, left, right, run } = usePlayerControls() const { forward, backward, left, right, run } = usePlayerControls()
const { camera } = useThree() const { camera } = useThree()
const [ref, api] = useSphere(() => ({
type: 'Dynamic', const groupRef = useRef<Group>()
fixedRotation: true, const collisionsRef = useRef<Mesh<CircleGeometry, MeshBasicMaterial>>(null)
position: [0, 2, 0], const velocity = useRef<Vector3>(new Vector3())
args: 1, const direction = useRef<Vector3>(new Vector3())
mass: 5, const worldOrigin = useRef<Vector3>(new Vector3())
linearDamping: 0, const currentPositionClone = useRef<Vector3>(new Vector3())
angularDamping: 0,
material: { friction: 10, restitution: 0 }, const bottomRaycaster = useRef(
...props, new Raycaster(new Vector3(), new Vector3(0, -1, 0), 0, HEIGHT + 0.5),
})) )
const collisionCircle = useRef<Mesh>()
const collisionCircleGeometry = new CircleGeometry(CIRCLE_RADIUS, CIRCLE_SEGMENTS)
collisionCircleGeometry.rotateX(Math.PI / 2)
const collisionCircleMaterial = new MeshBasicMaterial({
color: 0xff0000,
wireframe: true,
})
const wallRaycasters = useRef<Raycaster[]>(
Array(CIRCLE_SEGMENTS)
.fill(undefined)
.map((_, i) => {
const vert = collisionCircleGeometry.vertices[i + 1].clone()
const direction = vert.sub(collisionCircleGeometry.vertices[0]).normalize()
return new Raycaster(new Vector3(), direction, 0, CIRCLE_RADIUS)
}),
)
useEffect(() => { useEffect(() => {
const socketEmitTransformInterval = setInterval(() => { const socketEmitTransformInterval = setInterval(() => {
if (socket && ref.current && camera) { if (socket && groupRef.current && camera) {
const cameraRotation = camera.rotation.toArray() const cameraRotation = camera.rotation.toArray()
socket.emit('transform', { socket.emit('transform', {
position: [ position: [
ref.current.position.x, groupRef.current?.position.x,
ref.current.position.y, groupRef.current?.position.y,
ref.current.position.z, groupRef.current?.position.z,
], ],
rotation: [cameraRotation[0], cameraRotation[1], cameraRotation[2]], rotation: [cameraRotation[0], cameraRotation[1], cameraRotation[2]],
}) })
} }
}, 16) }, 50)
return () => { return () => {
clearInterval(socketEmitTransformInterval) clearInterval(socketEmitTransformInterval)
} }
}, []) }, [])
const velocity = useRef([0, 0, 0]) const moveForward = (distance: number) => {
useEffect(() => void api.velocity.subscribe((v) => (velocity.current = v)), []) if (groupRef.current) {
useFrame(() => { worldOrigin.current.setFromMatrixColumn(camera.matrix, 0)
if (!ref.current) return worldOrigin.current.crossVectors(camera.up, worldOrigin.current)
camera.position.set( groupRef.current.position.addScaledVector(worldOrigin.current, distance)
ref.current.position.x, }
ref.current.position.y + 0.4, }
ref.current.position.z,
const moveRight = (distance: number) => {
if (groupRef.current) {
worldOrigin.current.setFromMatrixColumn(camera.matrix, 0)
groupRef.current.position.addScaledVector(worldOrigin.current, distance)
}
}
useFrame((_, delta) => {
if (!groupRef.current || !collisionsRef.current || !collisionCircle.current) return
if (pointerLocked) {
// Slowdown
velocity.current.x -= velocity.current.x * 10.0 * delta
velocity.current.z -= velocity.current.z * 10.0 * delta
// Fall
bottomRaycaster.current.ray.origin.copy(groupRef.current.position)
let intersections = []
intersections = bottomRaycaster.current.intersectObject(
collisionsRef.current,
false,
) )
frontVector.set(0, 0, Number(backward) - Number(forward))
sideVector.set(Number(left) - Number(right), 0, 0)
direction
.subVectors(frontVector, sideVector)
.normalize()
.multiplyScalar(run ? SPEED * 2 : SPEED)
.applyEuler(camera.rotation)
if (pointerLocked && (forward || backward || left || right)) { if (intersections.length < 1) {
api.velocity.set(direction.x, -SPEED / 2, direction.z) velocity.current.y -= 9.8 * 10 * delta
} else { } else {
api.velocity.set(0, 0, 0) velocity.current.y = 0
groupRef.current.position.y = intersections[0].point.y + HEIGHT
}
// Direction
direction.current.z = Number(forward) - Number(backward)
direction.current.x = Number(left) - Number(right)
direction.current.normalize()
// Running
const speed = run ? 1.5 : 1
// Move
if (forward || backward)
velocity.current.z -= direction.current.z * SPEED * delta * speed
if (left || right) velocity.current.x -= direction.current.x * SPEED * delta * speed
// Wall collisions
const collisionCircleGeometry = collisionCircle.current.geometry as Geometry
if (collisionCircleGeometry.vertices) {
for (let i = 0; i < CIRCLE_SEGMENTS; i++) {
wallRaycasters.current[i].ray.origin.copy(groupRef.current.position)
let wallIntersections = []
wallIntersections = wallRaycasters.current[i].intersectObject(
collisionsRef.current,
false,
)
if (
wallIntersections.length > 0 &&
wallIntersections[0].distance < CIRCLE_RADIUS
) {
const distance = CIRCLE_RADIUS - wallIntersections[0].distance
currentPositionClone.current.copy(groupRef.current.position)
const direction = wallIntersections[0].point
.sub(currentPositionClone.current)
.normalize()
.multiplyScalar(distance)
groupRef.current.position.sub(direction)
}
}
}
// Apply speed
moveForward(-velocity.current.x * speed)
moveRight(-velocity.current.z * speed)
groupRef.current.position.y += velocity.current.y * delta
} }
// TODO enable jump if cheating // TODO enable jump if cheating
//if (jump && Math.abs(parseFloat(velocity.current[1].toFixed(2))) < 0.05) //if (jump && Math.abs(parseFloat(velocity.current[1].toFixed(2))) < 0.05)
// api.velocity.set(velocity.current[0], 10, velocity.current[2]) // api.velocity.set(velocity.current[0], 10, velocity.current[2])
}) })
return <mesh ref={ref} /> return (
<>
<group ref={groupRef} position={[0, 5, 0]} rotation={[0, -Math.PI / 2, 0]}>
<FirstPersonCamera rotation={[0, Math.PI / 2, 0]} />
<mesh
ref={collisionCircle}
position={[0, -HEIGHT / 2, 0]}
geometry={collisionCircleGeometry}
material={collisionCircleMaterial}
visible={false}
/>
</group>
<WorldCollisions ref={collisionsRef} visible={false} />
</>
)
} }
export default Player export default Player

@ -1,16 +1,16 @@
import React, { Suspense } from 'react' import React, { Suspense } from 'react'
import { Physics } from '@react-three/cannon' import { useThree } from 'react-three-fiber'
import { Stats, Text } from '@react-three/drei' import { Stats, Text } from '@react-three/drei'
import { Euler, Quaternion, Vector3 } from 'three' import { Euler, Quaternion } from 'three'
import World from './models/World' import World from './models/World'
import WorldCollisions from './models/WorldCollisions'
import Player from './Player' import Player from './Player'
import Lighting from './Lighting' import Lighting from './Lighting'
import Effects from './Effects' import Effects from './Effects'
import Controls from './Controls' import Controls from './Controls'
import Users from './Users' import Users from './Users'
import Computer from './models/Computer' import Computer from './models/Computer'
import { useEffect } from 'react'
const computerPositions = [ const computerPositions = [
{ {
@ -28,6 +28,15 @@ const computerPositions = [
] ]
const Scene: React.FC = () => { const Scene: React.FC = () => {
// addAfterEffect(() => {
// console.log(gl.info.render);
// });
const { scene } = useThree()
useEffect(() => {
//if (scene) scene.overrideMaterial = new MeshBasicMaterial({ color: 'green' })
}, [scene])
return ( return (
<> <>
<Stats /> <Stats />
@ -35,22 +44,7 @@ const Scene: React.FC = () => {
<Suspense fallback={null}> <Suspense fallback={null}>
<World /> <World />
<Physics
gravity={[0, 0, 0]}
tolerance={0}
iterations={2}
size={2}
broadphase="SAP"
axisIndex={1}
defaultContactMaterial={{
friction: 0,
restitution: 0,
}}
>
<WorldCollisions />
<Player /> <Player />
</Physics>
<Effects />
<Text <Text
position={[-5.640829086303711, 13, -74.78675842285156]} position={[-5.640829086303711, 13, -74.78675842285156]}
@ -144,6 +138,7 @@ const Scene: React.FC = () => {
/> />
))} ))}
<Users /> <Users />
<Effects />
<Controls /> <Controls />
</> </>

@ -7,7 +7,6 @@ import { useGLTF } from '@react-three/drei/useGLTF'
export default function Model(props) { export default function Model(props) {
const group = useRef() const group = useRef()
const { nodes, materials } = useGLTF('/model/computer.glb') const { nodes, materials } = useGLTF('/model/computer.glb')
console.log(nodes)
return ( return (
<group ref={group} dispose={null}> <group ref={group} dispose={null}>
<group {...props} scale={[0.3, 0.3, 0.3]}> <group {...props} scale={[0.3, 0.3, 0.3]}>

@ -14,7 +14,7 @@ export default function Model(props) {
return ( return (
<group ref={group} {...props} dispose={null}> <group ref={group} {...props} dispose={null}>
<primitive object={scene} dispose={null} /> <primitive object={scene} dispose={null} matrixAutoUpdate={false} />
</group> </group>
) )
} }

@ -1,12 +1,11 @@
/* /*
auto-generated by: https://github.com/pmndrs/gltfjsx auto-generated by: https://github.com/pmndrs/gltfjsx
*/ */
import React, { useRef } from 'react' import React, { forwardRef, ForwardedRef } from 'react'
import { useGLTF } from '@react-three/drei/useGLTF' import { useGLTF } from '@react-three/drei/useGLTF'
import * as THREE from 'three'
import { useTrimesh } from '@react-three/cannon'
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader' import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { Mesh, MeshBasicMaterial } from 'three'
type GLTFResult = GLTF & { type GLTFResult = GLTF & {
nodes: { nodes: {
@ -17,40 +16,29 @@ type GLTFResult = GLTF & {
} }
} }
export default function Model(props: JSX.IntrinsicElements['group']) { const Model = forwardRef(
const group = useRef<THREE.Group>() (props: JSX.IntrinsicElements['group'], ref: ForwardedRef<Mesh>) => {
const { nodes } = useGLTF('/model/plaza_collision.glb') as GLTFResult const { nodes } = useGLTF('/model/plaza_collision.glb') as GLTFResult
const material = new THREE.MeshBasicMaterial({ const material = new MeshBasicMaterial({
wireframe: true, wireframe: true,
color: 0xffff00, color: 0xffff00,
}) })
const geometry = nodes.collisions.geometry as THREE.BufferGeometry
if (geometry.index === null) return null
const vertices = geometry.attributes.position.array
const indices = geometry.index.array
// eslint-ignore-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const [ref] = useTrimesh(() => ({
type: 'Static',
position: [-5.31, 1.23, -32.92],
args: [vertices, indices],
}))
return ( return (
<group ref={group} {...props} dispose={null}> <group {...props} dispose={null}>
<mesh <mesh
ref={ref} ref={ref}
material={material} material={material}
geometry={nodes.collisions.geometry} geometry={nodes.collisions.geometry}
visible={false} visible={true}
position={[-5.31, 1.23, -32.92]}
/> />
</group> </group>
) )
} },
)
useGLTF.preload('/model/collision_mesh.glb') useGLTF.preload('/model/collision_mesh.glb')
export default Model

Loading…
Cancel
Save