From 833d0f91342d554681219f62b8309ee5a7bf5e15 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Mon, 17 Jun 2024 18:34:39 +0200 Subject: [PATCH] feat: new terrain generation approach --- public/grass.png | Bin 1283 -> 1279 bytes src/block.ts | 50 ----------------- src/chunk.ts | 126 ------------------------------------------- src/face.ts | 83 ---------------------------- src/main.ts | 112 ++++++++++++++++++++++++++------------ src/textureLoader.ts | 96 ++++++++++++++++----------------- 6 files changed, 124 insertions(+), 343 deletions(-) delete mode 100644 src/block.ts delete mode 100644 src/chunk.ts delete mode 100644 src/face.ts diff --git a/public/grass.png b/public/grass.png index e8b9d7e1fc7beeedaa01b7992971713e1d242b2d..f42e9ee0e445b96b569d74bbeb66e2dd07f33edd 100644 GIT binary patch delta 1222 zcmV;%1UdVI3jYa^Nq@iq01m(bYSxJf000D(Nkld;zfVX`oqQG2qqie$Cw zBaEj_$X79b{%S}MvF0}I*V#>EdVY~xYkRYipUye0KSuCLaIKAn77cEB58U`;;3f<) z4Ix$L6&M-#XD;wZXox(jN7SEw%K>5K^Q|a;MBEpqff6n{ix^BiQz5LyGJ_&>f^Bo9aYN4D?3F7NY@_6vR!n zuD25hW|jxSum%y9@zfhRo^+#Fn4mif%n}Rn%9T)_CfJj@r(r&q5&Xb zO@Gkwm=CAO{f3U$4lr*IXa0&9;SZ z9s@#r{d|dXGurm;aEPefwXDc0+yLF7!FkH_?6wlWIGK}>D<_=7U2m575rYmkpjGn4ERf>7t)Dz?FV>DG(bVQL!#!D#~}#1?MCsH1pHQ7N$1i+TB`o0 z;D)ReAd0L@wSUQvkrt|zI8YJ2a1G_1%LuknnACn(eeXoW+|6+TA7sEc&yVTtTJcEn zlmjF}0F-9#?caDCS?&uIubpH#?I?(@YZ-yWQMgU7&W#L->}=E?x=^r|vO|eFxS=q< zb?+N-XaIZeNy?3%@c{OzE$_)LbgV7N{a*ykD<&&O` zv`NCfXB~T~_TJl;{taHKUL7#p)~7NeNXU8KyHV~Hrcoa}@J6B+aJsjbR}g>bAM&7w zMN@d{UVnM>v+xYOVE<1I;5E!~;%a|%(5@jq|IV=AefTJAuHK1Z4h6mLJ5-(iy)e7Q zFU6PNrsdfz-Gw@|7Fw7rjAqQ#Wa%z~YSnkxPn(c0WBmNhkRD>qZCcgYP2}c|^ECf88CNy?{;R{_G8=SjgFDUZ z?aS3a=>(Vl7{MpOwKfh~G`!_Ku<+Z!A`CGNAyu9$uru(_Gui{gVu-$#hIt+qITKC3 z&41S2pCu0icPzz2xnp-K(^Sh?#_d$bj#*WzO$3!zOD#PzgcMN;cFKHuC#tlv8OQZF z!fiDhQl$P+caqL*suM9WpbfVOmQEaiMS>xLmOPFS~aO-ZkD(9}qV}I0> zhn(}2=h8KiN3uXD~!1lL}kPRJWzLW3`fyE)D$N zP7#v$)S*LseUd9bKttg)jD2cdv_ofpEQ*&CTKsvj-FVEgM;VyMg$(+SRJcrp)9+>O ob!73c^WcuhxSNOncplXL0!|_PQ%YehZvX%Q07*qoM6N<$f>*R=WdHyG diff --git a/src/block.ts b/src/block.ts deleted file mode 100644 index 9ddcd9b..0000000 --- a/src/block.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Object3D, Vector3 } from "three" -import Face from "./face" -import { degToRad } from "three/src/math/MathUtils.js" - -function randInt(min: number, max: number): number { - const minCeiled = Math.ceil(min) - const maxFloored = Math.floor(max) - return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled) -} - -export default class Block { - faces: Face[] - facesIndexes: number[] = [] - position: Vector3 - - constructor(faces: Face[], position: Vector3) { - this.faces = faces - this.position = position - this.initFaces() - } - - setPosition(vec3: Vector3) { - this.position = vec3 - this.updateFacesPosition() - } - - private updateFacesPosition() { - this.faces.forEach((face, i) => { - const dummy = new Object3D() - dummy.position.add(this.position) - dummy.updateMatrix() - face.instancedMesh!.setMatrixAt(this.facesIndexes[i], dummy.matrix) - face.instancedMesh!.instanceMatrix.needsUpdate = true - }) - } - - private initFaces() { - this.faces.forEach((face) => { - const dummy = new Object3D() - dummy.position.add(this.position) - if (face.orientation === "Up" || face.orientation === "Down") { - dummy.rotateOnAxis(new Vector3(0, 1, 0), degToRad(90 * randInt(0, 4))) - } - dummy.updateMatrix() - face.instancedMesh?.setMatrixAt(face.instaceIndex, dummy.matrix) - this.facesIndexes.push(face.instaceIndex) - face.instaceIndex += 1 - }) - } -} diff --git a/src/chunk.ts b/src/chunk.ts deleted file mode 100644 index 23af92d..0000000 --- a/src/chunk.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { InstancedMesh, Texture, Vector3 } from "three" -import Face, { Orientation } from "./face" - -const FACES_OFFSETS: { [key: string]: Vector3 } = { - Up: new Vector3(0, 1, 0), - North: new Vector3(0, 0, 1), - South: new Vector3(0, 0, -1), - East: new Vector3(-1, 0, 0), - West: new Vector3(1, 0, 0), - Down: new Vector3(0, -1, 0) -} - -interface BlockData { - position: Vector3 - textureName: string -} - -export default class Chunk { - position: Vector3 - chunk: BlockData[] - chunkSet: Set - radius: number - textures: { [name: string]: Texture[] } - faces: { [key: string]: Face[] } - - constructor( - radius: number, - textures: { [name: string]: Texture[] }, - position: Vector3 - ) { - this.position = position - this.chunk = [] - this.chunkSet = new Set() - this.radius = radius - this.textures = textures - this.faces = {} - } - - async generateChunk() { - return new Promise((resolve) => { - for (let i = this.position.x; i < this.radius + this.position.x; i++) { - for (let j = this.position.z; j < this.radius + this.position.z; j++) { - const max = Math.ceil(Math.sin(i / 30) * 10 + Math.cos(j / 30) * 10) - for (let k = -16; k <= max; k++) { - const position = new Vector3(i, k, j) - let textureName = "stone" - if (k === max) textureName = "grass" - else if (k > max - 6) textureName = "dirt" - this.chunk.push({ position, textureName }) - this.chunkSet.add(position.toArray().toString()) - } - } - } - resolve("") - }) - } - - initializeFaces() { - for (const textureName in this.textures) { - const textureFaces = this.textures[textureName].map((texture, index) => { - const orientation = ["Up", "North", "South", "East", "West", "Down"][ - index - ] as Orientation - return new Face(orientation, 0, texture) - }) - this.faces[textureName] = textureFaces - } - } - - filterFaces(block: Vector3, textureName: string): Face[] { - return this.faces[textureName].filter((face) => { - const offset = FACES_OFFSETS[face.orientation] - const adjacentBlock = new Vector3( - block.x + offset.x, - block.y + offset.y, - block.z + offset.z - ) - return !this.chunkSet.has(adjacentBlock.toArray().toString()) - }) - } - - countVisibleFaces() { - const faceCounts: { [key: string]: { [key: string]: number } } = {} - - this.chunk.forEach(({ position, textureName }) => { - if (!faceCounts[textureName]) { - faceCounts[textureName] = { - Up: 0, - North: 0, - South: 0, - East: 0, - West: 0, - Down: 0 - } - } - Object.keys(FACES_OFFSETS).forEach((orientation) => { - const offset = FACES_OFFSETS[orientation] - const adjacentBlock = new Vector3( - position.x + offset.x, - position.y + offset.y, - position.z + offset.z - ) - if (!this.chunkSet.has(adjacentBlock.toArray().toString())) { - faceCounts[textureName][orientation]++ - } - }) - }) - - for (const textureName in faceCounts) { - for (const orientation in faceCounts[textureName]) { - const face = this.faces[textureName].find( - (f) => f.orientation === orientation - ) - if (!face?.instancedMesh) return - face.instancedMesh.count = faceCounts[textureName][orientation] - face.instancedMesh = new InstancedMesh( - face?.geometry, - face?.material, - faceCounts[textureName][orientation] - ) // TODO dégueulasse - } - } - - return faceCounts - } -} diff --git a/src/face.ts b/src/face.ts deleted file mode 100644 index 82e5d2a..0000000 --- a/src/face.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - InstancedMesh, - MeshBasicMaterial, - PlaneGeometry, - Texture, - Vector3 -} from "three" - -export type Orientation = "North" | "South" | "East" | "West" | "Up" | "Down" - -export const DEG_RAD = Math.PI / 180 -export const FACE_ORIENTATION = [ - { - orientation: "North", - position: new Vector3(0, 0, 0.5), - rotation: new Vector3(0, 0, 0) - }, - { - orientation: "Down", - position: new Vector3(0, -0.5, 0), - rotation: new Vector3(90 * DEG_RAD, 0, 0) - }, - { - orientation: "Up", - position: new Vector3(0, 0.5, 0), - rotation: new Vector3(-90 * DEG_RAD, 0, 0) - }, - { - orientation: "East", - position: new Vector3(-0.5, 0, 0), - rotation: new Vector3(0, -90 * DEG_RAD, 0) - }, - { - orientation: "West", - position: new Vector3(0.5, 0, 0), - rotation: new Vector3(0, 90 * DEG_RAD, 0) - }, - { - orientation: "South", - position: new Vector3(0, 0, -0.5), - rotation: new Vector3(0, 180 * DEG_RAD, 0) - } -] - -export default class Face { - material: MeshBasicMaterial - orientation: Orientation - geometry: PlaneGeometry - instancedMesh: InstancedMesh | undefined - instaceIndex: number = 0 - - constructor( - orientation: Orientation, - instancesCount: number, - texture: Texture - ) { - this.material = new MeshBasicMaterial({ - map: texture - }) - - this.orientation = orientation - - const orientationFace = FACE_ORIENTATION.find( - (x) => x.orientation === orientation - ) - - this.geometry = new PlaneGeometry() - this.geometry.rotateX(orientationFace!.rotation.x) - this.geometry.rotateY(orientationFace!.rotation.y) - this.geometry.rotateZ(orientationFace!.rotation.z) - this.geometry.translate( - orientationFace!.position.x, - orientationFace!.position.y, - orientationFace!.position.z - ) - - this.instancedMesh = new InstancedMesh( - this.geometry, - this.material, - instancesCount - ) - } -} diff --git a/src/main.ts b/src/main.ts index be728d1..71288ee 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,6 @@ import "./style.css" import * as THREE from "three" import { OrbitControls } from "three/addons/controls/OrbitControls.js" -import Chunk from "./chunk" -import Block from "./block" import TextureLoader from "./textureLoader" const scene = new THREE.Scene() @@ -20,47 +18,92 @@ const controls = new OrbitControls(camera, renderer.domElement) controls.update() document.body.appendChild(renderer.domElement) -let textures: { [name: string]: THREE.Texture[] } = {} +const blocks: { + [name: string]: { mesh: THREE.InstancedMesh; index: number; count: number } +} = {} -async function loadTextures() { - const textureLoader = new TextureLoader(16, 16) - textures["grass"] = await textureLoader.load("/grass.png") - textures["dirt"] = await textureLoader.load("/dirt.png") - textures["stone"] = await textureLoader.load("/stone.png") - onTexturesLoaded() +const blocksToLoad = ["grass", "dirt", "stone"] + +async function initBlocks() { + const textureLoader = new TextureLoader() + for (let i = 0; i < blocksToLoad.length; i++) { + const blockName = blocksToLoad[i] + const textures = await textureLoader.load(`/${blockName}.png`, 16, 16) + const geometry = new THREE.BoxGeometry(1, 1) + const materials = textures.map((texture) => { + return new THREE.MeshBasicMaterial({ map: texture }) + }) + const mesh = new THREE.InstancedMesh(geometry, materials, 0) + blocks[blockName] = { mesh, index: 0, count: 0 } + scene.add(mesh) + } + onBlocksInitiated() } -function onTexturesLoaded() { - const radius = 16 +const terrain = {} as { + [blockName: string]: { x: number; y: number; z: number }[] +} - const positions = [] as THREE.Vector3[] - for (let i = -0.5; i < 0.5; i++) { - for (let j = -0.5; j < 0.5; j++) { - positions.push(new THREE.Vector3(i * radius, 0, j * radius)) +function initTerrain() { + for (let x = -256; x < 256; x++) { + for (let z = -256; z < 256; z++) { + for (let y = 0; y < 16; y++) { + let blockName = "stone" + if (y >= 11 && y < 15) { + blockName = "dirt" + } else if (y >= 15) { + blockName = "grass" + } + if (!terrain[blockName]) terrain[blockName] = [] + terrain[blockName].push({ x, y, z }) + } } } +} - const chunks = positions.map( - (position) => new Chunk(radius, textures, position) - ) - chunks.map((chunk) => - chunk.generateChunk().then(() => { - chunk.initializeFaces() - chunk.countVisibleFaces() +function onBlocksInitiated() { + initTerrain() - const cubes = [] as Block[] - chunk.chunk.forEach(({ position, textureName }) => { - const faces = chunk.filterFaces(position, textureName) - cubes.push(new Block(faces, position)) - }) + for (const blockName in terrain) { + const positions = terrain[blockName] - Object.values(chunk.faces).forEach((faceArray) => { - faceArray.forEach((face) => { - if (face.instancedMesh) scene.add(face.instancedMesh) - }) - }) + const block = blocks[blockName] + block.count = positions.length + // block.mesh.geometry.applyMatrix4(block.mesh.matrix) + const newMesh = new THREE.InstancedMesh( + block.mesh.geometry, + block.mesh.material, + block.count + ) + block.mesh = newMesh + scene.add(block.mesh) + + positions.forEach((position) => { + const dummy = new THREE.Object3D() + dummy.position.set(position.x, position.y, position.z) + dummy.updateMatrix() + block.mesh.setMatrixAt(block.index, dummy.matrix) + block.index += 1 }) - ) + } + + // const dummy = new THREE.Object3D() + // dummy.position.set(x, y, z) + // dummy.updateMatrix() + + // const block = blocks[blockName] + // block.mesh.setMatrixAt(blocks[blockName].index, dummy.matrix) + // block.index += 1 + // block.count += 1 + + // if (block.count > block.mesh.count - 1) { + // const newMesh = new THREE.InstancedMesh( + // block.mesh.geometry, + // block.mesh.material, + // block.mesh.count + 64 + // ) + // block.mesh = newMesh + // }} } camera.position.x = 15 @@ -69,8 +112,7 @@ camera.position.z = 15 function animate(elapsedTimeMs: number) { renderer.render(scene, camera) - console.log(renderer.info.render.calls) controls.update() } -loadTextures() +initBlocks() diff --git a/src/textureLoader.ts b/src/textureLoader.ts index 2584621..d068c5f 100644 --- a/src/textureLoader.ts +++ b/src/textureLoader.ts @@ -1,58 +1,56 @@ -import { NearestFilter, Texture } from "three"; +import { NearestFilter, Texture } from "three" export default class TextureLoader { - private image: HTMLImageElement; - private width: number; - private height: number; + private image: HTMLImageElement = document.createElement("img") + private width: number = 16 + private height: number = 16 - constructor(width: number, height: number) { - this.image = document.createElement("img"); - this.width = width; - this.height = height; - } + load(src: string, width: number, heigth: number): Promise { + return new Promise((resolve, reject) => { + this.image = document.createElement("img") + this.image.src = src + this.width = width + this.height = heigth + this.image.addEventListener( + "load", + () => { + const textures = this.onImageLoaded() + resolve(textures) + }, + false + ) + this.image.addEventListener("error", (err) => reject(err), false) + }) + } - load(src: string): Promise { - return new Promise((resolve, reject) => { - this.image.src = src; - this.image.addEventListener( - "load", - () => { - const textures = this.onImageLoaded(); - resolve(textures); - }, - false - ); - this.image.addEventListener("error", (err) => reject(err), false); - }); - } + private onImageLoaded(): Texture[] { + const textures = [] as Texture[] - private onImageLoaded(): Texture[] { - const textures = [] as Texture[]; + for (let i = 0; i < 6; i++) { + const canvas = document.createElement("canvas") + canvas.width = this.width + canvas.height = this.height - for (let i = 0; i < 6; i++) { - const canvas = document.createElement("canvas"); - canvas.width = this.width; - canvas.height = this.height; + const context = canvas.getContext("2d") + context?.drawImage( + this.image, + 0, + this.height * i, + this.width, + this.height, + 0, + 0, + this.width, + this.height + ) - const context = canvas.getContext("2d"); - context?.drawImage( - this.image, - 0, - this.height * i, - this.width, - this.height, - 0, - 0, - this.width, - this.height - ); + const texture = new Texture(canvas) + texture.needsUpdate = true + texture.magFilter = NearestFilter + textures.push(texture) + } - const texture = new Texture(canvas); - texture.needsUpdate = true; - texture.magFilter = NearestFilter; - textures.push(texture); - } - - return textures; - } + console.log(this.image.src + " loaded.") + return textures + } }