feat: new terrain generation approach

This commit is contained in:
Florian Sylvain
2024-06-17 18:34:39 +02:00
parent a6fb19a2e4
commit 833d0f9134
6 changed files with 124 additions and 343 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

-50
View File
@@ -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
})
}
}
-126
View File
@@ -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<string>
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<string>()
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
}
}
-83
View File
@@ -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
)
}
}
+76 -34
View File
@@ -1,8 +1,6 @@
import "./style.css" import "./style.css"
import * as THREE from "three" import * as THREE from "three"
import { OrbitControls } from "three/addons/controls/OrbitControls.js" import { OrbitControls } from "three/addons/controls/OrbitControls.js"
import Chunk from "./chunk"
import Block from "./block"
import TextureLoader from "./textureLoader" import TextureLoader from "./textureLoader"
const scene = new THREE.Scene() const scene = new THREE.Scene()
@@ -20,47 +18,92 @@ const controls = new OrbitControls(camera, renderer.domElement)
controls.update() controls.update()
document.body.appendChild(renderer.domElement) 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 blocksToLoad = ["grass", "dirt", "stone"]
const textureLoader = new TextureLoader(16, 16)
textures["grass"] = await textureLoader.load("/grass.png") async function initBlocks() {
textures["dirt"] = await textureLoader.load("/dirt.png") const textureLoader = new TextureLoader()
textures["stone"] = await textureLoader.load("/stone.png") for (let i = 0; i < blocksToLoad.length; i++) {
onTexturesLoaded() 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 terrain = {} as {
const radius = 16 [blockName: string]: { x: number; y: number; z: number }[]
}
const positions = [] as THREE.Vector3[] function initTerrain() {
for (let i = -0.5; i < 0.5; i++) { for (let x = -256; x < 256; x++) {
for (let j = -0.5; j < 0.5; j++) { for (let z = -256; z < 256; z++) {
positions.push(new THREE.Vector3(i * radius, 0, j * radius)) 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( function onBlocksInitiated() {
(position) => new Chunk(radius, textures, position) initTerrain()
for (const blockName in terrain) {
const positions = terrain[blockName]
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
) )
chunks.map((chunk) => block.mesh = newMesh
chunk.generateChunk().then(() => { scene.add(block.mesh)
chunk.initializeFaces()
chunk.countVisibleFaces()
const cubes = [] as Block[] positions.forEach((position) => {
chunk.chunk.forEach(({ position, textureName }) => { const dummy = new THREE.Object3D()
const faces = chunk.filterFaces(position, textureName) dummy.position.set(position.x, position.y, position.z)
cubes.push(new Block(faces, position)) dummy.updateMatrix()
block.mesh.setMatrixAt(block.index, dummy.matrix)
block.index += 1
}) })
}
Object.values(chunk.faces).forEach((faceArray) => { // const dummy = new THREE.Object3D()
faceArray.forEach((face) => { // dummy.position.set(x, y, z)
if (face.instancedMesh) scene.add(face.instancedMesh) // 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 camera.position.x = 15
@@ -69,8 +112,7 @@ camera.position.z = 15
function animate(elapsedTimeMs: number) { function animate(elapsedTimeMs: number) {
renderer.render(scene, camera) renderer.render(scene, camera)
console.log(renderer.info.render.calls)
controls.update() controls.update()
} }
loadTextures() initBlocks()
+26 -28
View File
@@ -1,40 +1,37 @@
import { NearestFilter, Texture } from "three"; import { NearestFilter, Texture } from "three"
export default class TextureLoader { export default class TextureLoader {
private image: HTMLImageElement; private image: HTMLImageElement = document.createElement("img")
private width: number; private width: number = 16
private height: number; private height: number = 16
constructor(width: number, height: number) { load(src: string, width: number, heigth: number): Promise<Texture[]> {
this.image = document.createElement("img");
this.width = width;
this.height = height;
}
load(src: string): Promise<Texture[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.image.src = src; this.image = document.createElement("img")
this.image.src = src
this.width = width
this.height = heigth
this.image.addEventListener( this.image.addEventListener(
"load", "load",
() => { () => {
const textures = this.onImageLoaded(); const textures = this.onImageLoaded()
resolve(textures); resolve(textures)
}, },
false false
); )
this.image.addEventListener("error", (err) => reject(err), false); this.image.addEventListener("error", (err) => reject(err), false)
}); })
} }
private onImageLoaded(): Texture[] { private onImageLoaded(): Texture[] {
const textures = [] as Texture[]; const textures = [] as Texture[]
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas")
canvas.width = this.width; canvas.width = this.width
canvas.height = this.height; canvas.height = this.height
const context = canvas.getContext("2d"); const context = canvas.getContext("2d")
context?.drawImage( context?.drawImage(
this.image, this.image,
0, 0,
@@ -45,14 +42,15 @@ export default class TextureLoader {
0, 0,
this.width, this.width,
this.height this.height
); )
const texture = new Texture(canvas); const texture = new Texture(canvas)
texture.needsUpdate = true; texture.needsUpdate = true
texture.magFilter = NearestFilter; texture.magFilter = NearestFilter
textures.push(texture); textures.push(texture)
} }
return textures; console.log(this.image.src + " loaded.")
return textures
} }
} }