Optimize terrain rendering for large exports
This commit is contained in:
132
renderer/mesh-buffers.mjs
Normal file
132
renderer/mesh-buffers.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
// Numeric section keys and one bit per opaque voxel avoid millions of coordinate
|
||||
// strings. Bounds checks prevent key aliasing outside the indexed sections.
|
||||
export class OpaqueIndex {
|
||||
constructor(bounds, minY, maxY) {
|
||||
this.minX = Math.floor(bounds.minX / 16);
|
||||
this.maxX = Math.floor(bounds.maxX / 16);
|
||||
this.minZ = Math.floor(bounds.minZ / 16);
|
||||
this.maxZ = Math.floor(bounds.maxZ / 16);
|
||||
this.minY = Math.floor(minY / 16);
|
||||
this.maxY = Math.floor(maxY / 16);
|
||||
this.width = this.maxX - this.minX + 1;
|
||||
this.depth = this.maxZ - this.minZ + 1;
|
||||
this.sections = new Map();
|
||||
}
|
||||
key(x, y, z) {
|
||||
const cx = Math.floor(x / 16),
|
||||
cy = Math.floor(y / 16),
|
||||
cz = Math.floor(z / 16);
|
||||
if (
|
||||
cx < this.minX ||
|
||||
cx > this.maxX ||
|
||||
cz < this.minZ ||
|
||||
cz > this.maxZ ||
|
||||
cy < this.minY ||
|
||||
cy > this.maxY
|
||||
)
|
||||
return -1;
|
||||
return (
|
||||
cx -
|
||||
this.minX +
|
||||
(cz - this.minZ) * this.width +
|
||||
(cy - this.minY) * this.width * this.depth
|
||||
);
|
||||
}
|
||||
add(x, y, z) {
|
||||
const key = this.key(x, y, z);
|
||||
if (key < 0) throw new Error("Terrain block outside indexed bounds");
|
||||
let section = this.sections.get(key);
|
||||
if (!section) {
|
||||
section = new Uint32Array(128);
|
||||
this.sections.set(key, section);
|
||||
}
|
||||
const bit = (x & 15) | ((z & 15) << 4) | ((y & 15) << 8);
|
||||
section[bit >>> 5] |= 1 << (bit & 31);
|
||||
}
|
||||
has(x, y, z) {
|
||||
const key = this.key(x, y, z);
|
||||
if (key < 0) return false;
|
||||
const section = this.sections.get(key);
|
||||
if (!section) return false;
|
||||
const bit = (x & 15) | ((z & 15) << 4) | ((y & 15) << 8);
|
||||
return (section[bit >>> 5] & (1 << (bit & 31))) !== 0;
|
||||
}
|
||||
clear() {
|
||||
this.sections.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Each buffer is bounded, keeps 16-bit triangle indices, and writes directly
|
||||
// into typed arrays. No per-vertex objects or growable JS number arrays.
|
||||
export class QuadBuffer {
|
||||
constructor(maxQuads = 4096) {
|
||||
this.maxQuads = maxQuads;
|
||||
this.capacity = Math.min(32, maxQuads);
|
||||
this.count = 0;
|
||||
this.positions = new Float32Array(this.capacity * 12);
|
||||
this.normals = new Float32Array(this.capacity * 12);
|
||||
this.uvs = new Float32Array(this.capacity * 8);
|
||||
this.indices = new Uint16Array(this.capacity * 6);
|
||||
this.min = [Infinity, Infinity, Infinity];
|
||||
this.max = [-Infinity, -Infinity, -Infinity];
|
||||
}
|
||||
get full() {
|
||||
return this.count === this.maxQuads;
|
||||
}
|
||||
add(face, x, y, z) {
|
||||
if (this.full) throw new Error("Geometry batch is full");
|
||||
if (this.count === this.capacity) {
|
||||
this.capacity = Math.min(this.maxQuads, this.capacity * 2);
|
||||
for (const [key, components, Type] of [
|
||||
["positions", 12, Float32Array],
|
||||
["normals", 12, Float32Array],
|
||||
["uvs", 8, Float32Array],
|
||||
["indices", 6, Uint16Array],
|
||||
]) {
|
||||
const next = new Type(this.capacity * components);
|
||||
next.set(this[key]);
|
||||
this[key] = next;
|
||||
}
|
||||
}
|
||||
const offset = this.count * 12;
|
||||
for (let i = 0; i < 12; i += 3) {
|
||||
this.positions[offset + i] = face.positions[i] + x;
|
||||
this.positions[offset + i + 1] = face.positions[i + 1] + y;
|
||||
this.positions[offset + i + 2] = face.positions[i + 2] + z;
|
||||
// Bounds must enclose the actual Float32 positions, including rounding
|
||||
// at large Minecraft coordinates, or frustum culling can cut off faces.
|
||||
const px = this.positions[offset + i],
|
||||
py = this.positions[offset + i + 1],
|
||||
pz = this.positions[offset + i + 2];
|
||||
this.normals[offset + i] = face.normal[0];
|
||||
this.normals[offset + i + 1] = face.normal[1];
|
||||
this.normals[offset + i + 2] = face.normal[2];
|
||||
if (px < this.min[0]) this.min[0] = px;
|
||||
if (py < this.min[1]) this.min[1] = py;
|
||||
if (pz < this.min[2]) this.min[2] = pz;
|
||||
if (px > this.max[0]) this.max[0] = px;
|
||||
if (py > this.max[1]) this.max[1] = py;
|
||||
if (pz > this.max[2]) this.max[2] = pz;
|
||||
}
|
||||
this.uvs.set(face.uvs, this.count * 8);
|
||||
const base = this.count * 4,
|
||||
index = this.count * 6;
|
||||
this.indices[index] = base;
|
||||
this.indices[index + 1] = base + 1;
|
||||
this.indices[index + 2] = base + 2;
|
||||
this.indices[index + 3] = base + 2;
|
||||
this.indices[index + 4] = base + 1;
|
||||
this.indices[index + 5] = base + 3;
|
||||
this.count++;
|
||||
}
|
||||
finish() {
|
||||
return {
|
||||
positions: this.positions.slice(0, this.count * 12),
|
||||
normals: this.normals.slice(0, this.count * 12),
|
||||
uvs: this.uvs.slice(0, this.count * 8),
|
||||
indices: this.indices.slice(0, this.count * 6),
|
||||
min: this.min,
|
||||
max: this.max,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user