```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