377 lines
9.2 KiB
JavaScript
377 lines
9.2 KiB
JavaScript
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()
|