voxeltest/src/index.js

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()