diff --git "a/packages/client/src/3d/\n" "b/packages/client/src/3d/\n" new file mode 100644 index 0000000..27b0f54 --- /dev/null +++ "b/packages/client/src/3d/\n" @@ -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 = { + 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() + 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 +} + +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() + const collisionsRef = useRef>(null) + + const bottomRaycaster = useRef( + new Raycaster(new Vector3(), new Vector3(0, -1, 0), 0, HEIGHT + 0.5), + ) + + const collisionCircle = useRef() + 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( + 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(new Vector3()) + const direction = useRef(new Vector3()) + const worldOrigin = useRef(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 ( + <> + + + + + + + ) +} + +export default Player diff --git a/packages/client/src/3d/Player.tsx b/packages/client/src/3d/Player.tsx index 3eac807..a18afba 100644 --- a/packages/client/src/3d/Player.tsx +++ b/packages/client/src/3d/Player.tsx @@ -1,12 +1,25 @@ -import * as THREE from 'three' import React, { useEffect, useRef, useState } from 'react' -import { SphereProps, useSphere } from '@react-three/cannon' 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 { 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 = { KeyW: 'forward', KeyS: 'backward', @@ -20,9 +33,18 @@ const keys: Record = { Space: 'jump', } const moveFieldByKey = (key: string) => keys[key] -const direction = new THREE.Vector3() -const frontVector = new THREE.Vector3() -const sideVector = new THREE.Vector3() + +function FirstPersonCamera(props: JSX.IntrinsicElements['perspectiveCamera']) { + const ref = useRef() + 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 +} const usePlayerControls = () => { const [movement, setMovement] = useState({ @@ -51,71 +73,169 @@ const usePlayerControls = () => { } // TODO Improve physics in player -const Player = (props: SphereProps) => { +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 [ref, api] = useSphere(() => ({ - type: 'Dynamic', - fixedRotation: true, - position: [0, 2, 0], - args: 1, - mass: 5, - linearDamping: 0, - angularDamping: 0, - material: { friction: 10, restitution: 0 }, - ...props, - })) + + const groupRef = useRef() + const collisionsRef = useRef>(null) + const velocity = useRef(new Vector3()) + const direction = useRef(new Vector3()) + const worldOrigin = useRef(new Vector3()) + const currentPositionClone = useRef(new Vector3()) + + const bottomRaycaster = useRef( + new Raycaster(new Vector3(), new Vector3(0, -1, 0), 0, HEIGHT + 0.5), + ) + + const collisionCircle = useRef() + const collisionCircleGeometry = new CircleGeometry(CIRCLE_RADIUS, CIRCLE_SEGMENTS) + collisionCircleGeometry.rotateX(Math.PI / 2) + const collisionCircleMaterial = new MeshBasicMaterial({ + color: 0xff0000, + wireframe: true, + }) + + const wallRaycasters = useRef( + 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(() => { const socketEmitTransformInterval = setInterval(() => { - if (socket && ref.current && camera) { + if (socket && groupRef.current && camera) { const cameraRotation = camera.rotation.toArray() socket.emit('transform', { position: [ - ref.current.position.x, - ref.current.position.y, - ref.current.position.z, + groupRef.current?.position.x, + groupRef.current?.position.y, + groupRef.current?.position.z, ], rotation: [cameraRotation[0], cameraRotation[1], cameraRotation[2]], }) } - }, 16) + }, 50) return () => { clearInterval(socketEmitTransformInterval) } }, []) - const velocity = useRef([0, 0, 0]) - useEffect(() => void api.velocity.subscribe((v) => (velocity.current = v)), []) - useFrame(() => { - if (!ref.current) return - camera.position.set( - ref.current.position.x, - ref.current.position.y + 0.4, - ref.current.position.z, - ) - 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)) { - api.velocity.set(direction.x, -SPEED / 2, direction.z) - } else { - api.velocity.set(0, 0, 0) + 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++) { + 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 //if (jump && Math.abs(parseFloat(velocity.current[1].toFixed(2))) < 0.05) // api.velocity.set(velocity.current[0], 10, velocity.current[2]) }) - return + return ( + <> + + + + + + + ) } export default Player diff --git a/packages/client/src/3d/index.tsx b/packages/client/src/3d/index.tsx index bc90858..3180f91 100644 --- a/packages/client/src/3d/index.tsx +++ b/packages/client/src/3d/index.tsx @@ -1,16 +1,16 @@ 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 { Euler, Quaternion, Vector3 } from 'three' +import { Euler, Quaternion } from 'three' import World from './models/World' -import WorldCollisions from './models/WorldCollisions' import Player from './Player' import Lighting from './Lighting' import Effects from './Effects' import Controls from './Controls' import Users from './Users' import Computer from './models/Computer' +import { useEffect } from 'react' const computerPositions = [ { @@ -28,6 +28,15 @@ const computerPositions = [ ] const Scene: React.FC = () => { + // addAfterEffect(() => { + // console.log(gl.info.render); + // }); + + const { scene } = useThree() + useEffect(() => { + //if (scene) scene.overrideMaterial = new MeshBasicMaterial({ color: 'green' }) + }, [scene]) + return ( <> @@ -35,22 +44,7 @@ const Scene: React.FC = () => { - - - - - + { /> ))} + diff --git a/packages/client/src/3d/models/Computer.js b/packages/client/src/3d/models/Computer.js index 08393c3..b638803 100644 --- a/packages/client/src/3d/models/Computer.js +++ b/packages/client/src/3d/models/Computer.js @@ -7,7 +7,6 @@ import { useGLTF } from '@react-three/drei/useGLTF' export default function Model(props) { const group = useRef() const { nodes, materials } = useGLTF('/model/computer.glb') - console.log(nodes) return ( diff --git a/packages/client/src/3d/models/World.js b/packages/client/src/3d/models/World.js index 5b83297..b08cf6f 100644 --- a/packages/client/src/3d/models/World.js +++ b/packages/client/src/3d/models/World.js @@ -14,7 +14,7 @@ export default function Model(props) { return ( - + ) } diff --git a/packages/client/src/3d/models/WorldCollisions.tsx b/packages/client/src/3d/models/WorldCollisions.tsx index 8a0ba8b..96cc7b5 100644 --- a/packages/client/src/3d/models/WorldCollisions.tsx +++ b/packages/client/src/3d/models/WorldCollisions.tsx @@ -1,12 +1,11 @@ /* 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 * as THREE from 'three' -import { useTrimesh } from '@react-three/cannon' import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader' +import { Mesh, MeshBasicMaterial } from 'three' type GLTFResult = GLTF & { nodes: { @@ -17,40 +16,29 @@ type GLTFResult = GLTF & { } } -export default function Model(props: JSX.IntrinsicElements['group']) { - const group = useRef() - const { nodes } = useGLTF('/model/plaza_collision.glb') as GLTFResult - - const material = new THREE.MeshBasicMaterial({ - wireframe: true, - 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 ( - - - - ) -} +const Model = forwardRef( + (props: JSX.IntrinsicElements['group'], ref: ForwardedRef) => { + const { nodes } = useGLTF('/model/plaza_collision.glb') as GLTFResult + + const material = new MeshBasicMaterial({ + wireframe: true, + color: 0xffff00, + }) + + return ( + + + + ) + }, +) useGLTF.preload('/model/collision_mesh.glb') + +export default Model