// 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, }; } }