feat: better optimized chunk generation

This commit is contained in:
Florian Sylvain
2024-03-18 00:52:26 +01:00
parent 849bc63a6c
commit 97ea9edfe6
4 changed files with 161 additions and 111 deletions
+2 -16
View File
@@ -2,16 +2,13 @@ import "./App.css";
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { KeyboardControls } from "@react-three/drei"; import { KeyboardControls } from "@react-three/drei";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef } from "react";
import { PCFSoftShadowMap, Vector3 } from "three"; import { PCFSoftShadowMap, Vector3 } from "three";
import { Player } from "./components/Player"; import { Player } from "./components/Player";
import { Bloom, EffectComposer, Vignette } from "@react-three/postprocessing"; import { Bloom, EffectComposer, Vignette } from "@react-three/postprocessing";
import { Cubes } from "./components/Cubes";
import { GameContext } from "./hooks/GameContext"; import { GameContext } from "./hooks/GameContext";
import { ChunkGenerator } from "./components/ChunkGenerator"; import { ChunkGenerator } from "./components/ChunkGenerator";
const CUBES_QT = 10;
function App() { function App() {
const container = useRef<HTMLCanvasElement>(null); const container = useRef<HTMLCanvasElement>(null);
@@ -19,15 +16,6 @@ function App() {
container.current?.focus(); container.current?.focus();
}); });
const [cubes] = useState<Vector3[]>(
Array.from({ length: CUBES_QT }, (_, i) =>
Array.from(
{ length: CUBES_QT },
(_, j) => new Vector3(i + 20 - CUBES_QT / 2, 0, j - CUBES_QT / 2)
)
).flat()
);
return ( return (
<main <main
className="container" className="container"
@@ -37,10 +25,8 @@ function App() {
className="canvas" className="canvas"
ref={container} ref={container}
shadows={{ type: PCFSoftShadowMap }} shadows={{ type: PCFSoftShadowMap }}
camera={{ fov: 50, frustumCulled: true, near: 0.1, far: 200 }} camera={{ fov: 50, frustumCulled: true, near: 0.1, far: 2000 }}
> >
<Cubes cubesPosition={cubes}></Cubes>
<GameContext.Provider value={{ playerPosition: new Vector3() }}> <GameContext.Provider value={{ playerPosition: new Vector3() }}>
<KeyboardControls <KeyboardControls
map={[ map={[
+74 -64
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react"; import { memo, useEffect } from "react";
import { import {
ClampToEdgeWrapping, ClampToEdgeWrapping,
FrontSide, FrontSide,
@@ -15,8 +15,8 @@ const grassTexture = new TextureLoader().load("./texture_grass.jpg");
grassTexture.wrapS = grassTexture.wrapT = RepeatWrapping; grassTexture.wrapS = grassTexture.wrapT = RepeatWrapping;
grassTexture.repeat.set(100, 100); grassTexture.repeat.set(100, 100);
const SCALE = 5; const SCALE = 1;
const AMPLITUDE = 10; const AMPLITUDE = 100;
const GEOMETRY = 50; const GEOMETRY = 50;
const SIZE = 100; // CHANGE WITH CAUTION const SIZE = 100; // CHANGE WITH CAUTION
@@ -27,66 +27,76 @@ const ADJUSTED_GEOMETRY = GEOMETRY + OVERLAP * 2;
const ADJUSTED_SIZE = SIZE + OVERLAP * 2; const ADJUSTED_SIZE = SIZE + OVERLAP * 2;
const ADJUSTED_SCALE = SIZE / SCALE; const ADJUSTED_SCALE = SIZE / SCALE;
export function Chunk(props: { position: Vector2; seed: number }): JSX.Element { export const Chunk = memo(
const noise = (x: number, y: number): number => { (props: { position: Vector2; seed: number }): JSX.Element => {
const noise = new ImprovedNoise(); const noise = (x: number, y: number): number => {
return noise.noise(x, y, props.seed); const noise = new ImprovedNoise();
}; return noise.noise(x, y, props.seed);
canvas.width = canvas.height = ADJUSTED_SIZE;
const context = canvas.getContext("2d", { willReadFrequently: true });
if (!context) throw new Error("Canvas context not found");
const offsetX = -OVERLAP + props.position.x;
const offsetY = -OVERLAP + props.position.y;
const imageData = context.getImageData(0, 0, ADJUSTED_SIZE, ADJUSTED_SIZE);
const data = imageData.data;
for (let x = 0; x < ADJUSTED_SIZE; x++) {
for (let y = 0; y < ADJUSTED_SIZE; y++) {
const n =
noise((x + offsetX) / ADJUSTED_SCALE, (y + offsetY) / ADJUSTED_SCALE) *
127 +
127;
const id = (x + y * ADJUSTED_SIZE) * 4;
data[id] = n;
data[id + 1] = n;
data[id + 2] = n;
data[id + 3] = 255;
}
}
context.putImageData(imageData, 0, 0);
const displacementMap = new TextureLoader().load(canvas.toDataURL());
displacementMap.wrapS = displacementMap.wrapT = ClampToEdgeWrapping;
useEffect(() => {
return () => {
displacementMap.dispose();
}; };
}, [displacementMap]);
return ( canvas.width = canvas.height = ADJUSTED_SIZE;
<group position={new Vector3(props.position.x, 0, props.position.y)}> const context = canvas.getContext("2d", { willReadFrequently: true });
<mesh if (!context) throw new Error("Canvas context not found");
position={[0, 0, 0]}
rotation={[-Math.PI / 2, 0, 0]} const offsetX = -OVERLAP + props.position.x;
castShadow const offsetY = -OVERLAP + props.position.y;
receiveShadow
> const imageData = context.getImageData(0, 0, ADJUSTED_SIZE, ADJUSTED_SIZE);
<planeGeometry const data = imageData.data;
attach="geometry"
args={[CHUNK_SIZE, CHUNK_SIZE, ADJUSTED_GEOMETRY, ADJUSTED_GEOMETRY]} for (let x = 0; x < ADJUSTED_SIZE; x++) {
></planeGeometry> for (let y = 0; y < ADJUSTED_SIZE; y++) {
<meshStandardMaterial const n =
toneMapped={false} noise(
attach="material" (x + offsetX) / ADJUSTED_SCALE,
map={grassTexture} (y + offsetY) / ADJUSTED_SCALE
displacementMap={displacementMap} ) *
displacementScale={AMPLITUDE} 127 +
shadowSide={FrontSide} 127;
/> const id = (x + y * ADJUSTED_SIZE) * 4;
</mesh> data[id] = n;
</group> data[id + 1] = n;
); data[id + 2] = n;
} data[id + 3] = 255;
}
}
context.putImageData(imageData, 0, 0);
const displacementMap = new TextureLoader().load(canvas.toDataURL());
displacementMap.wrapS = displacementMap.wrapT = ClampToEdgeWrapping;
useEffect(() => {
return () => {
displacementMap.dispose();
};
}, [displacementMap]);
return (
<group position={new Vector3(props.position.x, 0, props.position.y)}>
<mesh
position={[0, 0, 0]}
rotation={[-Math.PI / 2, 0, 0]}
castShadow
receiveShadow
>
<planeGeometry
attach="geometry"
args={[
CHUNK_SIZE,
CHUNK_SIZE,
ADJUSTED_GEOMETRY,
ADJUSTED_GEOMETRY,
]}
></planeGeometry>
<meshStandardMaterial
toneMapped={false}
attach="material"
map={grassTexture}
displacementMap={displacementMap}
displacementScale={AMPLITUDE}
shadowSide={FrontSide}
/>
</mesh>
</group>
);
}
);
+80 -22
View File
@@ -1,53 +1,111 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useRef, useState } from "react";
import { GameContext } from "../hooks/GameContext"; import { GameContext } from "../hooks/GameContext";
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import { Vector2 } from "three"; import { Vector2 } from "three";
import { Chunk } from "./Chunk"; import { Chunk } from "./Chunk";
const CHUNK_RADIUS = 20;
const CHUNK_PER_FRAME = 3;
export function ChunkGenerator(): JSX.Element { export function ChunkGenerator(): JSX.Element {
const { playerPosition } = useContext(GameContext); const { playerPosition } = useContext(GameContext);
const [lastPlayerPosition] = useState(new Vector2(0, 0)); const [lastPlayerPosition] = useState(new Vector2());
const [chunks, setChunks] = useState([] as JSX.Element[]); const [chunks, setChunks] = useState([] as JSX.Element[]);
const [chunkQueue, setChunkQueue] = useState([] as JSX.Element[]);
const generatedChunkKeys = useRef(new Set<string>());
const chunkPosVec2 = new Vector2(); const chunkPosVec2 = new Vector2();
const isChunkAlreadyGenerated = (x: number, y: number) => const getChunkCoordinates = (x: number, y: number): number[] => {
chunks.some((chunk) => chunk.key === `${x}-${y}`); return [Math.floor(x / 100 + 0.5), Math.floor(y / 100 + 0.5)];
};
function generateNewChunks(playerX: number, playerY: number) { const isChunkAlreadyGenerated = (x: number, y: number, key: string) =>
const newChunks = []; generatedChunkKeys.current.has(`${x}-${y}`) ||
for (let x = -2; x <= 2; x++) { chunkQueue.some((chunk) => chunk.key === key);
for (let y = -2; y <= 2; y++) {
if (isChunkAlreadyGenerated(playerX + x, playerY + y)) continue; const isChunkInPlayerRadius = (chunkPos: number[], playerPos: number[]) =>
chunkPos[0] < playerPos[0] - CHUNK_RADIUS ||
chunkPos[0] > playerPos[0] + CHUNK_RADIUS ||
chunkPos[1] < playerPos[1] - CHUNK_RADIUS ||
chunkPos[1] > playerPos[1] + CHUNK_RADIUS;
function generateNewChunks(playerX: number, playerY: number): void {
const newChunks = [] as JSX.Element[];
for (let x = -CHUNK_RADIUS; x <= CHUNK_RADIUS; x++) {
for (let y = -CHUNK_RADIUS; y <= CHUNK_RADIUS; y++) {
const key = `${playerX + x}-${playerY + y}`;
if (isChunkAlreadyGenerated(playerX + x, playerY + y, key)) continue;
newChunks.push( newChunks.push(
<Chunk <Chunk
key={`${playerX + x}-${playerY + y}`} key={key}
position={chunkPosVec2 position={chunkPosVec2
.clone() .clone()
.set((playerX + x) * 100, (playerY + y) * 100)} .set((playerX + x) * 100, (playerY + y) * 100)}
seed={0.25386} seed={0.25386}
/> />
); );
generatedChunkKeys.current.add(key);
} }
} }
setChunks([...chunks, ...newChunks]); setChunkQueue((prevQueue) => [...prevQueue, ...newChunks]);
}
function getFilteredChunks(prevChunks: JSX.Element[], playerPos: number[]) {
const filteredPrevChunks = [] as JSX.Element[];
let qtDeleted = 0;
prevChunks.forEach((chunk) => {
const chunkPos = getChunkCoordinates(
chunk.props.position.x,
chunk.props.position.y
);
if (
qtDeleted < CHUNK_PER_FRAME &&
isChunkInPlayerRadius(chunkPos, playerPos)
) {
generatedChunkKeys.current.delete(`${chunkPos[0]}-${chunkPos[1]}`);
qtDeleted++;
} else {
filteredPrevChunks.push(chunk);
}
});
return filteredPrevChunks;
}
function setNewChunks(queuedChunks: JSX.Element[], playerPos: number[]) {
setChunks((prevChunks) => {
const newChunks = queuedChunks
.splice(-CHUNK_PER_FRAME)
.filter(
(chunk) =>
!prevChunks.some((prevChunk) => prevChunk.key === chunk.key)
);
const filteredPrevChunks = getFilteredChunks(prevChunks, playerPos);
return [...filteredPrevChunks, ...newChunks];
});
} }
useFrame(() => { useFrame(() => {
const playerX = Math.floor(playerPosition.x / 100 + 0.5); const playerPos = getChunkCoordinates(playerPosition.x, playerPosition.z);
const playerY = Math.floor(playerPosition.z / 100 + 0.5);
if (lastPlayerPosition.x === playerX && lastPlayerPosition.y === playerY) if (chunkQueue.length > 0) {
return; setChunkQueue((prevQueue) => {
setNewChunks(prevQueue, playerPos);
return prevQueue;
});
}
generateNewChunks(playerX, playerY); if (
lastPlayerPosition.x !== playerPos[0] ||
lastPlayerPosition.set(playerX, playerY); lastPlayerPosition.y !== playerPos[1]
) {
generateNewChunks(playerPos[0], playerPos[1]);
lastPlayerPosition.set(playerPos[0], playerPos[1]);
}
}); });
useEffect(() => {
generateNewChunks(0, 0);
}, []);
return <>{chunks}</>; return <>{chunks}</>;
} }
+5 -9
View File
@@ -21,8 +21,7 @@ export interface KeyPressed {
sprint: boolean; sprint: boolean;
} }
const PLAYER_SPEED = 4.75 * 10; const PLAYER_SPEED = 4.5 * 100;
const DRAW_DISTANCE = 128;
const SHADOW_RESOLUTION = 2048; const SHADOW_RESOLUTION = 2048;
const MOUSE_X_SENSITIVITY = 0.002; const MOUSE_X_SENSITIVITY = 0.002;
const MOUSE_Y_SENSITIVITY = 0.001; const MOUSE_Y_SENSITIVITY = 0.001;
@@ -30,7 +29,7 @@ const ANIM_SPEED = 0.2;
const CAMERA_RADIUS = 2; const CAMERA_RADIUS = 2;
const CAMERA_HEIGHT = 1.5; const CAMERA_HEIGHT = 1.5;
const SHADOW_DRAW_DISTANCE = DRAW_DISTANCE * 1.2; const SHADOW_DRAW_DISTANCE = 128;
export function Player(): JSX.Element { export function Player(): JSX.Element {
const model = useGLTF("/Adventurer.glb"); const model = useGLTF("/Adventurer.glb");
@@ -181,7 +180,7 @@ export function Player(): JSX.Element {
function setDirLightPosition(): void { function setDirLightPosition(): void {
dirLightRef.current?.position.set( dirLightRef.current?.position.set(
model.scene.position.x + 100, model.scene.position.x + 100,
70, model.scene.position.y + 100,
model.scene.position.z + 100 model.scene.position.z + 100
); );
} }
@@ -203,7 +202,7 @@ export function Player(): JSX.Element {
child.receiveShadow = true; child.receiveShadow = true;
} }
}); });
model.scene.position.y = 10; model.scene.position.y = 70;
if (dirLightRef.current) { if (dirLightRef.current) {
dirLightRef.current.shadow.bias = -0.0001; dirLightRef.current.shadow.bias = -0.0001;
} }
@@ -234,10 +233,7 @@ export function Player(): JSX.Element {
]} ]}
/> />
</directionalLight> </directionalLight>
<fog <fog attach={"fog"} args={["lightblue", 1000, 2000]}></fog>
attach={"fog"}
args={["lightblue", DRAW_DISTANCE * 0.8, DRAW_DISTANCE]}
></fog>
</> </>
); );
} }