Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions src/render/ChunkBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,57 @@ export class ChunkBuilder {
console.error(`Error rendering block ${blockName}`, e)
}
}
}

if (!chunkPositions) {
this.chunks.forEach(x => x.forEach(y => y.forEach(chunk => {
chunk.mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true, blockPos: true })
chunk.transparentMesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true, blockPos: true })
})))
} else {
chunkPositions.forEach(chunkPos => {
const chunk = this.getChunk(chunkPos)
chunk.mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true, blockPos: true })
chunk.transparentMesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true, blockPos: true })
})
public getTransparentMeshes(cameraPos: vec3): Mesh[] {
// Flatten all existing chunks into a list with their computed world-space center.
const chunkList: { chunk: { mesh: Mesh, transparentMesh: Mesh }, center: vec3 }[] = []
for (let i = 0; i < this.chunks.length; i++) {
if (!this.chunks[i]) continue
for (let j = 0; j < this.chunks[i].length; j++) {
if (!this.chunks[i][j]) continue
for (let k = 0; k < this.chunks[i][j].length; k++) {
const chunk = this.chunks[i][j][k]
if (!chunk) continue

// Inverse mapping of getChunk() function
const chunkPosX = (i % 2 === 0) ? i / 2 : -((i - 1) / 2)
const chunkPosY = (j % 2 === 0) ? j / 2 : -((j - 1) / 2)
const chunkPosZ = (k % 2 === 0) ? k / 2 : -((k - 1) / 2)
// Compute the center of the chunk in world space.
const center: vec3 = [
chunkPosX * this.chunkSize[0] + this.chunkSize[0] / 2,
chunkPosY * this.chunkSize[1] + this.chunkSize[1] / 2,
chunkPosZ * this.chunkSize[2] + this.chunkSize[2] / 2
]
chunkList.push({ chunk, center })
}
}
}

// Sort the chunk list: farthest from the camera comes first
chunkList.sort((a, b) => {
const dxA = a.center[0] - cameraPos[0]
const dyA = a.center[1] - cameraPos[1]
const dzA = a.center[2] - cameraPos[2]
const distA = dxA * dxA + dyA * dyA + dzA * dzA

const dxB = b.center[0] - cameraPos[0]
const dyB = b.center[1] - cameraPos[1]
const dzB = b.center[2] - cameraPos[2]
const distB = dxB * dxB + dyB * dyB + dzB * dzB

return distB - distA // sort descending (farthest first)
})

// Return the transparent meshes from non-empty chunks in sorted order.
return chunkList
.filter(item => !item.chunk.transparentMesh.isEmpty())
.map(item => item.chunk.transparentMesh)
}

public getMeshes(): Mesh[] {
const chunks = this.chunks.flatMap(x => x.flatMap(y => y.flatMap(chunk => chunk ?? [])))
return chunks.flatMap(chunk => chunk.mesh.isEmpty() ? [] : chunk.mesh).concat(chunks.flatMap(chunk => chunk.transparentMesh.isEmpty() ? [] : chunk.transparentMesh))
public getNonTransparentMeshes(): Mesh[] {
return this.chunks.flatMap(x => x.flatMap(y => y.flatMap(chunk => chunk.mesh.isEmpty() ? [] : chunk.mesh)))
}

private needsCull(block: PlacedBlock, dir: Direction) {
Expand Down
1 change: 0 additions & 1 deletion src/render/ItemRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export class ItemRenderer extends Renderer {
mesh.merge(specialMesh)
mesh.transform(model.getDisplayTransform('gui'))
mesh.computeNormals()
mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true })
return mesh
}

