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={[
+17 -7
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,7 +27,8 @@ 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(
(props: { position: Vector2; seed: number }): JSX.Element => {
const noise = (x: number, y: number): number => { const noise = (x: number, y: number): number => {
const noise = new ImprovedNoise(); const noise = new ImprovedNoise();
return noise.noise(x, y, props.seed); return noise.noise(x, y, props.seed);
@@ -46,7 +47,10 @@ export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
for (let x = 0; x < ADJUSTED_SIZE; x++) { for (let x = 0; x < ADJUSTED_SIZE; x++) {
for (let y = 0; y < ADJUSTED_SIZE; y++) { for (let y = 0; y < ADJUSTED_SIZE; y++) {
const n = const n =
noise((x + offsetX) / ADJUSTED_SCALE, (y + offsetY) / ADJUSTED_SCALE) * noise(
(x + offsetX) / ADJUSTED_SCALE,
(y + offsetY) / ADJUSTED_SCALE
) *
127 + 127 +
127; 127;
const id = (x + y * ADJUSTED_SIZE) * 4; const id = (x + y * ADJUSTED_SIZE) * 4;
@@ -76,7 +80,12 @@ export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
> >
<planeGeometry <planeGeometry
attach="geometry" attach="geometry"
args={[CHUNK_SIZE, CHUNK_SIZE, ADJUSTED_GEOMETRY, ADJUSTED_GEOMETRY]} args={[
CHUNK_SIZE,
CHUNK_SIZE,
ADJUSTED_GEOMETRY,
ADJUSTED_GEOMETRY,
]}
></planeGeometry> ></planeGeometry>
<meshStandardMaterial <meshStandardMaterial
toneMapped={false} toneMapped={false}
@@ -89,4 +98,5 @@ export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
</mesh> </mesh>
</group> </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);
generateNewChunks(playerX, playerY); return prevQueue;
lastPlayerPosition.set(playerX, playerY);
}); });
}
useEffect(() => { if (
generateNewChunks(0, 0); lastPlayerPosition.x !== playerPos[0] ||
}, []); lastPlayerPosition.y !== playerPos[1]
) {
generateNewChunks(playerPos[0], playerPos[1]);
lastPlayerPosition.set(playerPos[0], playerPos[1]);
}
});
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>
</> </>
); );
} }