```html Project Obsidian - Voxel Engine
FPS: 0
X: 0 Y: 0 Z: 0
Chunk: 0, 0
GENERATING WORLD...
``` ```javascript // main.js import * as THREE from 'three'; import * as CANNON from 'cannon-es'; import { World } from './World.js'; import { Player } from './Player.js'; import { InputManager } from './InputManager.js'; class Game { constructor() { this.canvas = document.getElementById('canvas'); this.loadingEl = document.getElementById('loading'); this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x87CEEB); this.scene.fog = new THREE.Fog(0x87CEEB, 50, 200); this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: false, powerPreference: 'high-performance' }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); this.physicsWorld = new CANNON.World({ gravity: new CANNON.Vec3(0, -32, 0) }); this.physicsWorld.broadphase = new CANNON.SAPBroadphase(this.physicsWorld); this.physicsWorld.defaultContactMaterial.friction = 0.3; const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 100, 50); this.scene.add(directionalLight); this.world = new World(this.scene, this.physicsWorld); this.inputManager = new InputManager(this.canvas); this.player = new Player(this.camera, this.physicsWorld, this.inputManager, this.world); this.clock = new THREE.Clock(); this.fps = 0; this.frameCount = 0; this.lastFpsUpdate = 0; window.addEventListener('resize', () => this.onResize()); this.init(); } async init() { await this.world.init(); this.loadingEl.style.display = 'none'; this.inputManager.lockPointer(); this.animate(); } onResize() { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); } updateUI() { const pos = this.player.getPosition(); const chunkX = Math.floor(pos.x / 16); const chunkZ = Math.floor(pos.z / 16); document.getElementById('fps').textContent = `FPS: ${this.fps}`; document.getElementById('position').textContent = `X: ${pos.x.toFixed(1)} Y: ${pos.y.toFixed(1)} Z: ${pos.z.toFixed(1)}`; document.getElementById('chunk').textContent = `Chunk: ${chunkX}, ${chunkZ}`; } animate() { requestAnimationFrame(() => this.animate()); const delta = Math.min(this.clock.getDelta(), 0.1); this.physicsWorld.step(1 / 60, delta, 3); this.player.update(delta); this.world.update(this.player.getPosition()); this.renderer.render(this.scene, this.camera); this.frameCount++; const now = performance.now(); if (now - this.lastFpsUpdate > 1000) { this.fps = Math.round(this.frameCount * 1000 / (now - this.lastFpsUpdate)); this.frameCount = 0; this.lastFpsUpdate = now; this.updateUI(); } } } new Game(); ``` ```javascript // World.js import * as THREE from 'three'; import { Chunk } from './Chunk.js'; import { TerrainGenerator } from './TerrainGenerator.js'; export class World { constructor(scene, physicsWorld) { this.scene = scene; this.physicsWorld = physicsWorld; this.chunks = new Map(); this.renderDistance = 8; this.terrainGenerator = new TerrainGenerator(); this.chunkWorkers = []; this.workerQueue = []; const workerCount = navigator.hardwareConcurrency || 4; for (let i = 0; i < Math.min(workerCount, 4); i++) { const worker = new Worker('./ChunkWorker.js', { type: 'module' }); worker.onmessage = (e) => this.handleWorkerMessage(e); this.chunkWorkers.push({ worker, busy: false }); } } async init() { const promises = []; const spawnChunkX = 0; const spawnChunkZ = 0; for (let x = spawnChunkX - 2; x <= spawnChunkX + 2; x++) { for (let z = spawnChunkZ - 2; z <= spawnChunkZ + 2; z++) { promises.push(this.loadChunk(x, z)); } } await Promise.all(promises); } getChunkKey(x, z) { return `${x},${z}`; } async loadChunk(chunkX, chunkZ) { const key = this.getChunkKey(chunkX, chunkZ); if (this.chunks.has(key)) return; const chunk = new Chunk(chunkX, chunkZ, this.scene); this.chunks.set(key, chunk); const blocks = this.terrainGenerator.generateChunk(chunkX, chunkZ); chunk.setBlocks(blocks); return new Promise((resolve) => { this.workerQueue.push({ chunkX, chunkZ, blocks, chunk, resolve }); this.processWorkerQueue(); }); } processWorkerQueue() { if (this.workerQueue.length === 0) return; const availableWorker = this.chunkWorkers.find(w => !w.busy); if (!availableWorker) return; const job = this.workerQueue.shift(); availableWorker.busy = true; availableWorker.currentJob = job; availableWorker.worker.postMessage({ chunkX: job.chunkX, chunkZ: job.chunkZ, blocks: job.blocks }); } handleWorkerMessage(e) { const worker = this.chunkWorkers.find(w => w.worker === e.target); if (!worker || !worker.currentJob) return; const { chunk, resolve } = worker.currentJob; const { positions, normals, uvs, indices, colors } = e.data; if (positions.length > 0) { chunk.createMesh(positions, normals, uvs, indices, colors); } worker.busy = false; worker.currentJob = null; resolve(); this.processWorkerQueue(); } unloadChunk(chunkX, chunkZ) { const key = this.getChunkKey(chunkX, chunkZ); const chunk = this.chunks.get(key); if (chunk) { chunk.dispose(); this.chunks.delete(key); } } update(playerPosition) { const playerChunkX = Math.floor(playerPosition.x / 16); const playerChunkZ = Math.floor(playerPosition.z / 16); const toLoad = []; for (let x = playerChunkX - this.renderDistance; x <= playerChunkX + this.renderDistance; x++) { for (let z = playerChunkZ - this.renderDistance; z <= playerChunkZ + this.renderDistance; z++) { const key = this.getChunkKey(x, z); if (!this.chunks.has(key)) { toLoad.push([x, z]); } } } if (toLoad.length > 0) { const closest = toLoad.sort((a, b) => { const distA = Math.hypot(a[0] - playerChunkX, a[1] - playerChunkZ); const distB = Math.hypot(b[0] - playerChunkX, b[1] - playerChunkZ); return distA - distB; }); for (let i = 0; i < Math.min(2, closest.length); i++) { this.loadChunk(closest[i][0], closest[i][1]); } } const toUnload = []; for (const [key, chunk] of this.chunks) { const dist = Math.hypot(chunk.chunkX - playerChunkX, chunk.chunkZ - playerChunkZ); if (dist > this.renderDistance + 2) { toUnload.push([chunk.chunkX, chunk.chunkZ]); } } for (const [x, z] of toUnload) { this.unloadChunk(x, z); } } getBlock(x, y, z) { const chunkX = Math.floor(x / 16); const chunkZ = Math.floor(z / 16); const key = this.getChunkKey(chunkX, chunkZ); const chunk = this.chunks.get(key); if (!chunk) return 0; const localX = ((x % 16) + 16) % 16; const localY = y; const localZ = ((z % 16) + 16) % 16; return chunk.getBlock(localX, localY, localZ); } setBlock(x, y, z, type) { const chunkX = Math.floor(x / 16); const chunkZ = Math.floor(z / 16); const key = this.getChunkKey(chunkX, chunkZ); const chunk = this.chunks.get(key); if (!chunk) return; const localX = ((x % 16) + 16) % 16; const localY = y; const localZ = ((z % 16) + 16) % 16; chunk.setBlock(localX, localY, localZ, type); } } ``` ```javascript // Chunk.js import * as THREE from 'three'; export class Chunk { constructor(chunkX, chunkZ, scene) { this.chunkX = chunkX; this.chunkZ = chunkZ; this.scene = scene; this.width = 16; this.height = 256; this.depth = 16; this.blocks = new Uint8Array(this.width * this.height * this.depth); this.mesh = null; } setBlocks(blocks) { this.blocks = blocks; } getBlock(x, y, z) { if (x < 0 || x >= this.width || y < 0 || y >= this.height || z < 0 || z >= this.depth) { return 0; } return this.blocks[x + y * this.width + z * this.width * this.height]; } setBlock(x, y, z, type) { if (x < 0 || x >= this.width || y < 0 || y >= this.height || z < 0 || z >= this.depth) { return; } this.blocks[x + y * this.width + z * this.width * this.height] = type; this.needsRebuild = true; } createMesh(positions, normals, uvs, indices, colors) { if (this.mesh) { this.scene.remove(this.mesh); this.mesh.geometry.dispose(); this.mesh.material.dispose(); } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); geometry.setIndex(indices); const material = new THREE.MeshLambertMaterial({ vertexColors: true, side: THREE.FrontSide }); this.mesh = new THREE.Mesh(geometry, material); this.mesh.position.set(this.chunkX * 16, 0, this.chunkZ * 16); this.mesh.castShadow = false; this.mesh.receiveShadow = false; this.scene.add(this.mesh); } dispose() { if (this.mesh) { this.scene.remove(this.mesh); this.mesh.geometry.dispose(); this.mesh.material.dispose(); } } } ``` ```javascript // ChunkWorker.js self.onmessage = function(e) { const { chunkX, chunkZ, blocks } = e.data; const geometry = generateGreedyMesh(blocks); self.postMessage(geometry); }; function generateGreedyMesh(blocks) { const width = 16; const height = 256; const depth = 16; const positions = []; const normals = []; const uvs = []; const indices = []; const colors = []; let vertexCount = 0; const getBlock = (x, y, z) => { if (x < 0 || x >= width || y < 0 || y >= height || z < 0 || z >= depth) { return 0; } return blocks[x + y * width + z * width * height]; }; const getBlockColor = (type) => { switch(type) { case 1: return [0.13, 0.55, 0.13]; // Grass case 2: return [0.55, 0.27, 0.07]; // Dirt case 3: return [0.5, 0.5, 0.5]; // Stone case 4: return [0.76, 0.70, 0.50]; // Sand case 5: return [0.4, 0.26, 0.13]; // Wood case 6: return [0.13, 0.55, 0.13]; // Leaves case 7: return [0.9, 0.9, 0.9]; // Snow case 8: return [0.7, 0.9, 1.0]; // Ice default: return [1, 1, 1]; } }; const faces = [ { dir: [0, 1, 0], corners: [[0,1,1],[1,1,1],[0,1,0],[1,1,0]], normal: [0,1,0] }, { dir: [0,-1, 0], corners: [[1,0,1],[0,0,1],[1,0,0],[0,0,0]], normal: [0,-1,0] }, { dir: [1, 0, 0], corners: [[1,0,1],[1,1,1],[1,0,0],[1,1,0]], normal: [1,0,0] }, { dir: [-1, 0, 0], corners: [[0,0,0],[0,1,0],[0,0,1],[0,1,1]], normal: [-1,0,0] }, { dir: [0, 0, 1], corners: [[0,0,1],[0,1,1],[1,0,1],[1,1,1]], normal: [0,0,1] }, { dir: [0, 0,-1], corners: [[1,0,0],[1,1,0],[0,0,0],[0,1,0]], normal: [0,0,-1] } ]; for (let face of faces) { const [dx, dy, dz] = face.dir; const mask = new Int32Array(width * height); for (let d = 0; d < depth; d++) { mask.fill(0); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let px, py, pz, nx, ny, nz; if (dx !== 0) { px = x; py = y; pz = d; nx = x + dx; ny = y; nz = d; } else if (dy !== 0) { px = x; py = y; pz = d; nx = x; ny = y + dy; nz = d; } else { px = x; py = y; pz = d; nx = x; ny = y; nz = d + dz; } const current = getBlock(px, py, pz); const neighbor = getBlock(nx, ny, nz); if (current > 0 && neighbor === 0) { mask[x + y * width] = current; } } } for (let y = 0; y < height; y++) { for (let x = 0; x < width;) { if (mask[x + y * width] === 0) { x++; continue; } const currentBlock = mask[x + y * width]; let w = 1; while (x + w < width && mask[x + w + y * width] === currentBlock) { w++; } let h = 1; let done = false; while (y + h < height && !done) { for (let k = 0; k < w; k++) { if (mask[x + k + (y + h) * width] !== currentBlock) { done = true; break; } } if (!done) h++; } const color = getBlockColor(currentBlock); const ao = 0.85; for (let corner of face.corners) { let vx, vy, vz; if (dx !== 0) { vx = x + (dx > 0 ? 1 : 0); vy = y + corner[1] * h; vz = d + corner[2]; } else if (dy !== 0) { vx = x + corner[0] * w; vy = y + (dy > 0 ? 1 : 0); vz = d + corner[2]; } else { vx = x + corner[0] * w; vy = y + corner[1] * h; vz = d + (dz > 0 ? 1 : 0); } positions.push(vx, vy, vz); normals.push(face.normal[0], face.normal[1], face.normal[2]); uvs.push(corner[0], corner[1]); colors.push(color[0] * ao, color[1] * ao, color[2] * ao); } indices.push( vertexCount, vertexCount + 1, vertexCount + 2, vertexCount + 2, vertexCount + 1, vertexCount + 3 ); vertexCount += 4; for (let j = 0; j < h; j++) { for (let i = 0; i < w; i++) { mask[x + i + (y + j) * width] = 0; } } x += w; } } } } return { positions, normals, uvs, indices, colors }; } ``` ```javascript // TerrainGenerator.js export class TerrainGenerator { constructor(seed = 12345) { this.seed = seed; } hash(x, y) { let h = this.seed + x * 374761393 + y * 668265263; h = (h ^ (h >> 13)) * 1274126177; return (h ^ (h >> 16)) >>> 0; } noise2D(x, y) { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; x -= Math.floor(x); y -= Math.floor(y); const u = x * x * (3 - 2 * x); const v = y * y * (3 - 2 * y); const a = this.hash(X, Y) / 0xFFFFFFFF; const b = this.hash(X + 1, Y) / 0xFFFFFFFF; const c = this.hash(X, Y + 1) / 0xFFFFFFFF; const d = this.hash(X + 1, Y + 1) / 0xFFFFFFFF; return a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v; } noise3D(x, y, z) { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; const Z = Math.floor(z) & 255; x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z); const u = x * x * (3 - 2 * x); const v = y * y * (3 - 2 * y); const w = z * z * (3 - 2 * z); const a = this.hash(X, Y + this.hash(0, Z)) / 0xFFFFFFFF; const b = this.hash(X + 1, Y + this.hash(0, Z)) / 0xFFFFFFFF; const c = this.hash(X, Y + 1 + this.hash(0, Z)) / 0xFFFFFFFF; const d = this.hash(X + 1, Y + 1 + this.hash(0, Z)) / 0xFFFFFFFF; const e = this.hash(X, Y + this.hash(0, Z + 1)) / 0xFFFFFFFF; const f = this.hash(X + 1, Y + this.hash(0, Z + 1)) / 0xFFFFFFFF; const g = this.hash(X, Y + 1 + this.hash(0, Z + 1)) / 0xFFFFFFFF; const h = this.hash(X + 1, Y + 1 + this.hash(0, Z + 1)) / 0xFFFFFFFF; const k1 = a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v; const k2 = e * (1 - u) * (1 - v) + f * u * (1 - v) + g * (1 - u) * v + h * u * v; return k1 * (1 - w) + k2 * w; } octaveNoise2D(x, y, octaves, persistence) { let total = 0; let frequency = 1; let amplitude = 1; let maxValue = 0; for (let i = 0; i < octaves; i++) { total += this.noise2D(x * frequency, y * frequency) * amplitude; maxValue += amplitude; amplitude *= persistence; frequency *= 2; } return total / maxValue; } getBiome(x, z) { const temp = this.octaveNoise2D(x * 0.001, z * 0.001, 4, 0.5); const humidity = this.octaveNoise2D(x * 0.001 + 1000, z * 0.001 + 1000, 4, 0.5); if (temp < 0.3) return 'winter'; if (humidity < 0.3) return 'desert'; if (temp > 0.7 && humidity > 0.6) return 'mountains'; return 'plains'; } generateChunk(chunkX, chunkZ) { const width = 16; const height = 256; const depth = 16; const blocks = new Uint8Array(width * height * depth); for (let x = 0; x < width; x++) { for (let z = 0; z < depth; z++) { const worldX = chunkX * 16 + x; const worldZ = chunkZ * 16 + z; const biome = this.getBiome(worldX, worldZ); const continentalness = this.octaveNoise2D(worldX * 0.0005, worldZ * 0.0005, 4, 0.5); const erosion = this.octaveNoise2D(worldX * 0.002, worldZ * 0.002, 4, 0.5); let baseHeight = 64; baseHeight += continentalness * 32; baseHeight += this.octaveNoise2D(worldX * 0.01, worldZ * 0.01, 4, 0.5) * 16; if (biome === 'mountains') { baseHeight += 40; } const terrainHeight = Math.floor(baseHeight); for (let y = 0; y < height; y++) { const index = x + y * width + z * width * height; const cave = this.noise3D(worldX * 0.05, y * 0.05, worldZ * 0.05); const isCave = y > 5 && y < terrainHeight - 5 && cave > 0.6; if