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 { KeyboardControls } from "@react-three/drei";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import { PCFSoftShadowMap, Vector3 } from "three";
import { Player } from "./components/Player";
import { Bloom, EffectComposer, Vignette } from "@react-three/postprocessing";
import { Cubes } from "./components/Cubes";
import { GameContext } from "./hooks/GameContext";
import { ChunkGenerator } from "./components/ChunkGenerator";
const CUBES_QT = 10;
function App() {
const container = useRef<HTMLCanvasElement>(null);
@@ -19,15 +16,6 @@ function App() {
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 (
<main
className="container"
@@ -37,10 +25,8 @@ function App() {
className="canvas"
ref={container}
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() }}>
<KeyboardControls
map={[
+74 -64
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { memo, useEffect } from "react";
import {
ClampToEdgeWrapping,
FrontSide,
@@ -15,8 +15,8 @@ const grassTexture = new TextureLoader().load("./texture_grass.jpg");
grassTexture.wrapS = grassTexture.wrapT = RepeatWrapping;
grassTexture.repeat.set(100, 100);
const SCALE = 5;
const AMPLITUDE = 10;
const SCALE = 1;
const AMPLITUDE = 100;
const GEOMETRY = 50;
const SIZE = 100; // CHANGE WITH CAUTION
@@ -27,66 +27,76 @@ const ADJUSTED_GEOMETRY = GEOMETRY + OVERLAP * 2;
const ADJUSTED_SIZE = SIZE + OVERLAP * 2;
const ADJUSTED_SCALE = SIZE / SCALE;
export function Chunk(props: { position: Vector2; seed: number }): JSX.Element {
const noise = (x: number, y: number): number => {
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();
export const Chunk = memo(
(props: { position: Vector2; seed: number }): JSX.Element => {
const noise = (x: number, y: number): number => {
const noise = new ImprovedNoise();
return noise.noise(x, y, props.seed);
};
}, [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>
);
}
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 (
<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 { useFrame } from "@react-three/fiber";
import { Vector2 } from "three";
import { Chunk } from "./Chunk";
const CHUNK_RADIUS = 20;
const CHUNK_PER_FRAME = 3;
export function ChunkGenerator(): JSX.Element {
const { playerPosition } = useContext(GameContext);
const [lastPlayerPosition] = useState(new Vector2(0, 0));
const [lastPlayerPosition] = useState(new Vector2());
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 isChunkAlreadyGenerated = (x: number, y: number) =>
chunks.some((chunk) => chunk.key === `${x}-${y}`);
const getChunkCoordinates = (x: number, y: number): number[] => {
return [Math.floor(x / 100 + 0.5), Math.floor(y / 100 + 0.5)];
};
function generateNewChunks(playerX: number, playerY: number) {
const newChunks = [];
for (let x = -2; x <= 2; x++) {
for (let y = -2; y <= 2; y++) {
if (isChunkAlreadyGenerated(playerX + x, playerY + y)) continue;
const isChunkAlreadyGenerated = (x: number, y: number, key: string) =>
generatedChunkKeys.current.has(`${x}-${y}`) ||
chunkQueue.some((chunk) => chunk.key === key);
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(
<Chunk
key={`${playerX + x}-${playerY + y}`}
key={key}
position={chunkPosVec2
.clone()
.set((playerX + x) * 100, (playerY + y) * 100)}
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(() => {
const playerX = Math.floor(playerPosition.x / 100 + 0.5);
const playerY = Math.floor(playerPosition.z / 100 + 0.5);
const playerPos = getChunkCoordinates(playerPosition.x, playerPosition.z);
if (lastPlayerPosition.x === playerX && lastPlayerPosition.y === playerY)
return;
if (chunkQueue.length > 0) {
setChunkQueue((prevQueue) => {
setNewChunks(prevQueue, playerPos);
return prevQueue;
});
}
generateNewChunks(playerX, playerY);
lastPlayerPosition.set(playerX, playerY);
if (
lastPlayerPosition.x !== playerPos[0] ||
lastPlayerPosition.y !== playerPos[1]
) {
generateNewChunks(playerPos[0], playerPos[1]);
lastPlayerPosition.set(playerPos[0], playerPos[1]);
}
});
useEffect(() => {
generateNewChunks(0, 0);
}, []);
return <>{chunks}</>;
}
+5 -9
View File
@@ -21,8 +21,7 @@ export interface KeyPressed {
sprint: boolean;
}
const PLAYER_SPEED = 4.75 * 10;
const DRAW_DISTANCE = 128;
const PLAYER_SPEED = 4.5 * 100;
const SHADOW_RESOLUTION = 2048;
const MOUSE_X_SENSITIVITY = 0.002;
const MOUSE_Y_SENSITIVITY = 0.001;
@@ -30,7 +29,7 @@ const ANIM_SPEED = 0.2;
const CAMERA_RADIUS = 2;
const CAMERA_HEIGHT = 1.5;
const SHADOW_DRAW_DISTANCE = DRAW_DISTANCE * 1.2;
const SHADOW_DRAW_DISTANCE = 128;
export function Player(): JSX.Element {
const model = useGLTF("/Adventurer.glb");
@@ -181,7 +180,7 @@ export function Player(): JSX.Element {
function setDirLightPosition(): void {
dirLightRef.current?.position.set(
model.scene.position.x + 100,
70,
model.scene.position.y + 100,
model.scene.position.z + 100
);
}
@@ -203,7 +202,7 @@ export function Player(): JSX.Element {
child.receiveShadow = true;
}
});
model.scene.position.y = 10;
model.scene.position.y = 70;
if (dirLightRef.current) {
dirLightRef.current.shadow.bias = -0.0001;
}
@@ -234,10 +233,7 @@ export function Player(): JSX.Element {
]}
/>
</directionalLight>
<fog
attach={"fog"}
args={["lightblue", DRAW_DISTANCE * 0.8, DRAW_DISTANCE]}
></fog>
<fog attach={"fog"} args={["lightblue", 1000, 2000]}></fog>
</>
);
}