3dexperiments/src/engine/voxel/index.js

378 lines
10 KiB
JavaScript
Raw Normal View History

2020-01-05 15:13:55 +00:00
import { Mesh } from '../mesh'
2020-04-09 16:37:35 +00:00
import { MeshInstance } from '../components'
2020-04-09 12:57:00 +00:00
import { vec3 } from 'gl-matrix'
2020-04-09 16:37:35 +00:00
import { addv3, subv3, dim3to1 } from '../utility'
2020-04-09 12:57:00 +00:00
import VoxelData from './voxeldata'
2020-04-09 16:37:35 +00:00
import VoxelTexture from './voxeltexture'
2020-01-05 15:13:55 +00:00
const FACE_VERTEX = [
// Bottom
[
[1.0, 0.0, 0.0],
[1.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
2020-01-05 19:11:05 +00:00
[0.0, 0.0, 0.0],
[0.0, -1.0, 0.0]
2020-01-05 15:13:55 +00:00
],
// Top
[
[0.0, 1.0, 0.0],
[0.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
2020-01-05 19:11:05 +00:00
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0]
2020-01-05 15:13:55 +00:00
],
// Left
[
[0.0, 1.0, 0.0],
2020-01-05 19:11:05 +00:00
[0.0, 0.0, 0.0],
2020-04-09 16:37:35 +00:00
[0.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
2020-01-05 19:11:05 +00:00
[-1.0, 0.0, 0.0]
2020-01-05 15:13:55 +00:00
],
// Right
[
[1.0, 1.0, 1.0],
2020-01-05 19:11:05 +00:00
[1.0, 0.0, 1.0],
2020-04-09 16:37:35 +00:00
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
2020-01-05 19:11:05 +00:00
[1.0, 0.0, 0.0]
2020-01-05 15:13:55 +00:00
],
// Front
[
[0.0, 1.0, 1.0],
2020-01-05 19:11:05 +00:00
[0.0, 0.0, 1.0],
2020-04-09 16:37:35 +00:00
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
2020-04-09 12:57:00 +00:00
[0.0, 0.0, -1.0]
2020-01-05 15:13:55 +00:00
],
// Back
[
[1.0, 1.0, 0.0],
2020-01-05 19:11:05 +00:00
[1.0, 0.0, 0.0],
2020-04-09 16:37:35 +00:00
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
2020-04-09 12:57:00 +00:00
[0.0, 0.0, 1.0]
2020-01-05 15:13:55 +00:00
]
]
const FACE_BOTTOM = 0
const FACE_TOP = 1
const FACE_LEFT = 2
const FACE_RIGHT = 3
const FACE_FRONT = 4
const FACE_BACK = 5
2020-04-09 16:37:35 +00:00
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] })
2020-01-05 15:13:55 +00:00
class VoxelChunk extends MeshInstance {
2020-04-09 12:57:00 +00:00
constructor (world, pos, size = 16) {
super(null, [pos[0] * size, pos[1] * size, pos[2] * size])
this.world = world
2020-01-05 21:20:32 +00:00
this.relativePos = pos
2020-01-05 15:13:55 +00:00
this.size = size
this.mesh = null
2020-01-05 19:11:05 +00:00
// Voxel data
2020-04-09 12:57:00 +00:00
this.data = []
2020-01-05 19:11:05 +00:00
// Set to true when this chunk mesh requires to be recreated
2020-01-05 15:13:55 +00:00
this.dirty = true
2020-01-05 19:11:05 +00:00
// Set to true when the generation has been finished
this.generated = false
2020-04-09 12:57:00 +00:00
// If the chunk is outside of the view, make it inactive
this.active = true
2020-04-09 16:37:35 +00:00
this.bounds = [
vec3.transformMat4([], [0.0, 0.0, 0.0], this.transform),
vec3.transformMat4([], [this.size, this.size, this.size], this.transform)
]
2020-01-05 15:13:55 +00:00
}
getVoxel (x, y, z) {
2020-04-09 12:57:00 +00:00
if (this.world) {
2020-01-05 21:20:32 +00:00
let neighbor
if (x < 0) {
2020-04-09 12:57:00 +00:00
neighbor = this.world.getChunk(this.relativePos[0] - 1, this.relativePos[1], this.relativePos[2])
2020-01-05 21:20:32 +00:00
if (neighbor) {
return neighbor.getVoxel(this.size + x, y, z)
}
2020-04-09 16:37:35 +00:00
return AIR
2020-01-05 21:20:32 +00:00
} else if (x >= this.size) {
2020-04-09 12:57:00 +00:00
neighbor = this.world.getChunk(this.relativePos[0] + 1, this.relativePos[1], this.relativePos[2])
2020-01-05 21:20:32 +00:00
if (neighbor) {
return neighbor.getVoxel(x - this.size, y, z)
}
2020-04-09 16:37:35 +00:00
return AIR
2020-01-05 21:20:32 +00:00
} else if (y < 0) {
2020-04-09 12:57:00 +00:00
neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1] - 1, this.relativePos[2])
2020-01-05 21:20:32 +00:00
if (neighbor) {
return neighbor.getVoxel(x, this.size + y, z)
}
2020-04-09 16:37:35 +00:00
return AIR
2020-01-05 21:20:32 +00:00
} else if (y >= this.size) {
2020-04-09 12:57:00 +00:00
neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1] + 1, this.relativePos[2])
2020-01-05 21:20:32 +00:00
if (neighbor) {
return neighbor.getVoxel(x, y - this.size, z)
}
2020-04-09 16:37:35 +00:00
return AIR
2020-01-05 21:20:32 +00:00
} else if (z < 0) {
2020-04-09 12:57:00 +00:00
neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] - 1)
2020-01-05 21:20:32 +00:00
if (neighbor) {
return neighbor.getVoxel(x, y, this.size + z)
}
2020-04-09 16:37:35 +00:00
return AIR
2020-01-05 21:20:32 +00:00
} else if (z >= this.size) {
2020-04-09 12:57:00 +00:00
neighbor = this.world.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] + 1)
2020-01-05 21:20:32 +00:00
if (neighbor) {
return neighbor.getVoxel(x, y, z - this.size)
}
2020-04-09 16:37:35 +00:00
return AIR
2020-01-05 21:20:32 +00:00
}
}
2020-04-09 16:37:35 +00:00
return this.data[dim3to1(x, y, z, this.size)] || AIR
2020-01-05 21:20:32 +00:00
}
getVoxelv (v) {
return this.getVoxel(v[0], v[1], v[2])
2020-01-05 15:13:55 +00:00
}
2020-01-05 19:11:05 +00:00
generate (generator) {
2020-04-09 16:37:35 +00:00
this.material = VoxelTexture.material
2020-01-05 15:13:55 +00:00
for (let x = 0; x < this.size; x++) {
2020-01-05 21:20:32 +00:00
for (let z = 0; z < this.size; z++) {
2020-04-09 16:37:35 +00:00
const columnHeight = Math.floor(generator.getHeight(x + this.pos[0], z + this.pos[2]))
2020-01-05 21:20:32 +00:00
for (let y = 0; y < this.size; y++) {
2020-04-09 16:37:35 +00:00
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
2020-01-05 15:13:55 +00:00
}
}
}
2020-01-05 21:20:32 +00:00
2020-01-05 19:11:05 +00:00
this.generated = true
2020-04-09 12:57:00 +00:00
return true
2020-01-05 15:13:55 +00:00
}
// Programmatically generate a voxel face
// Returns the position, normal and texture coordinates for each vertex in this face
2020-04-09 16:37:35 +00:00
createFace (indices, points, pos, vId, face) {
2020-01-05 19:11:05 +00:00
// Add the corresponding offsets for this face to the position
2020-03-31 08:15:29 +00:00
const corners = [
2020-01-05 15:13:55 +00:00
addv3(pos, FACE_VERTEX[face][0]), addv3(pos, FACE_VERTEX[face][1]),
addv3(pos, FACE_VERTEX[face][2]), addv3(pos, FACE_VERTEX[face][3])
]
2020-01-05 19:11:05 +00:00
// Select the normal for this face
2020-03-31 08:15:29 +00:00
const normal = FACE_VERTEX[face][4]
2020-04-09 16:37:35 +00:00
const uvs = VoxelData.textureIndex(vId, face)
2020-01-05 19:11:05 +00:00
2020-04-09 12:57:00 +00:00
// Create the 4 vertices that make up this face
2020-01-05 19:11:05 +00:00
// They're named points because this function returns not only vertices,
// but corresponding texture coordinates and normals at the same time for convenience
2020-04-09 16:37:35 +00:00
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]])
2020-04-09 12:57:00 +00:00
// 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)
2020-01-05 15:13:55 +00:00
}
createMesh (gl) {
2020-01-05 19:11:05 +00:00
// Makes sure the createMesh function is not called again while it is generating
2020-01-05 15:13:55 +00:00
this.dirty = false
2020-01-05 19:11:05 +00:00
// If there is no generated chunk, we have nothing to base a mesh off of
2020-01-05 21:20:32 +00:00
if (!this.generated) return false
2020-01-05 19:11:05 +00:00
// If there already exists a mesh, dispose of it
2020-01-05 15:13:55 +00:00
if (this.mesh) {
this.mesh.dispose(gl)
}
2020-01-05 19:11:05 +00:00
// Array of vertices with texture positions and normals
2020-03-31 08:15:29 +00:00
const points = []
2020-04-09 12:57:00 +00:00
const indices = []
2020-01-05 15:13:55 +00:00
2020-01-05 19:11:05 +00:00
// Generate face quads for each voxel in the chunk
2020-01-05 15:13:55 +00:00
for (let x = 0; x < this.size; x++) {
for (let y = 0; y < this.size; y++) {
for (let z = 0; z < this.size; z++) {
2020-03-31 08:15:29 +00:00
const cellPos = [x, y, z]
2020-04-09 16:37:35 +00:00
const vid = this.getVoxel(x, y, z)
if (!VoxelData.isSolid(vid)) continue
2020-01-05 15:13:55 +00:00
2020-04-09 12:57:00 +00:00
if (!VoxelData.isSolid(this.getVoxel(x, y - 1, z))) {
2020-04-09 16:37:35 +00:00
this.createFace(indices, points, cellPos, vid, FACE_BOTTOM)
2020-01-05 15:13:55 +00:00
}
2020-04-09 12:57:00 +00:00
if (!VoxelData.isSolid(this.getVoxel(x, y + 1, z))) {
2020-04-09 16:37:35 +00:00
this.createFace(indices, points, cellPos, vid, FACE_TOP)
2020-01-05 15:13:55 +00:00
}
2020-04-09 12:57:00 +00:00
if (!VoxelData.isSolid(this.getVoxel(x - 1, y, z))) {
2020-04-09 16:37:35 +00:00
this.createFace(indices, points, cellPos, vid, FACE_LEFT)
2020-01-05 15:13:55 +00:00
}
2020-04-09 12:57:00 +00:00
if (!VoxelData.isSolid(this.getVoxel(x + 1, y, z))) {
2020-04-09 16:37:35 +00:00
this.createFace(indices, points, cellPos, vid, FACE_RIGHT)
2020-01-05 15:13:55 +00:00
}
2020-04-09 12:57:00 +00:00
if (!VoxelData.isSolid(this.getVoxel(x, y, z + 1))) {
2020-04-09 16:37:35 +00:00
this.createFace(indices, points, cellPos, vid, FACE_FRONT)
2020-01-05 15:13:55 +00:00
}
2020-04-09 12:57:00 +00:00
if (!VoxelData.isSolid(this.getVoxel(x, y, z - 1))) {
2020-04-09 16:37:35 +00:00
this.createFace(indices, points, cellPos, vid, FACE_BACK)
2020-01-05 15:13:55 +00:00
}
}
}
}
2020-01-05 19:11:05 +00:00
// Do not create a mesh when there are no faces in this chunk
2020-01-05 15:13:55 +00:00
if (points.length === 0) {
2020-01-05 21:20:32 +00:00
return false
2020-01-05 15:13:55 +00:00
}
2020-01-05 19:11:05 +00:00
// Flatten the points array to three separate arrays
2020-03-31 08:15:29 +00:00
const vertices = []
const normals = []
const uvs = []
for (const i in points) {
const vert = points[i]
2020-01-05 15:13:55 +00:00
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])
}
2020-04-09 12:57:00 +00:00
// Create a new mesh with an element array buffer
this.mesh = Mesh.construct(gl, vertices, indices, uvs, normals)
2020-01-05 15:13:55 +00:00
2020-01-05 19:11:05 +00:00
if (this.material) this.mesh.material = this.material
2020-01-05 21:20:32 +00:00
return true
2020-01-05 19:11:05 +00:00
}
2020-01-05 15:13:55 +00:00
2020-04-09 12:57:00 +00:00
update (gl, dt, camera) {
2020-04-09 16:37:35 +00:00
if (this.bounds.length) this.active = camera.frustum.containsBox(this.bounds[0], this.bounds[1])
2020-04-09 12:57:00 +00:00
if (!this.active) return false
if (!this.generated) return this.generate(this.world.generator)
2020-01-05 19:11:05 +00:00
if (this.dirty) return this.createMesh(gl)
2020-01-05 21:20:32 +00:00
return false
}
destroy (gl) {
this.generated = false
this.mesh && this.mesh.dispose(gl)
this.data = {}
}
2020-04-09 12:57:00 +00:00
draw (gl, shader, camera) {
if (this.active) super.draw(gl, shader)
}
2020-01-05 21:20:32 +00:00
}
2020-04-09 12:57:00 +00:00
class VoxelWorld {
constructor (generator, origin, chunkSize = 16, renderDistance = 5) {
this.generator = generator
this.origin = origin
2020-01-05 21:20:32 +00:00
this.chunkSize = chunkSize
2020-04-09 12:57:00 +00:00
this.renderDistance = renderDistance
this.chunks = []
2020-01-05 21:20:32 +00:00
}
getChunk (x, y, z) {
2020-04-09 12:57:00 +00:00
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
2020-01-05 21:20:32 +00:00
}
getChunkv (v) {
return this.getChunk(v[0], v[1], v[2])
}
destroy (gl) {
2020-04-09 12:57:00 +00:00
for (const i in this.chunks) {
const ch = this.chunks[i]
2020-01-05 21:20:32 +00:00
if (!(ch instanceof VoxelChunk)) continue
ch.destroy(gl)
}
2020-04-09 12:57:00 +00:00
this.chunks = []
2020-01-06 13:27:18 +00:00
}
2020-04-09 12:57:00 +00:00
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)
]
2020-01-06 13:27:18 +00:00
2020-04-09 12:57:00 +00:00
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)
2020-01-06 13:27:18 +00:00
}
}
}
2020-04-09 12:57:00 +00:00
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
2020-01-06 13:27:18 +00:00
2020-04-09 12:57:00 +00:00
for (const i in this.chunks) {
const ch = this.chunks[i]
if (ch.update(gl, dt, camera)) break
2020-01-06 13:27:18 +00:00
}
2020-04-09 12:57:00 +00:00
}
2020-01-06 13:27:18 +00:00
2020-04-09 12:57:00 +00:00
draw (gl, shader) {
for (const i in this.chunks) {
this.chunks[i].draw(gl, shader)
2020-01-06 13:27:18 +00:00
}
}
}
2020-01-05 21:20:32 +00:00
class VoxelGenerator {
2020-04-09 16:37:35 +00:00
constructor (noise, groundHeight = 64) {
2020-01-05 21:20:32 +00:00
this.noise = noise
this.groundHeight = groundHeight
}
// Get chunk height
getHeight (x, z) {
2020-04-09 12:57:00 +00:00
return this.noise.getHeight(x / 100, z / 100) * this.noise.amplitude + this.groundHeight
2020-01-05 15:13:55 +00:00
}
}
2020-04-09 12:57:00 +00:00
export { VoxelGenerator, VoxelChunk, VoxelWorld }