import { Mesh } from '../mesh' import { MeshInstance } from '../components' import { vec3 } from 'gl-matrix' import { addv3, subv3, dim3to1 } from '../utility' import VoxelData from './voxeldata' import VoxelTexture from './voxeltexture' 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, 1.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 1.0], [-1.0, 0.0, 0.0] ], // Right [ [1.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [1.0, 0.0, 0.0] ], // Front [ [0.0, 1.0, 1.0], [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 0.0, -1.0] ], // Back [ [1.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.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 const AIR = VoxelData.register('air', { solid: false }) const GRASS = VoxelData.register('grass', { solid: true, tiles: [0, 2, 1, 1, 1, 1] }) const DIRT = VoxelData.register('dirt', { solid: true, tiles: [0] }) const STONE = VoxelData.register('stone', { solid: true, tiles: [3] }) class VoxelChunk extends MeshInstance { constructor (world, pos, size = 16) { super(null, [pos[0] * size, pos[1] * size, pos[2] * size]) this.world = world 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 // If the chunk is outside of the view, make it inactive this.active = true this.bounds = [ vec3.transformMat4([], [0.0, 0.0, 0.0], this.transform), vec3.transformMat4([], [this.size, this.size, this.size], this.transform) ] } getVoxel (x, y, z) { if (this.world) { let neighbor if (x < 0) { neighbor = this.world.getChunk(this.relativePos[0] - 1, this.relativePos[1], this.relativePos[2]) if (neighbor) { return neighbor.getVoxel(this.size + x, y, z) } return AIR } else if (x >= this.size) { neighbor = this.world.getChunk(this.relativePos[0] + 1, this.relativePos[1], this.relativePos[2]) if (neighbor) { return neighbor.getVoxel(x - this.size, y, z) } return AIR } else if (y < 0) { neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1] - 1, this.relativePos[2]) if (neighbor) { return neighbor.getVoxel(x, this.size + y, z) } return AIR } else if (y >= this.size) { neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1] + 1, this.relativePos[2]) if (neighbor) { return neighbor.getVoxel(x, y - this.size, z) } return AIR } else if (z < 0) { neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] - 1) if (neighbor) { return neighbor.getVoxel(x, y, this.size + z) } return AIR } else if (z >= this.size) { neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] + 1) if (neighbor) { return neighbor.getVoxel(x, y, z - this.size) } return AIR } } return this.data[dim3to1(x, y, z, this.size)] || AIR } getVoxelv (v) { return this.getVoxel(v[0], v[1], v[2]) } generate (generator) { this.material = VoxelTexture.material for (let x = 0; x < this.size; x++) { for (let z = 0; z < this.size; z++) { const columnHeight = Math.floor(generator.getHeight(x + this.pos[0], z + this.pos[2])) for (let y = 0; y < this.size; y++) { let voxel = AIR if (this.pos[1] + y === columnHeight) voxel = GRASS else if (this.pos[1] + y < columnHeight - 3) voxel = STONE else if (this.pos[1] + y < columnHeight) voxel = DIRT this.data[dim3to1(x, y, z, this.size)] = voxel } } } this.generated = true return true } // Programmatically generate a voxel face // Returns the position, normal and texture coordinates for each vertex in this face createFace (indices, points, pos, vId, face) { // Add the corresponding offsets for this face to the position const 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 const normal = FACE_VERTEX[face][4] const uvs = VoxelData.textureIndex(vId, face) // Create the 4 vertices that make up this face // 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, uvs[1]]) points.push([corners[1], normal, uvs[0]]) points.push([corners[2], normal, uvs[2]]) points.push([corners[3], normal, uvs[3]]) // Create the face const inx = points.length - 4 indices.push(inx) indices.push(inx + 1) indices.push(inx + 2) indices.push(inx) indices.push(inx + 2) indices.push(inx + 3) } 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 const points = [] const indices = [] // 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++) { const cellPos = [x, y, z] const vid = this.getVoxel(x, y, z) if (!VoxelData.isSolid(vid)) continue if (!VoxelData.isSolid(this.getVoxel(x, y - 1, z))) { this.createFace(indices, points, cellPos, vid, FACE_BOTTOM) } if (!VoxelData.isSolid(this.getVoxel(x, y + 1, z))) { this.createFace(indices, points, cellPos, vid, FACE_TOP) } if (!VoxelData.isSolid(this.getVoxel(x - 1, y, z))) { this.createFace(indices, points, cellPos, vid, FACE_LEFT) } if (!VoxelData.isSolid(this.getVoxel(x + 1, y, z))) { this.createFace(indices, points, cellPos, vid, FACE_RIGHT) } if (!VoxelData.isSolid(this.getVoxel(x, y, z + 1))) { this.createFace(indices, points, cellPos, vid, FACE_FRONT) } if (!VoxelData.isSolid(this.getVoxel(x, y, z - 1))) { this.createFace(indices, points, cellPos, vid, 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 const vertices = [] const normals = [] const uvs = [] for (const i in points) { const 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 with an element array buffer this.mesh = Mesh.construct(gl, vertices, indices, uvs, normals) if (this.material) this.mesh.material = this.material return true } update (gl, dt, camera) { if (this.bounds.length) this.active = camera.frustum.containsBox(this.bounds[0], this.bounds[1]) if (!this.active) return false if (!this.generated) return this.generate(this.world.generator) if (this.dirty) return this.createMesh(gl) return false } destroy (gl) { this.generated = false this.mesh && this.mesh.dispose(gl) this.data = {} } draw (gl, shader, camera) { if (this.active) super.draw(gl, shader) } } class VoxelWorld { constructor (generator, origin, chunkSize = 16, renderDistance = 5) { this.generator = generator this.origin = origin this.chunkSize = chunkSize this.renderDistance = renderDistance this.chunks = [] } getChunk (x, y, z) { for (const i in this.chunks) { const c = this.chunks[i] if (c.relativePos[0] === x && c.relativePos[1] === y && c.relativePos[2] === z) return c } return null } getChunkv (v) { return this.getChunk(v[0], v[1], v[2]) } destroy (gl) { for (const i in this.chunks) { const ch = this.chunks[i] if (!(ch instanceof VoxelChunk)) continue ch.destroy(gl) } this.chunks = [] } update (gl, dt, camera) { const slgrid = [ Math.floor(camera.pos[0] / this.chunkSize), Math.floor(camera.pos[1] / this.chunkSize), Math.floor(camera.pos[2] / this.chunkSize) ] for (let x = slgrid[0] - this.renderDistance; x < slgrid[0] + this.renderDistance; x++) { for (let z = slgrid[2] - this.renderDistance; z < slgrid[2] + this.renderDistance; z++) { for (let y = slgrid[1] + this.renderDistance / 2; y > slgrid[1] - this.renderDistance / 2; y--) { if (this.getChunk(x, y, z)) continue const chunk = new VoxelChunk(this, [x, y, z], this.chunkSize) this.chunks.push(chunk) } } } const resp = [] for (const i in this.chunks) { const chunk = this.chunks[i] const dist = vec3.length(subv3(chunk.relativePos, slgrid)) if (dist < this.renderDistance) resp.push(chunk) } this.chunks = resp for (const i in this.chunks) { const ch = this.chunks[i] if (ch.update(gl, dt, camera)) break } } draw (gl, shader) { for (const i in this.chunks) { this.chunks[i].draw(gl, shader) } } } class VoxelGenerator { constructor (noise, groundHeight = 64) { this.noise = noise this.groundHeight = groundHeight } // Get chunk height getHeight (x, z) { return this.noise.getHeight(x / 100, z / 100) * this.noise.amplitude + this.groundHeight } } export { VoxelGenerator, VoxelChunk, VoxelWorld }