From f3aba2c9997787ad1df2c87f42b86f65630940c4 Mon Sep 17 00:00:00 2001 From: Hanke Chen Date: Sat, 5 Apr 2025 16:06:46 -0400 Subject: [PATCH 1/2] Fix major rendering bug of semi-transparent blocks --- src/render/ChunkBuilder.ts | 56 ++++++++++++++++++++++++++++++--- src/render/Renderer.ts | 54 +++++++++++++++++++++++++++++-- src/render/StructureRenderer.ts | 17 ++++++---- 3 files changed, 113 insertions(+), 14 deletions(-) diff --git a/src/render/ChunkBuilder.ts b/src/render/ChunkBuilder.ts index 82f4bbae..29cae12d 100644 --- a/src/render/ChunkBuilder.ts +++ b/src/render/ChunkBuilder.ts @@ -89,20 +89,66 @@ export class ChunkBuilder { 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 }) + // We don't sort the transparent mesh here, as we trust the user will pass sort=True when calling drawMesh() to prevent double sorting }))) } 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 }) + // We don't sort the transparent mesh here, as we trust the user will pass sort=True when calling drawMesh() to prevent double sorting }) } } - 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 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 getNonTransparentMeshes(): Mesh[] { + return this.chunks.flatMap(x => x.flatMap(y => y.flatMap(chunk => chunk.mesh.isEmpty() ? [] : chunk.mesh))) } private needsCull(block: PlacedBlock, dir: Direction) { diff --git a/src/render/Renderer.ts b/src/render/Renderer.ts index 645996fb..b409b93e 100644 --- a/src/render/Renderer.ts +++ b/src/render/Renderer.ts @@ -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 = ` @@ -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, @@ -114,7 +115,54 @@ 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) + }) + // Rebuild the index buffer to reflect the new quad order. + mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true, blockPos: true }) + } + if (mesh.quadVertices() > 0) { if (options.pos) this.setVertexAttr('vertPos', 3, mesh.posBuffer) if (options.color) this.setVertexAttr('vertColor', 3, mesh.colorBuffer) diff --git a/src/render/StructureRenderer.ts b/src/render/StructureRenderer.ts index b5435082..9f85e9fe 100644 --- a/src/render/StructureRenderer.ts +++ b/src/render/StructureRenderer.ts @@ -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' @@ -212,8 +211,11 @@ 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 }) }) } @@ -221,8 +223,11 @@ export class StructureRenderer extends Renderer { 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 }) }) } From cc0dfd29d72b783c831c19bf7a42013af123ec6a Mon Sep 17 00:00:00 2001 From: Hanke Chen Date: Sat, 5 Apr 2025 22:04:11 -0400 Subject: [PATCH 2/2] Fix uint16 overflow issue when having large index --- src/render/ChunkBuilder.ts | 13 ----- src/render/ItemRenderer.ts | 1 - src/render/Mesh.ts | 93 +++++++++++++++++++++++++++++---- src/render/Renderer.ts | 22 ++++++-- src/render/StructureRenderer.ts | 5 +- src/render/VoxelRenderer.ts | 3 -- 6 files changed, 104 insertions(+), 33 deletions(-) diff --git a/src/render/ChunkBuilder.ts b/src/render/ChunkBuilder.ts index 29cae12d..e228bb4c 100644 --- a/src/render/ChunkBuilder.ts +++ b/src/render/ChunkBuilder.ts @@ -85,19 +85,6 @@ 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 }) - // We don't sort the transparent mesh here, as we trust the user will pass sort=True when calling drawMesh() to prevent double sorting - }))) - } else { - chunkPositions.forEach(chunkPos => { - const chunk = this.getChunk(chunkPos) - chunk.mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true, blockPos: true }) - // We don't sort the transparent mesh here, as we trust the user will pass sort=True when calling drawMesh() to prevent double sorting - }) - } } public getTransparentMeshes(cameraPos: vec3): Mesh[] { diff --git a/src/render/ItemRenderer.ts b/src/render/ItemRenderer.ts index ade44c5d..13205e66 100644 --- a/src/render/ItemRenderer.ts +++ b/src/render/ItemRenderer.ts @@ -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 } diff --git a/src/render/Mesh.ts b/src/render/Mesh.ts index 0da5567f..919b79a9 100644 --- a/src/render/Mesh.ts +++ b/src/render/Mesh.ts @@ -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 } @@ -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 } @@ -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 } @@ -74,6 +105,7 @@ 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 } @@ -81,6 +113,8 @@ export class Mesh { for (const quad of this.quads) { quad.transform(transformation) } + + this.setDirty({ quads: true }) return this } @@ -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) { @@ -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 diff --git a/src/render/Renderer.ts b/src/render/Renderer.ts index b409b93e..1e3c6453 100644 --- a/src/render/Renderer.ts +++ b/src/render/Renderer.ts @@ -148,7 +148,6 @@ export class Renderer { } 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() @@ -159,10 +158,27 @@ export class Renderer { const distB = vec3.distance(cameraPos, centerB) return distB - distA // Sort in descending order (farthest first) }) - // Rebuild the index buffer to reflect the new quad order. - mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true, blockPos: true }) } + // 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) diff --git a/src/render/StructureRenderer.ts b/src/render/StructureRenderer.ts index 9f85e9fe..758314ba 100644 --- a/src/render/StructureRenderer.ts +++ b/src/render/StructureRenderer.ts @@ -152,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 { @@ -186,7 +185,7 @@ export class StructureRenderer extends Renderer { } } - return mesh.rebuild(this.gl, { pos: true, color: true }) + return mesh } public drawGrid(viewMatrix: mat4) { diff --git a/src/render/VoxelRenderer.ts b/src/render/VoxelRenderer.ts index f85b8003..7f5adcea 100644 --- a/src/render/VoxelRenderer.ts +++ b/src/render/VoxelRenderer.ts @@ -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 }