feat: Minecraft Clone MVP – prozedurale Welt, Blöcke, Abbauen/Setzen
- Weltgenerierung: FBM-Terrain, 3 Biome (Plains/Wald/Wüste), Höhlen, Wasser, Bäume - Blöcke: Grass, Dirt, Stein, Sand, Holz, Blätter, Wasser (+Bedrock) - Interaktion: DDA-Voxel-Raycast, Abbauen (LMB) & Setzen (RMB), Hotbar 1-6 - Spieler: First-Person, Gravitation, AABB-Kollision, Springen, Fly-Mode (F) - Engine: Chunk-Streaming (16x16x64), Face-Culling, opaker+transparenter Pass - Stack: Three.js + simplex-noise via CDN, prozedurale Texturen, kein Build-Step Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
1049253d54
15 files changed
+1436
No files matched your search
+145
@@ -0,0 +1,145 @@
|
||||
// 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,
|
||||
} from './constants.js';
|
||||
|
||||
// Die 6 Wuerfelseiten mit Eck-Positionen und Tile-UVs (0..1).
|
||||
// side wird aus dir abgeleitet: dir[1]===1 -> 'top', -1 -> 'bottom', sonst 'side'.
|
||||
const FACES = [
|
||||
{ // links -X
|
||||
dir: [-1, 0, 0],
|
||||
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
|
||||
dir: [1, 0, 0],
|
||||
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
|
||||
dir: [0, -1, 0],
|
||||
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
|
||||
dir: [0, 1, 0],
|
||||
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
|
||||
dir: [0, 0, -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
|
||||
dir: [0, 0, 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: [], indices: [] };
|
||||
}
|
||||
|
||||
function addFace(buf, x, y, z, face, block) {
|
||||
const side = sideOf(face.dir);
|
||||
const tile = tileFor(block, side);
|
||||
const ndx = buf.positions.length / 3;
|
||||
for (const c of face.corners) {
|
||||
buf.positions.push(x + c.pos[0], y + c.pos[1], z + c.pos[2]);
|
||||
buf.normals.push(face.dir[0], face.dir[1], face.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);
|
||||
}
|
||||
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));
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { opaque: toGeometry(opaque), transparent: toGeometry(transparent) };
|
||||
}
|
||||
Reference in new issue
Block a user