import { Mesh } from '../mesh' import { Node, MeshInstance } from '../components' import { addv3, mulv3 } 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 VoxelWorld extends Node { constructor (generator, renderDistance = 2, worldSize = 64, blockSize = 8, chunkSize = 16) { super([0.0, 0.0, 0.0]) this.generator = generator this.renderDistance = renderDistance this.generator = generator this.worldSize = worldSize this.blockSize = blockSize this.chunkSize = chunkSize this.blocks = {} this.mapblockqueue = [] } queueMapblock (pos) { this.mapblockqueue.push(pos) } getBlock (x, y, z) { if (!this.blocks[x] || !this.blocks[x][z] || !this.blocks[x][z][y]) return null return this.blocks[x][z][y] } update (gl, cam, dt) { // Generate queued mapblocks before adding new ones to queue if (this.mapblockqueue.length) { let leftover = [] let generated = false for (let i in this.mapblockqueue) { let pos = this.mapblockqueue[i] let check = this.getBlock(pos[0], pos[1], pos[2]) if (generated || check) { leftover.push(pos) continue } let absPos = mulv3(pos, this.blockSize * (this.chunkSize / 2)) let block = new VoxelMapBlock(absPos, this.chunkSize, this.blockSize) block.generate(this.generator) if (!this.blocks[pos[0]]) this.blocks[pos[0]] = {} if (!this.blocks[pos[0]][pos[2]]) this.blocks[pos[0]][pos[2]] = {} if (!this.blocks[pos[0]][pos[2]][pos[1]]) this.blocks[pos[0]][pos[2]][pos[1]] = {} this.blocks[pos[0]][pos[2]][pos[1]] = block this.addChild(block) generated = true } this.mapblockqueue = leftover return } // Calculate a fixed grid let total = this.blockSize * this.chunkSize let slgrid = [Math.floor(cam.pos[0] / total), Math.floor(cam.pos[1] / total), Math.floor(cam.pos[2] / total)] for (let x = slgrid[0] - this.renderDistance / 2; x < slgrid[0] + this.renderDistance / 2; x++) { for (let z = slgrid[1] - this.renderDistance / 2; z < slgrid[1] + this.renderDistance / 2; z++) { for (let y = slgrid[2] - this.renderDistance / 2; y < slgrid[2] + this.renderDistance / 2; y++) { if (this.getBlock(x, y, z)) continue this.queueMapblock([x, y, z]) } } } for (let i in this.children) { this.children[i].update(gl, dt) } } } 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, VoxelWorld }