// Face-Culling-Mesher: baut aus Chunk-Blockdaten + Nachbar-Lookups zwei // BufferGeometrys (opaque + transparent/Wasser). Nur Faces zu Luft/transparenten // Nachbarn werden erzeugt. import * as THREE from 'three'; import { CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, ATLAS_COLS, tileFor, isRenderTransparent, isSolid, } from './constants.js'; import { AO_LEVEL } from './config.js'; // Baked Ambient Occlusion (Minecraft-"Smooth Lighting"): fuer jeden Vertex // wird aus den soliden Nachbarbloecken in der Aussen-Schicht ein Verdunkelungs- // wert berechnet. Billiger als SSAO und bei Voxels korrekt (keine Bildraum- // Artifacts). Siehe 0fps "Ambient occlusion for Minecraft-like worlds". // Index 0 = staerkste Verdeckung (dunkelste Ecke), 3 = keine Verdeckung. // Die Stufen-Werte selbst liegen in config.js (AO_LEVEL). // side1/side2/corner: je 0 (offen) oder 1 (solid). Liefert AO-Stufe 0..3. function vertexAO(s1, s2, c) { if (s1 && s2) return 0; // beide Seiten zu -> Ecke voll verdeckt return 3 - (s1 + s2 + c); } // Die 6 Wuerfelseiten mit Eck-Positionen und Tile-UVs (0..1). // side wird aus dir abgeleitet: dir[1]===1 -> 'top', -1 -> 'bottom', sonst 'side'. // uAxis/vAxis: die zwei zu dir (Normalen-Achse) senkrechten Achsen (0=x,1=y,2=z), // benoetigt fuer die AO-Nachbar-Lookups je Vertex. const FACES = [ { // links -X (normal-Achse 0) dir: [-1, 0, 0], uAxis: 2, vAxis: 1, corners: [ { pos: [0, 1, 0], uv: [0, 1] }, { pos: [0, 0, 0], uv: [0, 0] }, { pos: [0, 1, 1], uv: [1, 1] }, { pos: [0, 0, 1], uv: [1, 0] }, ], }, { // rechts +X (normal-Achse 0) dir: [1, 0, 0], uAxis: 2, vAxis: 1, corners: [ { pos: [1, 1, 1], uv: [0, 1] }, { pos: [1, 0, 1], uv: [0, 0] }, { pos: [1, 1, 0], uv: [1, 1] }, { pos: [1, 0, 0], uv: [1, 0] }, ], }, { // unten -Y (normal-Achse 1) dir: [0, -1, 0], uAxis: 0, vAxis: 2, corners: [ { pos: [1, 0, 1], uv: [1, 0] }, { pos: [0, 0, 1], uv: [0, 0] }, { pos: [1, 0, 0], uv: [1, 1] }, { pos: [0, 0, 0], uv: [0, 1] }, ], }, { // oben +Y (normal-Achse 1) dir: [0, 1, 0], uAxis: 0, vAxis: 2, corners: [ { pos: [0, 1, 1], uv: [1, 1] }, { pos: [1, 1, 1], uv: [0, 1] }, { pos: [0, 1, 0], uv: [1, 0] }, { pos: [1, 1, 0], uv: [0, 0] }, ], }, { // hinten -Z (normal-Achse 2) dir: [0, 0, -1], uAxis: 0, vAxis: 1, corners: [ { pos: [1, 0, 0], uv: [0, 0] }, { pos: [0, 0, 0], uv: [1, 0] }, { pos: [1, 1, 0], uv: [0, 1] }, { pos: [0, 1, 0], uv: [1, 1] }, ], }, { // vorne +Z (normal-Achse 2) dir: [0, 0, 1], uAxis: 0, vAxis: 1, corners: [ { pos: [0, 0, 1], uv: [0, 0] }, { pos: [1, 0, 1], uv: [1, 0] }, { pos: [0, 1, 1], uv: [0, 1] }, { pos: [1, 1, 1], uv: [1, 1] }, ], }, ]; function sideOf(dir) { if (dir[1] === 1) return 'top'; if (dir[1] === -1) return 'bottom'; return 'side'; } // Soll ein Face zwischen Block und Nachbar gezeichnet werden? function shouldDrawFace(block, neighbor) { if (neighbor === BLOCK.AIR) return true; // Wasser/Sichtbarkeit: Face zeichnen, wenn Nachbar transparent ist und // nicht derselbe Block (vermeidet innere Wasserfaces). if (isRenderTransparent(neighbor)) return neighbor !== block; return false; } function makeBuffer() { return { positions: [], normals: [], uvs: [], colors: [], indices: [] }; } function addFace(buf, x, y, z, face, block, getBlockWorld, baseX, baseZ) { const side = sideOf(face.dir); const tile = tileFor(block, side); const ndx = buf.positions.length / 3; const dir = face.dir; const ua = face.uAxis, va = face.vAxis; const wx = baseX + x, wz = baseZ + z; // Welt-Koordinaten des Blocks const ao = [0, 0, 0, 0]; for (let i = 0; i < 4; i++) { const c = face.corners[i]; buf.positions.push(x + c.pos[0], y + c.pos[1], z + c.pos[2]); buf.normals.push(dir[0], dir[1], dir[2]); const u = (tile.col + c.uv[0]) / ATLAS_COLS; const v = 1 - (tile.row + 1 - c.uv[1]) / ATLAS_COLS; buf.uvs.push(u, v); // AO: Nachbarn in der Aussen-Schicht (Block + dir). du/dv = -1/+1 je nach // Ecke. side1 in uAxis-, side2 in vAxis-Richtung, corner diagonal. const du = c.pos[ua] === 1 ? 1 : -1; const dv = c.pos[va] === 1 ? 1 : -1; const s1 = [dir[0], dir[1], dir[2]]; s1[ua] += du; const s2 = [dir[0], dir[1], dir[2]]; s2[va] += dv; const co = [dir[0], dir[1], dir[2]]; co[ua] += du; co[va] += dv; const s1Solid = isSolid(getBlockWorld(wx + s1[0], y + s1[1], wz + s1[2])) ? 1 : 0; const s2Solid = isSolid(getBlockWorld(wx + s2[0], y + s2[1], wz + s2[2])) ? 1 : 0; const cSolid = isSolid(getBlockWorld(wx + co[0], y + co[1], wz + co[2])) ? 1 : 0; const level = vertexAO(s1Solid, s2Solid, cSolid); ao[i] = level; const b = AO_LEVEL[level]; buf.colors.push(b, b, b); } // Flip-Triangulation: Diagonale entlang der Eck-Paarung mit hoeherer AO-Summe // legen -> reduziert Interpolations-Artifacts (Helligkeitssprung) an der Naht. // Winding bleibt erhalten (geprueft: beide Varianten ergeben gleiche Normalen). if (ao[0] + ao[3] > ao[1] + ao[2]) { buf.indices.push(ndx, ndx + 1, ndx + 3, ndx, ndx + 3, ndx + 2); } else { buf.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3); } } function toGeometry(buf) { if (buf.positions.length === 0) return null; const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.Float32BufferAttribute(buf.positions, 3)); g.setAttribute('normal', new THREE.Float32BufferAttribute(buf.normals, 3)); g.setAttribute('uv', new THREE.Float32BufferAttribute(buf.uvs, 2)); g.setAttribute('color', new THREE.Float32BufferAttribute(buf.colors, 3)); // WICHTIG: setIndex braucht ein BufferAttribute, kein rohes TypedArray. // In three r0.165 speichert setIndex(rohesTypedArray) es ungepackt, so dass // der Renderer beim Render auf attribute.array undefined trifft und crasht // ("Cannot read properties of undefined (reading 'byteLength')"). const maxV = (buf.positions.length / 3) - 1; const IndexAttr = maxV > 65535 ? THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute; g.setIndex(new IndexAttr(buf.indices, 1)); return g; } // world.getBlockWorld(x,y,z) liefert Block auch ueber Chunk-Grenzen. export function buildChunkGeometry(getBlockWorld, chunk) { const opaque = makeBuffer(); const transparent = makeBuffer(); const baseX = chunk.cx * CHUNK_SIZE; const baseZ = chunk.cz * CHUNK_SIZE; for (let y = 0; y < CHUNK_HEIGHT; y++) { for (let z = 0; z < CHUNK_SIZE; z++) { for (let x = 0; x < CHUNK_SIZE; x++) { const block = chunk.get(x, y, z); if (block === BLOCK.AIR) continue; const buf = isRenderTransparent(block) ? transparent : opaque; for (const face of FACES) { const nx = x + face.dir[0]; const ny = y + face.dir[1]; const nz = z + face.dir[2]; const neighbor = getBlockWorld(baseX + nx, ny, baseZ + nz); if (shouldDrawFace(block, neighbor)) { addFace(buf, x, y, z, face, block, getBlockWorld, baseX, baseZ); } } } } } return { opaque: toGeometry(opaque), transparent: toGeometry(transparent) }; }