Skip to content
Open
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
56 changes: 51 additions & 5 deletions src/render/ChunkBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
54 changes: 51 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,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)
Expand Down
17 changes: 11 additions & 6 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 @@ -212,17 +211,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