133 lines
4.5 KiB
JavaScript
133 lines
4.5 KiB
JavaScript
// 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,
|
|
};
|
|
}
|
|
}
|