Expand Down
93 changes: 83 additions & 10 deletions src/render/Mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,41 @@ export class Mesh {
public linePosBuffer: WebGLBuffer | undefined
public lineColorBuffer: WebGLBuffer | undefined

public posBufferDirty = true
public colorBufferDirty = true
public textureBufferDirty = true
public normalBufferDirty = true
public blockPosBufferDirty = true
public indexBufferDirty = true

public linePosBufferDirty = true
public lineColorBufferDirty = true

constructor(
public quads: Quad[] = [],
public lines: Line[] = []
) {}

public setDirty(options: { quads?: boolean, lines?: boolean }) {
if (options.quads !== undefined) {
this.posBufferDirty = options.quads
this.colorBufferDirty = options.quads
this.textureBufferDirty = options.quads
this.normalBufferDirty = options.quads
this.blockPosBufferDirty = options.quads
this.indexBufferDirty = options.quads
}
if (options.lines) {
this.linePosBufferDirty = true
this.lineColorBufferDirty = true
}
}

public clear() {
this.quads = []
this.lines = []

this.setDirty({ quads: true, lines: true })
return this
}

Expand All @@ -46,6 +73,8 @@ export class Mesh {
public merge(other: Mesh) {
this.quads = this.quads.concat(other.quads)
this.lines = this.lines.concat(other.lines)

this.setDirty({ quads: true, lines: true })
return this
}

Expand All @@ -55,6 +84,8 @@ export class Mesh {
Vertex.fromPos(new Vector(x2, y2, z2))
).setColor(color)
this.lines.push(line)

this.setDirty({ lines: true })
return this
}

Expand All @@ -74,13 +105,16 @@ export class Mesh {
this.addLine(x1, y2, z1, x2, y2, z1, color)
this.addLine(x1, y2, z2, x2, y2, z2, color)

this.setDirty({ lines: true })
return this
}

public transform(transformation: mat4) {
for (const quad of this.quads) {
quad.transform(transformation)
}

this.setDirty({ quads: true })
return this
}

Expand All @@ -91,6 +125,26 @@ export class Mesh {
}
}

public split(): Mesh[] {
// Maximum number of quads per mesh, calculated so that:
// (number of vertices) = (quads * 4) and highest index (4*n - 1) remains <= 65535.
// Using the condition (quads * 4 + 3) <= 65536 leads to:
const maxQuads: number = Math.floor((65536 - 3) / 4);

if (this.quads.length * 4 + 3 <= 65536) {
return [this];
}

const meshes: Mesh[] = [];
for (let i: number = 0; i < this.quads.length; i += maxQuads) {
const chunk: Quad[] = this.quads.slice(i, i + maxQuads);
// For lines, we include lines only in the first split mesh.
const lines: Line[] = i === 0 ? this.lines : [];
meshes.push(new Mesh(chunk, lines));
}
return meshes;
}

