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 }) }) }