import { key, isFullCube, unitBox } from "./math.js"; import { getBlockFaceTextures, getFaceUV } from "./block-textures.js"; import { createAmbientOcclusionSampler } from "./ambient-occlusion.js"; /** Shared terrain and actor format; all values become Float32 at upload. */ export const MESH_VERTEX_STRIDE = 20; export const faces = [ { n: [1, 0, 0], v: [ [1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1], ], }, { n: [-1, 0, 0], v: [ [0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0], ], }, { n: [0, 1, 0], v: [ [0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0], ], }, { n: [0, -1, 0], v: [ [0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1], ], }, { n: [0, 0, 1], v: [ [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1], ], }, { n: [0, 0, -1], v: [ [1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0], ], }, ]; export function boxVertices( out, pos, box, color, material = 0, skip = () => false, yaw = 0, opacity = 1, textures = null, state = "", ambient = null, emission = 0, blockLight = null, dynamicLight = false, origin = [0,0,0], ) { for (let f = 0; f < 6; f++) { if (skip(faces[f].n)) continue; const face = faces[f], c = Math.cos(yaw), s = Math.sin(yaw), n = [ face.n[0] * c - face.n[2] * s, face.n[1], face.n[0] * s + face.n[2] * c, ]; const light = ambient ? ambient(pos, box, face.n, face.v) : [1, 1, 1, 1]; const propagated = !dynamicLight && blockLight ? blockLight.sampleFace(pos, box, face.n, face.v) : null; // Flip the diagonal so occlusion interpolation has no dark triangular seam. const indices = light[0] + light[2] > light[1] + light[3] ? [0, 1, 3, 1, 2, 3] : [0, 1, 2, 0, 2, 3]; for (const i of indices) { const v = face.v[i].map( (v, k) => box.min[k] + v * (box.max[k] - box.min[k]), ); const point = [pos[0] + v[0] * c - v[2] * s, pos[1] + v[1], pos[2] + v[0] * s + v[2] * c]; const local = dynamicLight && blockLight ? blockLight.sample(point.map((p, axis) => p + n[axis] * 0.02)) : null; out.push( point[0]-origin[0], point[1]-origin[1], point[2]-origin[2], ...(textures?.[f]?.tint || color), ...n, material, opacity, ...getFaceUV(f, v, state, textures?.[f]), textures?.[f]?.layer ?? -1, light[i], emission, ...(propagated?.block[i] || local?.block || [0, 0, 0]), propagated?.sky[i] ?? local?.sky ?? 1, ); } } } export function materialType(state = "", effect) { if (effect === "bounce") return 5; if (/^minecraft:(water|bubble_column)(\[|$)/.test(state)) return 6; if (state.includes("grass_block")) return 1; if (state.includes("planks")) return 2; if (state.includes("brick")) return 3; if (/log|stem|wood/.test(state)) return 4; return 0; } /** * Pure section meshing for both worker and synchronous fallback. World maps and * the light field are immutable for a build. A caller may reuse faceTextureCache * between sections; clear it whenever texture layers change. */ export function buildSectionMesh({ id, blocks, materials, textureLayers = new Map(), blockLightField = null, faceTextureCache = new Map(), localCoordinates = false, }) { const faceTextures = (mat) => { if (!textureLayers.size) return null; if (!faceTextureCache.has(mat.state)) faceTextureCache.set(mat.state, getBlockFaceTextures(mat.state, textureLayers)); return faceTextureCache.get(mat.state); }; const base = id.split(",").map((value) => Number(value) * 16), vertices = [], transparent = [], ambient = createAmbientOcclusionSampler(blocks, materials, (mat) => !mat || (mat.opacity ?? 1) >= 1 && !faceTextures(mat)?.some(face => face?.cutout)); for (let x = base[0]; x < base[0] + 16; x++) for (let y = base[1]; y < base[1] + 16; y++) for (let z = base[2]; z < base[2] + 16; z++) { const pos = [x, y, z], block = blocks.getAt ? blocks.getAt(x,y,z) : blocks.get(key(pos)); if (!block) continue; const mat = materials.get(block) || { color: [150, 152, 142], render: [unitBox], }, color = mat.color.map((v) => v / 255), textures = faceTextures(mat), cutout = textures?.some((face) => face?.cutout), opacity = cutout ? 1 : (mat.opacity ?? 1), full = isFullCube(mat) && !cutout; for (const box of mat.render || [unitBox]) { boxVertices( opacity < 1 ? transparent : vertices, pos, box, color, materialType(mat.state, mat.effect), (n) => { if (!full) return false; const neighbor = materials.get( (blocks.getAt ? blocks.getAt(pos[0]+n[0],pos[1]+n[1],pos[2]+n[2]) : blocks.get(key(pos.map((v, i) => v + n[i])))), ); return ( isFullCube(neighbor) && !faceTextures(neighbor)?.some((face) => face?.cutout) ); }, 0, opacity, textures, mat.state, ambient, Math.max(0, Math.min(1, (mat.light || 0) / 15)), blockLightField, false, localCoordinates ? base : [0,0,0], ); } } return { vertices: new Float32Array(vertices), transparent: new Float32Array(transparent), origin: localCoordinates ? base : [0,0,0] }; }