mirror of
https://github.com/Floriansylvain/ThatsMyWorld.git
synced 2026-08-19 11:43:16 +02:00
feat: new terrain generation approach
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user