public rebuild(gl: WebGLRenderingContext, options: { pos?: boolean, color?: boolean, texture?: boolean, normal?: boolean, blockPos?: boolean }) {
const rebuildBuffer = (buffer: WebGLBuffer | undefined, type: number, data: BufferSource): WebGLBuffer | undefined => {
if (!buffer) {
Expand All @@ -116,28 +170,47 @@ export class Mesh {
return rebuildBuffer(buffer, gl.ARRAY_BUFFER, new Float32Array(data))
}

if (options.pos) {
if (options.pos && this.posBufferDirty) {
this.posBuffer = rebuildBufferV(this.quads, this.posBuffer, v => v.pos.components())
this.posBufferDirty = false
}
if (options.pos && this.linePosBufferDirty) {
this.linePosBuffer = rebuildBufferV(this.lines, this.linePosBuffer, v => v.pos.components())
this.linePosBufferDirty = false
}
if (options.color) {
if (options.color && this.colorBufferDirty) {
this.colorBuffer = rebuildBufferV(this.quads, this.colorBuffer, v => v.color)
this.colorBufferDirty = false
}
if (options.color && this.lineColorBufferDirty) {
this.lineColorBuffer = rebuildBufferV(this.lines, this.lineColorBuffer, v => v.color)
this.lineColorBufferDirty = false
}
if (options.texture) {
if (options.texture && this.textureBufferDirty) {
this.textureBuffer = rebuildBufferV(this.quads, this.textureBuffer, v => v.texture)
this.textureBufferDirty = false
}
if (options.normal) {
if (options.normal && this.normalBufferDirty) {
this.normalBuffer = rebuildBufferV(this.quads, this.normalBuffer, v => v.normal?.components())
this.normalBufferDirty = false
}
if (options.blockPos) {
if (options.blockPos && this.blockPosBufferDirty) {
this.blockPosBuffer = rebuildBufferV(this.quads, this.blockPosBuffer, v => v.blockPos?.components())
this.blockPosBufferDirty = false
}
if (this.quads.length === 0) {
if (this.indexBuffer) gl.deleteBuffer(this.indexBuffer)
this.indexBuffer = undefined
} else {
this.indexBuffer = rebuildBuffer(this.indexBuffer, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.quads.flatMap((_, i) => [4*i, 4*i + 1, 4*i + 2, i*4, 4*i + 2, 4*i + 3], true)))

if (this.indexBufferDirty) {
if (this.quads.length === 0) {
if (this.indexBuffer) gl.deleteBuffer(this.indexBuffer)
this.indexBuffer = undefined
this.indexBufferDirty = false
} else {
if (this.quads.length * 4 + 3 > 65536) {
throw new Error('You got ' + this.quads.length * 4 + 3 + ' vertices, which is more than the max index of uint16 (65536)')
}
this.indexBuffer = rebuildBuffer(this.indexBuffer, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.quads.flatMap((_, i) => [4*i, 4*i + 1, 4*i + 2, i*4, 4*i + 2, 4*i + 3], true)))
this.indexBufferDirty = false
}
}

return this
Expand Down
70 changes: 67 additions & 3 deletions src/render/Renderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mat4 } from 'gl-matrix'
import { mat4, vec3 } from 'gl-matrix'
import type { Mesh } from './Mesh.js'
import type { Quad } from './Quad.js'
import { ShaderProgram } from './ShaderProgram.js'

const vsSource = `
Expand Down Expand Up @@ -42,7 +43,7 @@ export class Renderer {
protected readonly shaderProgram: WebGLProgram

protected projMatrix: mat4
private activeShader: WebGLProgram
protected activeShader: WebGLProgram

constructor(
protected readonly gl: WebGLRenderingContext,
Expand Down Expand Up @@ -114,7 +115,70 @@ export class Renderer {
this.setUniform('mProj', this.projMatrix)
}

protected drawMesh(mesh: Mesh, options: { pos?: boolean, color?: boolean, texture?: boolean, normal?: boolean, blockPos?: boolean }) {
protected extractCameraPositionFromView() {
// should only be used after prepareDraw()
const viewLocation = this.gl.getUniformLocation(this.activeShader, 'mView')
if (!viewLocation) {
throw new Error('Failed to get location of mView uniform')
}
const viewMatrixRaw = this.gl.getUniform(this.activeShader, viewLocation)
// Ensure we have a valid matrix; gl.getUniform returns an array-like object.
const viewMatrix = mat4.clone(viewMatrixRaw)
const invView = mat4.create()
if (!mat4.invert(invView, viewMatrix)) {
throw new Error('Inverting view matrix failed')
}
// Translation components are at indices 12, 13, 14.
return vec3.fromValues(invView[12], invView[13], invView[14])
}

public static computeQuadCenter(quad: Quad) {
const vertices = quad.vertices() // Array of Vertex objects
const center = [0, 0, 0]
for (const v of vertices) {
const pos = v.pos.components() // [x, y, z]
center[0] += pos[0]
center[1] += pos[1]
center[2] += pos[2]
}
center[0] /= vertices.length
center[1] /= vertices.length
center[2] /= vertices.length
return vec3.fromValues(center[0], center[1], center[2])
}

protected drawMesh(mesh: Mesh, options: { pos?: boolean, color?: boolean, texture?: boolean, normal?: boolean, blockPos?: boolean, sort?: boolean }) {
// If the mesh is intended for transparent rendering, sort the quads.
if (mesh.quadVertices() > 0 && options.sort) {
const cameraPos = this.extractCameraPositionFromView()
mesh.quads.sort((a, b) => {
const centerA = Renderer.computeQuadCenter(a)
const centerB = Renderer.computeQuadCenter(b)
const distA = vec3.distance(cameraPos, centerA)
const distB = vec3.distance(cameraPos, centerB)
return distB - distA // Sort in descending order (farthest first)
})
}

// If the mesh is too large, split it into smaller meshes
const meshes = mesh.split()

// We rebuild mesh only right before we render to avoid multiple rebuild
// Mesh will keep tracking whether itself is dirty or not to avoid unnecessary rebuild as well
meshes.forEach(m => m.rebuild(this.gl, {
pos: options.pos,
color: options.color,
texture: options.texture,
normal: options.normal,
blockPos: options.blockPos,
}))

for (const m of meshes) {
this.drawMeshInner(m, options)
}
}

protected drawMeshInner(mesh: Mesh, options: { pos?: boolean, color?: boolean, texture?: boolean, normal?: boolean, blockPos?: boolean, sort?: boolean }) {
if (mesh.quadVertices() > 0) {
if (options.pos) this.setVertexAttr('vertPos', 3, mesh.posBuffer)
if (options.color) this.setVertexAttr('vertColor', 3, mesh.colorBuffer)
Expand Down
22 changes: 13 additions & 9 deletions src/render/StructureRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { vec3 } from 'gl-matrix'
import { mat4 } from 'gl-matrix'
import { mat4, vec3 } from 'gl-matrix'
import type { Identifier, StructureProvider } from '../core/index.js'
import { BlockState } from '../core/index.js'
import type { Color } from '../index.js'
Expand Down Expand Up @@ -153,13 +152,12 @@ export class StructureRenderer extends Renderer {
for (let x = 1; x <= X; x += 1) mesh.addLine(x, 0, 0, x, 0, Z, c)
for (let z = 1; z <= Z; z += 1) mesh.addLine(0, 0, z, X, 0, z, c)

return mesh.rebuild(this.gl, { pos: true, color: true })
return mesh
}

private getOutlineMesh(): Mesh {
return new Mesh()
.addLineCube(0, 0, 0, 1, 1, 1, [1, 1, 1])
.rebuild(this.gl, { pos: true, color: true })
}

private getInvisibleBlocksMesh(): Mesh {
Expand Down Expand Up @@ -187,7 +185,7 @@ export class StructureRenderer extends Renderer {
}
}

return mesh.rebuild(this.gl, { pos: true, color: true })
return mesh
}

public drawGrid(viewMatrix: mat4) {
Expand All @@ -212,17 +210,23 @@ export class StructureRenderer extends Renderer {
this.setTexture(this.atlasTexture)
this.prepareDraw(viewMatrix)

this.chunkBuilder.getMeshes().forEach(mesh => {
this.drawMesh(mesh, { pos: true, color: true, texture: true, normal: true })
this.chunkBuilder.getNonTransparentMeshes().forEach(mesh => {
this.drawMesh(mesh, { pos: true, color: true, texture: true, normal: true, sort: false })
})
this.chunkBuilder.getTransparentMeshes(this.extractCameraPositionFromView()).forEach(mesh => {
this.drawMesh(mesh, { pos: true, color: true, texture: true, normal: true, sort: true })
})
}

public drawColoredStructure(viewMatrix: mat4) {
this.setShader(this.colorShaderProgram)
this.prepareDraw(viewMatrix)

this.chunkBuilder.getMeshes().forEach(mesh => {
this.drawMesh(mesh, { pos: true, color: true, normal: true, blockPos: true })
this.chunkBuilder.getNonTransparentMeshes().forEach(mesh => {
this.drawMesh(mesh, { pos: true, color: true, normal: true, blockPos: true, sort: false })
})
this.chunkBuilder.getTransparentMeshes(this.extractCameraPositionFromView()).forEach(mesh => {
this.drawMesh(mesh, { pos: true, color: true, normal: true, blockPos: true, sort: true })
})
}

Expand Down
3 changes: 0 additions & 3 deletions src/render/VoxelRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,6 @@ export class VoxelRenderer extends Renderer {
if (!mesh.isEmpty()) {
meshes.push(mesh)
}
for (const mesh of meshes) {
mesh.rebuild(this.gl, { pos: true, color: true })
}
return meshes
}

Expand Down