/** * @typedef {import("./types.js").GeneratedAssets} GeneratedAssets * @typedef {import("./VoxelGrid.js").VoxelGrid} VoxelGrid */ import * as THREE from "three"; /** * VoxelManager.js * Handles the Three.js rendering of the VoxelGrid data. * Updated to support Camera Focus Targeting, Emissive Textures, and Multi-Material Voxels. * @class */ export class VoxelManager { /** * @param {VoxelGrid} grid - Voxel grid to render * @param {THREE.Scene} scene - Three.js scene */ constructor(grid, scene) { /** @type {VoxelGrid} */ this.grid = grid; /** @type {THREE.Scene} */ this.scene = scene; // Map of Voxel ID -> InstancedMesh /** @type {Map} */ this.meshes = new Map(); // Default Material Definitions (Fallback) // Store actual Material instances, not just configs /** @type {Record} */ this.materials = { 1: new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.8 }), // Stone 2: new THREE.MeshStandardMaterial({ color: 0x3d2817, roughness: 1.0 }), // Dirt/Floor Base 10: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), // Wood (Destructible) 15: new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x004444, }), // Crystal }; // Shared Geometry /** @type {THREE.BoxGeometry} */ this.geometry = new THREE.BoxGeometry(1, 1, 1); // Camera Anchor: Invisible object to serve as OrbitControls target /** @type {THREE.Object3D} */ this.focusTarget = new THREE.Object3D(); this.focusTarget.name = "CameraFocusTarget"; this.scene.add(this.focusTarget); } /** * Updates the material definitions with generated assets. * Supports both simple Canvas textures and complex {diffuse, emissive, normal, roughness, bump} objects. * NOW SUPPORTS: 'palette' for batch loading procedural variations. * @param {GeneratedAssets} assets - Generated assets from world generator */ updateMaterials(assets) { if (!assets) return; // Helper to create a material INSTANCE from an asset const createMaterial = (asset) => { if (!asset) return null; const matDef = { color: 0xffffff, // White base to show texture colors roughness: 0.8, }; // Check if it's a simple Canvas or a Composite Object if ( asset instanceof OffscreenCanvas || asset instanceof HTMLCanvasElement ) { // Simple Diffuse Map const tex = new THREE.CanvasTexture(asset); tex.magFilter = THREE.NearestFilter; tex.minFilter = THREE.NearestFilter; tex.colorSpace = THREE.SRGBColorSpace; matDef.map = tex; } else if (asset.diffuse) { // Complex Map (Diffuse + optional maps) const diffTex = new THREE.CanvasTexture(asset.diffuse); diffTex.magFilter = THREE.NearestFilter; diffTex.minFilter = THREE.NearestFilter; diffTex.colorSpace = THREE.SRGBColorSpace; matDef.map = diffTex; // Emissive Map if (asset.emissive) { const emTex = new THREE.CanvasTexture(asset.emissive); emTex.magFilter = THREE.NearestFilter; emTex.minFilter = THREE.NearestFilter; emTex.colorSpace = THREE.SRGBColorSpace; matDef.emissiveMap = emTex; matDef.emissive = 0xffffff; // Max brightness for the map values matDef.emissiveIntensity = 1.0; // Adjustable glow strength } // Normal Map if (asset.normal) { const normTex = new THREE.CanvasTexture(asset.normal); normTex.magFilter = THREE.NearestFilter; normTex.minFilter = THREE.NearestFilter; normTex.colorSpace = THREE.NoColorSpace; // Normal maps are data, linear color space matDef.normalMap = normTex; } // Roughness Map if (asset.roughness) { const roughTex = new THREE.CanvasTexture(asset.roughness); roughTex.magFilter = THREE.NearestFilter; roughTex.minFilter = THREE.NearestFilter; roughTex.colorSpace = THREE.NoColorSpace; matDef.roughnessMap = roughTex; matDef.roughness = 1.0; // Let the map drive the value } // Bump Map if (asset.bump) { const bumpTex = new THREE.CanvasTexture(asset.bump); bumpTex.magFilter = THREE.NearestFilter; bumpTex.minFilter = THREE.NearestFilter; bumpTex.colorSpace = THREE.NoColorSpace; matDef.bumpMap = bumpTex; matDef.bumpScale = 0.05; // Standard bump scale for voxels } } return new THREE.MeshStandardMaterial(matDef); }; // 1. Process Standard Single Textures (Legacy/Base) const floorMat = assets.textures ? createMaterial(assets.textures["floor"]) : null; const wallMat = assets.textures ? createMaterial(assets.textures["wall"]) : null; // ID 1: Wall (All faces same) if (wallMat) this.materials[1] = wallMat; // ID 2: Floor (Top = Floor Tex, Sides/Bottom = Wall Tex) if (floorMat) { // Use wall texture for sides if available. // If not, fallback to the existing definition for ID 1 (Stone) to ensure sides look like walls. let sideMat = wallMat; if (!sideMat && this.materials[1] && !Array.isArray(this.materials[1])) { sideMat = this.materials[1]; } // Fallback to floor material if absolutely nothing else exists if (!sideMat) sideMat = floorMat; // BoxGeometry Material Index Order: Right, Left, Top, Bottom, Front, Back this.materials[2] = [ sideMat, // Right (+x) sideMat, // Left (-x) floorMat, // Top (+y) - The Damp Cave Floor sideMat, // Bottom (-y) sideMat, // Front (+z) sideMat, // Back (-z) ]; } // 2. Process Palette (Procedural Variations) if (assets.palette) { // First pass: Create all materials from palette assets for (const [idStr, asset] of Object.entries(assets.palette)) { const id = parseInt(idStr); const mat = createMaterial(asset); if (mat) { this.materials[id] = mat; } } // Second pass: Organize multi-material arrays for floors for (const [idStr, asset] of Object.entries(assets.palette)) { const id = parseInt(idStr); // Logic for Floor Variations (IDs 200-299) if (id >= 200 && id <= 299) { const floorMat = this.materials[id]; // Attempt to find matching wall variation (e.g. Floor 205 -> Wall 105) // If missing, fallback to Base Wall (ID 1) let sideMat = this.materials[id - 100]; if ( !sideMat && this.materials[1] && !Array.isArray(this.materials[1]) ) { sideMat = this.materials[1]; } // Fallback to floor material itself if absolutely nothing else exists if (!sideMat) sideMat = floorMat; if (sideMat && floorMat) { this.materials[id] = [ sideMat, sideMat, floorMat, sideMat, sideMat, sideMat, ]; } } } } this.update(); } /** * Re-calculates meshes based on grid data. */ update() { // 1. Cleanup existing meshes this.meshes.forEach((mesh) => { this.scene.remove(mesh); // Handle cleanup for single material or array of materials if (Array.isArray(mesh.material)) { mesh.material.forEach((m) => { // Dispose textures if they are unique to this mesh instance // (But here we share them via this.materials, so be careful not to over-dispose if reused) // For now, assume simple cleanup // if (m.map) m.map.dispose(); // m.dispose(); }); } else { // if (mesh.material.map) mesh.material.map.dispose(); // mesh.material.dispose(); } mesh.dispose(); }); this.meshes.clear(); // 2. Bucket coordinates by Voxel ID const buckets = {}; const dummy = new THREE.Object3D(); for (let x = 0; x < this.grid.size.x; x++) { for (let y = 0; y < this.grid.size.y; y++) { for (let z = 0; z < this.grid.size.z; z++) { const id = this.grid.getCell(x, y, z); if (id !== 0) { if (!buckets[id]) buckets[id] = []; dummy.position.set(x, y, z); dummy.updateMatrix(); buckets[id].push(dummy.matrix.clone()); } } } } // 3. Generate InstancedMesh for each ID for (const [idStr, matrices] of Object.entries(buckets)) { const id = parseInt(idStr); const count = matrices.length; // Get the actual Material Instance (or Array of Instances) const material = this.materials[id] || new THREE.MeshStandardMaterial({ color: 0xff00ff }); const mesh = new THREE.InstancedMesh(this.geometry, material, count); matrices.forEach((mat, index) => { mesh.setMatrixAt(index, mat); }); mesh.instanceMatrix.needsUpdate = true; this.meshes.set(id, mesh); this.scene.add(mesh); } // 4. Update Focus Target to Center of Grid // We position it at the horizontal center, and slightly up (y=0 or 1) this.focusTarget.position.set( this.grid.size.x / 2, 0, this.grid.size.z / 2 ); } /** * Helper to center the camera view on the grid. * @param {import("three/examples/jsm/controls/OrbitControls.js").OrbitControls} controls - The OrbitControls instance to update */ focusCamera(controls) { if (controls && this.focusTarget) { controls.target.copy(this.focusTarget.position); controls.update(); } } /** * Updates a single voxel (triggers full update). * @param {number} x - X coordinate * @param {number} y - Y coordinate * @param {number} z - Z coordinate */ updateVoxel(x, y, z) { this.update(); } }