feat: add player camera
This commit is contained in:
+73
-2
@@ -1,9 +1,80 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import type { AmbientLight, DirectionalLight } from "three";
|
||||
import { Debug } from "@/debug/Debug";
|
||||
|
||||
type LightingState = {
|
||||
ambientIntensity: number;
|
||||
sunIntensity: number;
|
||||
sunX: number;
|
||||
sunY: number;
|
||||
sunZ: number;
|
||||
};
|
||||
|
||||
const LIGHTING_STATE: LightingState = {
|
||||
ambientIntensity: 1.8,
|
||||
sunIntensity: 2.8,
|
||||
sunX: 60,
|
||||
sunY: 80,
|
||||
sunZ: 30,
|
||||
};
|
||||
|
||||
export function Lighting(): React.JSX.Element {
|
||||
const ambient = useRef<AmbientLight>(null);
|
||||
const sun = useRef<DirectionalLight>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const debug = Debug.getInstance();
|
||||
|
||||
if (!debug.active) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const folder = debug.createFolder("Lighting");
|
||||
|
||||
if (!folder) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
folder.add(LIGHTING_STATE, "ambientIntensity", 0, 5, 0.1).name("Ambient");
|
||||
folder.add(LIGHTING_STATE, "sunIntensity", 0, 8, 0.1).name("Sun Intensity");
|
||||
folder.add(LIGHTING_STATE, "sunX", -100, 100, 1).name("Sun X");
|
||||
folder.add(LIGHTING_STATE, "sunY", 0, 150, 1).name("Sun Y");
|
||||
folder.add(LIGHTING_STATE, "sunZ", -100, 100, 1).name("Sun Z");
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
useFrame(() => {
|
||||
if (ambient.current) {
|
||||
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
||||
}
|
||||
|
||||
if (sun.current) {
|
||||
sun.current.position.set(
|
||||
LIGHTING_STATE.sunX,
|
||||
LIGHTING_STATE.sunY,
|
||||
LIGHTING_STATE.sunZ,
|
||||
);
|
||||
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight
|
||||
ref={ambient}
|
||||
intensity={LIGHTING_STATE.ambientIntensity}
|
||||
color="#dbeafe"
|
||||
/>
|
||||
<directionalLight
|
||||
position={[60, 80, 30]}
|
||||
intensity={2.8}
|
||||
ref={sun}
|
||||
position={[
|
||||
LIGHTING_STATE.sunX,
|
||||
LIGHTING_STATE.sunY,
|
||||
LIGHTING_STATE.sunZ,
|
||||
]}
|
||||
intensity={LIGHTING_STATE.sunIntensity}
|
||||
color="#fff7ed"
|
||||
castShadow
|
||||
/>
|
||||
|
||||
+10
-71
@@ -1,28 +1,14 @@
|
||||
// # route path src/world/Map.tsx
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useMemo } from "react";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
import { Debug } from "@/debug/Debug";
|
||||
|
||||
const MODEL_PATH = "/models/map/blocking/model.glb";
|
||||
|
||||
type MapDebugState = {
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
positionZ: number;
|
||||
rotationY: number;
|
||||
type CenteredModel = {
|
||||
object: THREE.Object3D;
|
||||
scale: number;
|
||||
};
|
||||
|
||||
const DEFAULT_DEBUG_STATE: MapDebugState = {
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
positionZ: 0,
|
||||
rotationY: 0,
|
||||
scale: 1,
|
||||
};
|
||||
|
||||
function centerModel(model: THREE.Object3D): number {
|
||||
model.updateMatrixWorld(true);
|
||||
|
||||
@@ -36,64 +22,17 @@ function centerModel(model: THREE.Object3D): number {
|
||||
}
|
||||
|
||||
export function Map(): React.JSX.Element {
|
||||
const root = useRef<THREE.Group>(null);
|
||||
const debugState = useRef<MapDebugState>({ ...DEFAULT_DEBUG_STATE });
|
||||
const { scene } = useGLTF(MODEL_PATH);
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const centeredModel = useMemo<CenteredModel>(() => {
|
||||
const object = scene.clone(true);
|
||||
const scale = centerModel(object);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
debugState.current.scale = centerModel(model);
|
||||
}, [model]);
|
||||
|
||||
useEffect(() => {
|
||||
const debug = Debug.getInstance();
|
||||
|
||||
if (!debug.active) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const folder = debug.createFolder("Map");
|
||||
|
||||
if (!folder) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
folder
|
||||
.add(debugState.current, "positionX", -100, 100, 0.1)
|
||||
.name("Position X");
|
||||
folder
|
||||
.add(debugState.current, "positionY", -20, 50, 0.1)
|
||||
.name("Position Y");
|
||||
folder
|
||||
.add(debugState.current, "positionZ", -100, 100, 0.1)
|
||||
.name("Position Z");
|
||||
folder
|
||||
.add(debugState.current, "rotationY", -Math.PI, Math.PI, 0.01)
|
||||
.name("Rotation Y");
|
||||
folder.add(debugState.current, "scale", 0.1, 10, 0.01).name("Scale");
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
useFrame(() => {
|
||||
const currentRoot = root.current;
|
||||
|
||||
if (!currentRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentRoot.position.set(
|
||||
debugState.current.positionX,
|
||||
debugState.current.positionY,
|
||||
debugState.current.positionZ,
|
||||
);
|
||||
currentRoot.rotation.y = debugState.current.rotationY;
|
||||
currentRoot.scale.setScalar(debugState.current.scale);
|
||||
});
|
||||
return { object, scale };
|
||||
}, [scene]);
|
||||
|
||||
return (
|
||||
<group ref={root}>
|
||||
<primitive object={model} />
|
||||
<group scale={centeredModel.scale}>
|
||||
<primitive object={centeredModel.object} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,17 +1,21 @@
|
||||
import { Suspense } from "react";
|
||||
import { DebugCameraControls } from "@/debug/scene/DebugCameraControls";
|
||||
import { DebugHelpers } from "@/debug/scene/DebugHelpers";
|
||||
import { useCameraMode } from "@/debug/useCameraMode";
|
||||
import { Environment } from "@/world/Environment";
|
||||
import { Lighting } from "@/world/Lighting";
|
||||
import { Map } from "@/world/Map";
|
||||
import { FPSController } from "@/world/player/FPSController";
|
||||
|
||||
export function World(): React.JSX.Element {
|
||||
const cameraMode = useCameraMode();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Environment />
|
||||
<Lighting />
|
||||
<DebugHelpers />
|
||||
<DebugCameraControls />
|
||||
{cameraMode === "debug" ? <DebugCameraControls /> : <FPSController />}
|
||||
<Suspense fallback={null}>
|
||||
<Map />
|
||||
</Suspense>
|
||||
|
||||
@@ -1 +1,124 @@
|
||||
// src/world/player/FPSController.tsx
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { PointerLockControls } from "@react-three/drei";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
|
||||
const PLAYER_EYE_HEIGHT = 1.75;
|
||||
const PLAYER_SPAWN_POSITION = new THREE.Vector3(0, PLAYER_EYE_HEIGHT, 6);
|
||||
const PLAYER_LOOK_AT = new THREE.Vector3(0, PLAYER_EYE_HEIGHT, 0);
|
||||
const MOVE_SPEED = 5;
|
||||
|
||||
type PlayerKeys = {
|
||||
forward: boolean;
|
||||
backward: boolean;
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_KEYS: PlayerKeys = {
|
||||
forward: false,
|
||||
backward: false,
|
||||
left: false,
|
||||
right: false,
|
||||
};
|
||||
|
||||
export function FPSController(): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const keys = useRef<PlayerKeys>({ ...DEFAULT_KEYS });
|
||||
const interact = useRef<() => void>(() => {});
|
||||
const forward = useMemo(() => new THREE.Vector3(), []);
|
||||
const right = useMemo(() => new THREE.Vector3(), []);
|
||||
const movement = useMemo(() => new THREE.Vector3(), []);
|
||||
const up = useMemo(() => new THREE.Vector3(0, 1, 0), []);
|
||||
|
||||
useEffect(() => {
|
||||
camera.position.copy(PLAYER_SPAWN_POSITION);
|
||||
camera.lookAt(PLAYER_LOOK_AT);
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
return () => {
|
||||
document.exitPointerLock?.();
|
||||
};
|
||||
}, [camera]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyChange =
|
||||
(pressed: boolean) =>
|
||||
(event: KeyboardEvent): void => {
|
||||
switch (event.code) {
|
||||
case "KeyZ":
|
||||
keys.current.forward = pressed;
|
||||
break;
|
||||
case "KeyS":
|
||||
keys.current.backward = pressed;
|
||||
break;
|
||||
case "KeyQ":
|
||||
keys.current.left = pressed;
|
||||
break;
|
||||
case "KeyD":
|
||||
keys.current.right = pressed;
|
||||
break;
|
||||
case "KeyE":
|
||||
if (pressed) {
|
||||
interact.current();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleKeyDown = handleKeyChange(true);
|
||||
const handleKeyUp = handleKeyChange(false);
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
keys.current = { ...DEFAULT_KEYS };
|
||||
};
|
||||
}, []);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
movement.set(0, 0, 0);
|
||||
|
||||
camera.getWorldDirection(forward);
|
||||
forward.y = 0;
|
||||
|
||||
if (forward.lengthSq() > 0) {
|
||||
forward.normalize();
|
||||
right.crossVectors(forward, up).normalize();
|
||||
}
|
||||
|
||||
if (keys.current.forward) {
|
||||
movement.add(forward);
|
||||
}
|
||||
|
||||
if (keys.current.backward) {
|
||||
movement.sub(forward);
|
||||
}
|
||||
|
||||
if (keys.current.left) {
|
||||
movement.sub(right);
|
||||
}
|
||||
|
||||
if (keys.current.right) {
|
||||
movement.add(right);
|
||||
}
|
||||
|
||||
if (movement.lengthSq() > 0) {
|
||||
movement.normalize().multiplyScalar(MOVE_SPEED * delta);
|
||||
camera.position.add(movement);
|
||||
}
|
||||
|
||||
if (camera.position.y < PLAYER_EYE_HEIGHT) {
|
||||
camera.position.y = PLAYER_EYE_HEIGHT;
|
||||
}
|
||||
});
|
||||
|
||||
return <PointerLockControls />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user