import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import Worker from './chunk.worker.js' const renderer = new THREE.WebGLRenderer() renderer.setSize(window.innerWidth, window.innerHeight) const div = document.createElement('div') div.appendChild(renderer.domElement) document.body.appendChild(div) renderer.setClearColor(0x00aaff) const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000) const controls = new OrbitControls(camera, renderer.domElement) class GeometryFromArrays extends THREE.BufferGeometry { constructor (vertices, indices, normals) { super() this.setIndex(indices) this.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)) this.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)) // this.computeVertexNormals() } } const mat = new THREE.MeshStandardMaterial() mat.color = new THREE.Color(0x02ff02) // mat.wireframe = true // mat.side = THREE.DoubleSide /* mat.vertexShader = ` void main (void) { gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position,1); } ` mat.fragmentShader = ` void main (void) { gl_FragColor = vec4(0.2, 1.0, 0.2, 1.0); } ` */ const LEFT = 0 const RIGHT = 1 const FRONT = 2 const BACK = 3 const TOP = 4 const BOTTOM = 5 let workerIDs = 0 let chunkIDs = 0 class WorkerThread { constructor (script) { this.worker = new Worker(script) this.worker.onmessage = (e) => this.onMessage(e) this.resolve = null this.id = workerIDs++ this.worker.onerror = function (event) { console.log(event.message, event) } } onMessage (e) { const resolve = this.resolve this.resolve = null resolve(e.data) } postMessage (data, resolve) { this.resolve = resolve this.worker.postMessage(data) } } class WorkerThreadPool { constructor (workers, script) { this.workers = [...Array(workers)].map(_ => new WorkerThread(script)) this.free = [...this.workers] this.busy = {} this.queue = [] } enqueue (data, resolve) { this.queue.push([data, resolve]) this.pump() } pump () { while (this.free.length > 0 && this.queue.length > 0) { const w = this.free.pop() this.busy[w.id] = w const [workItem, workResolve] = this.queue.shift() w.postMessage(workItem, (v) => { delete this.busy[w.id] this.free.push(w) workResolve(v) this.pump() }) } } } class Chunk { constructor (w, x, y, dims) { this.world = w this.volume = [] this.dims = dims this.x = x this.y = y this.dirty = false this.id = chunkIDs++ this.generatorWaiting = false this.mesherWaiting = false this.disposed = false this.meshReady = false } generate () { if (this.volume.length || this.generatorWaiting || this.disposed) return this.generatorWaiting = true this.world.thread.enqueue(JSON.stringify(Object.assign({ subject: 'gen', x: this.x, y: this.y, dims: this.dims }, this.world.noiseParams)), (e) => { e = JSON.parse(e) this.generatorWaiting = false if (e.subject !== 'gen_result' || this.disposed) return this.volume = e.data this.dirty = true this.notifyNeighbors() }) } getBlockAt (x, y, z) { if (x < 0) return this.getNeighbor(LEFT).getBlockAt(this.dims[0] - 1, y, z) if (x >= this.dims[0]) return this.getNeighbor(RIGHT).getBlockAt(0, y, z) if (z < 0) return this.getNeighbor(FRONT).getBlockAt(x, y, this.dims[2] - 1) if (z >= this.dims[2]) return this.getNeighbor(BACK).getBlockAt(x, y, 0) if (y < 0 || y >= this.dims[1]) return null return this.volume[x + this.dims[0] * (y + this.dims[1] * z)] } getNeighbor (side) { let neighbor switch (side) { case LEFT: neighbor = this.world.getChunkByPosition(this.x - 1, this.y) break case RIGHT: neighbor = this.world.getChunkByPosition(this.x + 1, this.y) break case FRONT: neighbor = this.world.getChunkByPosition(this.x, this.y - 1) break case BACK: neighbor = this.world.getChunkByPosition(this.x, this.y + 1) break } return neighbor } hasNeighbors () { let has = true for (let i = 0; i < 4; i++) { const n = this.getNeighbor(i) if (!n || !n.isGenerated()) { has = false break } } return has } notifyNeighbors () { for (let i = 0; i < 4; i++) { const n = this.getNeighbor(i) if (n) n.markDirty() } } isGenerated () { return this.volume.length > 0 } isMeshed () { return this.mesh != null } markDirty () { this.dirty = true } createMesh () { if (this.meshReady) { if (this.disposed) return this.destroyMesh() scene.add(this.mesh) this.dirty = false this.meshReady = false return } if (!this.hasNeighbors() || this.disposed || this.mesherWaiting) return false this.mesherWaiting = true this.world.thread.enqueue(JSON.stringify({ subject: 'mesh', dims: this.dims, volume: this.volume, neighbors: [ this.getNeighbor(RIGHT).volume, this.getNeighbor(LEFT).volume, this.getNeighbor(FRONT).volume, this.getNeighbor(BACK).volume ] }), (e) => { e = JSON.parse(e) this.mesherWaiting = false if (e.subject !== 'mesh_data' || this.disposed) return const geom = new GeometryFromArrays(e.data.vertices, e.data.indices, e.data.normals) const mesh = new THREE.Mesh(geom, mat) mesh.position.set(this.x * this.dims[0], 0, this.y * this.dims[2]) if (this.mesh) this.destroyMesh() this.mesh = mesh this.meshReady = true }) return true } dispose () { this.disposed = true this.destroyMesh() } destroyMesh () { if (!this.mesh) return scene.remove(this.mesh) this.mesh.geometry.dispose() this.mesh = null } } class ChunkWorld { constructor (dims) { this.dims = dims this.chunks = {} // amplitude - Controls the amount the height changes. The higher, the taller the hills. // period - Distance above which we start to see similarities. The higher, the longer "hills" will be on a terrain. // persistence - Controls details, value in [0,1]. Higher increases grain, lower increases smoothness. // lacunarity - Controls period change across octaves. 2 is usually a good value to address all detail levels. // octaves - Number of noise layers this.noiseParams = { seed: '123', amplitude: 15, period: 0.01, persistence: 0.4, lacunarity: 2, octaves: 5 } this.thread = new WorkerThreadPool(4, 'src/chunk-worker.js') this.generateQueue = [] this.updateQueue = [] this.rebuildQueue = [] this.loadQueue = [] this.unloadQueue = [] } getChunkByPosition (x, y) { return this.chunks[x + ';' + y] } updateChunks (cam) { for (const ch in this.chunks) { const chunk = this.chunks[ch] const dist = cam.position.distanceTo(new THREE.Vector3(chunk.x * this.dims[0], cam.position.y, chunk.y * this.dims[2])) if (dist < 256) { if (chunk.dirty && chunk.isGenerated()) this.rebuildQueue.push(chunk) if (!chunk.isGenerated()) this.generateQueue.push(chunk) } else { this.unloadQueue.push(chunk) } } const grid = new THREE.Vector3( Math.floor(cam.position.x / this.dims[0]), Math.floor(cam.position.y / this.dims[1]), Math.floor(cam.position.z / this.dims[2]) ) for (let x = grid.x - 6; x < grid.x + 6; x++) { for (let y = grid.z - 6; y < grid.z + 6; y++) { if (this.getChunkByPosition(x, y)) continue const c = new Chunk(this, x, y, this.dims) this.chunks[x + ';' + y] = c } } } rebuildChunks () { for (const i in this.rebuildQueue) { const c = this.rebuildQueue[i] c.createMesh() } this.rebuildQueue = [] } generateChunks () { for (const i in this.generateQueue) { const c = this.generateQueue[i] c.generate() } this.generateQueue = [] } unloadChunks () { for (const i in this.unloadQueue) { const c = this.unloadQueue[i] c.dispose() delete this.chunks[c.x + ';' + c.y] } this.unloadQueue = [] } update (cam) { this.updateChunks(cam) this.generateChunks() this.rebuildChunks() this.unloadChunks() } } const cw = new ChunkWorld([16, 256, 16]) camera.position.x = 0 camera.position.y = 150 camera.position.z = 0 const light = new THREE.DirectionalLight(0xffffff, 0.5) light.position.set(1, 1, 1) scene.add(light) const alight = new THREE.AmbientLight(0x202020) scene.add(alight) controls.update() function loop () { window.requestAnimationFrame(loop) controls.update() cw.update(camera) renderer.render(scene, camera) } function start () { let mpos = new THREE.Vector2(0, 0) div.addEventListener('pointerdown', function (e) { console.log('?') }) div.addEventListener('mousemove', function (e) { }) div.addEventListener('mouseup', function (e) { }) loop() } renderer.domElement.addEventListener('keyup', function (e) { if (e.key === 'x') mat.wireframe = !mat.wireframe }) start()