368 lines
9.5 KiB
JavaScript
368 lines
9.5 KiB
JavaScript
import { Mesh } from '../mesh'
|
|
import { Node, MeshInstance } from '../components'
|
|
import { addv3 } from '../utility'
|
|
|
|
const FACE_VERTEX = [
|
|
// Bottom
|
|
[
|
|
[1.0, 0.0, 0.0],
|
|
[1.0, 0.0, 1.0],
|
|
[0.0, 0.0, 1.0],
|
|
[0.0, 0.0, 0.0],
|
|
[0.0, -1.0, 0.0]
|
|
],
|
|
// Top
|
|
[
|
|
[0.0, 1.0, 0.0],
|
|
[0.0, 1.0, 1.0],
|
|
[1.0, 1.0, 1.0],
|
|
[1.0, 1.0, 0.0],
|
|
[0.0, 1.0, 0.0]
|
|
],
|
|
// Left
|
|
[
|
|
[0.0, 0.0, 1.0],
|
|
[0.0, 1.0, 1.0],
|
|
[0.0, 1.0, 0.0],
|
|
[0.0, 0.0, 0.0],
|
|
[-1.0, 0.0, 0.0]
|
|
],
|
|
// Right
|
|
[
|
|
[1.0, 0.0, 0.0],
|
|
[1.0, 1.0, 0.0],
|
|
[1.0, 1.0, 1.0],
|
|
[1.0, 0.0, 1.0],
|
|
[1.0, 0.0, 0.0]
|
|
],
|
|
// Front
|
|
[
|
|
[1.0, 0.0, 1.0],
|
|
[1.0, 1.0, 1.0],
|
|
[0.0, 1.0, 1.0],
|
|
[0.0, 0.0, 1.0],
|
|
[0.0, 0.0, 1.0]
|
|
],
|
|
// Back
|
|
[
|
|
[0.0, 0.0, 0.0],
|
|
[0.0, 1.0, 0.0],
|
|
[1.0, 1.0, 0.0],
|
|
[1.0, 0.0, 0.0],
|
|
[0.0, 0.0, -1.0]
|
|
]
|
|
]
|
|
|
|
const FACE_BOTTOM = 0
|
|
const FACE_TOP = 1
|
|
const FACE_LEFT = 2
|
|
const FACE_RIGHT = 3
|
|
const FACE_FRONT = 4
|
|
const FACE_BACK = 5
|
|
|
|
class Voxel {
|
|
constructor (id) {
|
|
this.id = id
|
|
}
|
|
|
|
get solid () {
|
|
return this.id !== 0
|
|
}
|
|
}
|
|
|
|
const AIR_VOXEL = new Voxel(0)
|
|
const GROUND_VOXEL = new Voxel(1)
|
|
|
|
class VoxelChunk extends MeshInstance {
|
|
constructor (origin, pos, size = 16) {
|
|
super(null, [origin[0] + pos[0] * size, origin[1] + pos[1] * size, origin[2] + pos[2] * size])
|
|
this.relativePos = pos
|
|
this.size = size
|
|
this.mesh = null
|
|
|
|
// Voxel data
|
|
this.data = {}
|
|
|
|
// Set to true when this chunk mesh requires to be recreated
|
|
this.dirty = true
|
|
|
|
// Set to true when the generation has been finished
|
|
this.generated = false
|
|
}
|
|
|
|
getVoxel (x, y, z) {
|
|
if (this.parent) {
|
|
let neighbor
|
|
if (x < 0) {
|
|
neighbor = this.parent.getChunk(this.relativePos[0] - 1, this.relativePos[1], this.relativePos[2])
|
|
if (neighbor) {
|
|
return neighbor.getVoxel(this.size + x, y, z)
|
|
}
|
|
return AIR_VOXEL
|
|
} else if (x >= this.size) {
|
|
neighbor = this.parent.getChunk(this.relativePos[0] + 1, this.relativePos[1], this.relativePos[2])
|
|
if (neighbor) {
|
|
return neighbor.getVoxel(x - this.size, y, z)
|
|
}
|
|
return AIR_VOXEL
|
|
} else if (y < 0) {
|
|
neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1] - 1, this.relativePos[2])
|
|
if (neighbor) {
|
|
return neighbor.getVoxel(x, this.size + y, z)
|
|
}
|
|
return AIR_VOXEL
|
|
} else if (y >= this.size) {
|
|
neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1] + 1, this.relativePos[2])
|
|
if (neighbor) {
|
|
return neighbor.getVoxel(x, y - this.size, z)
|
|
}
|
|
return AIR_VOXEL
|
|
} else if (z < 0) {
|
|
neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] - 1)
|
|
if (neighbor) {
|
|
return neighbor.getVoxel(x, y, this.size + z)
|
|
}
|
|
return AIR_VOXEL
|
|
} else if (z >= this.size) {
|
|
neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] + 1)
|
|
if (neighbor) {
|
|
return neighbor.getVoxel(x, y, z - this.size)
|
|
}
|
|
return AIR_VOXEL
|
|
}
|
|
}
|
|
|
|
return this.data[x][z][y]
|
|
}
|
|
|
|
getVoxelv (v) {
|
|
return this.getVoxel(v[0], v[1], v[2])
|
|
}
|
|
|
|
generate (generator) {
|
|
this.data = {}
|
|
for (let x = 0; x < this.size; x++) {
|
|
if (!this.data[x]) this.data[x] = {}
|
|
for (let z = 0; z < this.size; z++) {
|
|
if (!this.data[x][z]) this.data[x][z] = {}
|
|
let columnHeight = generator.getHeight(x + this.pos[0], z + this.pos[2])
|
|
for (let y = 0; y < this.size; y++) {
|
|
let solid = this.pos[1] + y < columnHeight
|
|
this.data[x][z][y] = solid ? GROUND_VOXEL : AIR_VOXEL
|
|
}
|
|
}
|
|
}
|
|
|
|
this.generated = true
|
|
|
|
// Make sure the parent knows that we have generated everything
|
|
if (this.parent) {
|
|
this.parent.chunksLeft--
|
|
if (this.parent.chunksLeft <= 0) {
|
|
this.parent.generated = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Programmatically generate a voxel face
|
|
// Returns the position, normal and texture coordinates for each vertex in this face
|
|
createFace (points, pos, face) {
|
|
// Add the corresponding offsets for this face to the position
|
|
let corners = [
|
|
addv3(pos, FACE_VERTEX[face][0]), addv3(pos, FACE_VERTEX[face][1]),
|
|
addv3(pos, FACE_VERTEX[face][2]), addv3(pos, FACE_VERTEX[face][3])
|
|
]
|
|
|
|
// Select the normal for this face
|
|
let normal = FACE_VERTEX[face][4]
|
|
|
|
// Return the 6 vertices that make up this face (two triangles)
|
|
// They're named points because this function returns not only vertices,
|
|
// but corresponding texture coordinates and normals at the same time for convenience
|
|
points.push([corners[0], normal, [0.0, 1.0]])
|
|
points.push([corners[1], normal, [0.0, 0.0]])
|
|
points.push([corners[2], normal, [1.0, 0.0]])
|
|
points.push([corners[0], normal, [0.0, 1.0]])
|
|
points.push([corners[2], normal, [1.0, 0.0]])
|
|
points.push([corners[3], normal, [1.0, 1.0]])
|
|
}
|
|
|
|
createMesh (gl) {
|
|
// Makes sure the createMesh function is not called again while it is generating
|
|
this.dirty = false
|
|
|
|
// If there is no generated chunk, we have nothing to base a mesh off of
|
|
if (!this.generated) return false
|
|
|
|
// If there already exists a mesh, dispose of it
|
|
if (this.mesh) {
|
|
this.mesh.dispose(gl)
|
|
}
|
|
|
|
// Array of vertices with texture positions and normals
|
|
let points = []
|
|
|
|
// Generate face quads for each voxel in the chunk
|
|
for (let x = 0; x < this.size; x++) {
|
|
for (let y = 0; y < this.size; y++) {
|
|
for (let z = 0; z < this.size; z++) {
|
|
let cellPos = [x, y, z]
|
|
if (!this.getVoxel(x, y, z).solid) continue
|
|
|
|
if (!this.getVoxel(x, y - 1, z).solid) {
|
|
this.createFace(points, cellPos, FACE_BOTTOM)
|
|
}
|
|
|
|
if (!this.getVoxel(x, y + 1, z).solid) {
|
|
this.createFace(points, cellPos, FACE_TOP)
|
|
}
|
|
|
|
if (!this.getVoxel(x - 1, y, z).solid) {
|
|
this.createFace(points, cellPos, FACE_LEFT)
|
|
}
|
|
|
|
if (!this.getVoxel(x + 1, y, z).solid) {
|
|
this.createFace(points, cellPos, FACE_RIGHT)
|
|
}
|
|
|
|
if (!this.getVoxel(x, y, z + 1).solid) {
|
|
this.createFace(points, cellPos, FACE_FRONT)
|
|
}
|
|
|
|
if (!this.getVoxel(x, y, z - 1).solid) {
|
|
this.createFace(points, cellPos, FACE_BACK)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Do not create a mesh when there are no faces in this chunk
|
|
if (points.length === 0) {
|
|
return false
|
|
}
|
|
|
|
// Flatten the points array to three separate arrays
|
|
let vertices = []
|
|
let normals = []
|
|
let uvs = []
|
|
for (let i in points) {
|
|
let vert = points[i]
|
|
vertices.push(vert[0][0])
|
|
vertices.push(vert[0][1])
|
|
vertices.push(vert[0][2])
|
|
normals.push(vert[1][0])
|
|
normals.push(vert[1][1])
|
|
normals.push(vert[1][2])
|
|
uvs.push(vert[2][0])
|
|
uvs.push(vert[2][1])
|
|
}
|
|
|
|
// Create a new mesh without an element array buffer (TODO maybe?)
|
|
this.mesh = Mesh.construct(gl, vertices, null, uvs, normals)
|
|
|
|
if (this.material) this.mesh.material = this.material
|
|
|
|
return true
|
|
}
|
|
|
|
update (gl, dt) {
|
|
if (this.dirty) return this.createMesh(gl)
|
|
return false
|
|
}
|
|
|
|
destroy (gl) {
|
|
this.generated = false
|
|
this.mesh && this.mesh.dispose(gl)
|
|
this.data = {}
|
|
}
|
|
}
|
|
|
|
class VoxelMapBlock extends Node {
|
|
constructor (pos, chunkSize = 16, size = 8) {
|
|
super(pos)
|
|
this.chunkSize = chunkSize
|
|
this.size = size
|
|
this.chunks = {}
|
|
this.generated = false
|
|
this.chunksLeft = size * size * size
|
|
}
|
|
|
|
getChunk (x, y, z) {
|
|
if (x < 0 || x >= this.size || y < 0 || y >= this.size || z < 0 || z >= this.size) return null
|
|
return this.chunks[x][z][y]
|
|
}
|
|
|
|
getChunkv (v) {
|
|
return this.getChunk(v[0], v[1], v[2])
|
|
}
|
|
|
|
generate (generator) {
|
|
this.chunks = {}
|
|
for (let x = 0; x < this.size; x++) {
|
|
if (!this.chunks[x]) this.chunks[x] = {}
|
|
for (let z = 0; z < this.size; z++) {
|
|
if (!this.chunks[x][z]) this.chunks[x][z] = {}
|
|
for (let y = 0; y < this.size; y++) {
|
|
if (this.chunks[x][z][y]) continue
|
|
let chunk = new VoxelChunk(this.pos, [x, y, z], this.chunkSize)
|
|
this.chunks[x][z][y] = chunk
|
|
this.addChild(chunk)
|
|
generator.pushChunk(chunk)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
destroy (gl) {
|
|
this.generated = false
|
|
for (let i in this.children) {
|
|
let ch = this.children[i]
|
|
if (!(ch instanceof VoxelChunk)) continue
|
|
ch.destroy(gl)
|
|
}
|
|
this.children = []
|
|
this.chunks = {}
|
|
}
|
|
|
|
update (gl, dt) {
|
|
if (!this.generated) return
|
|
for (let i in this.children) {
|
|
let ch = this.children[i]
|
|
if (!(ch instanceof VoxelChunk)) continue
|
|
if (ch.update(gl, dt)) break
|
|
}
|
|
}
|
|
}
|
|
|
|
class VoxelGenerator {
|
|
constructor (noise, material, groundHeight = 64) {
|
|
this.material = material
|
|
this.noise = noise
|
|
this.groundHeight = groundHeight
|
|
this.generateQueue = []
|
|
}
|
|
|
|
// Push a chunk into the generation queue
|
|
pushChunk (chunk) {
|
|
this.generateQueue.push(chunk)
|
|
}
|
|
|
|
// Get chunk height
|
|
getHeight (x, z) {
|
|
return this.noise.getHeight(x, z) + this.groundHeight
|
|
}
|
|
|
|
// Generate chunks in the queue
|
|
update (dt) {
|
|
for (let i in this.generateQueue) {
|
|
let chunk = this.generateQueue[i]
|
|
if (chunk.generated) continue
|
|
|
|
chunk.material = this.material
|
|
chunk.generate(this)
|
|
}
|
|
}
|
|
}
|
|
|
|
export { VoxelGenerator, Voxel, VoxelChunk, VoxelMapBlock }
|