2025-12-16 23:52:58 +00:00
|
|
|
import * as THREE from "three";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* VoxelManager.js
|
2025-12-17 19:26:42 +00:00
|
|
|
* Handles the Three.js rendering of the VoxelGrid data.
|
2025-12-19 05:19:22 +00:00
|
|
|
* Updated to support Camera Focus Targeting, Emissive Textures, and Multi-Material Voxels.
|
2025-12-16 23:52:58 +00:00
|
|
|
*/
|
|
|
|
|
export class VoxelManager {
|
2025-12-19 05:19:22 +00:00
|
|
|
constructor(grid, scene) {
|
2025-12-16 23:52:58 +00:00
|
|
|
this.grid = grid;
|
|
|
|
|
this.scene = scene;
|
2025-12-19 05:19:22 +00:00
|
|
|
|
|
|
|
|
// Map of Voxel ID -> InstancedMesh
|
|
|
|
|
this.meshes = new Map();
|
|
|
|
|
|
|
|
|
|
// Default Material Definitions (Fallback)
|
|
|
|
|
// Store actual Material instances, not just configs
|
|
|
|
|
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
|
2025-12-17 19:26:42 +00:00
|
|
|
};
|
2025-12-19 05:19:22 +00:00
|
|
|
|
|
|
|
|
// Shared Geometry
|
|
|
|
|
this.geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
|
|
|
|
|
|
|
|
// Camera Anchor: Invisible object to serve as OrbitControls target
|
|
|
|
|
this.focusTarget = new THREE.Object3D();
|
|
|
|
|
this.focusTarget.name = "CameraFocusTarget";
|
|
|
|
|
this.scene.add(this.focusTarget);
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 19:26:42 +00:00
|
|
|
/**
|
2025-12-19 05:19:22 +00:00
|
|
|
* 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.
|
2025-12-17 19:26:42 +00:00
|
|
|
*/
|
2025-12-19 05:19:22 +00:00
|
|
|
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)
|
|
|
|
|
];
|
2025-12-17 19:26:42 +00:00
|
|
|
}
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
// 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];
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-17 19:26:42 +00:00
|
|
|
this.update();
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 05:19:22 +00:00
|
|
|
* Re-calculates meshes based on grid data.
|
2025-12-16 23:52:58 +00:00
|
|
|
*/
|
|
|
|
|
update() {
|
2025-12-19 05:19:22 +00:00
|
|
|
// 1. Cleanup existing meshes
|
|
|
|
|
this.meshes.forEach((mesh) => {
|
|
|
|
|
this.scene.remove(mesh);
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
// 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();
|
|
|
|
|
}
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
mesh.dispose();
|
|
|
|
|
});
|
|
|
|
|
this.meshes.clear();
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
// 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);
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
if (id !== 0) {
|
|
|
|
|
if (!buckets[id]) buckets[id] = [];
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
dummy.position.set(x, y, z);
|
2025-12-17 19:26:42 +00:00
|
|
|
dummy.updateMatrix();
|
2025-12-19 05:19:22 +00:00
|
|
|
buckets[id].push(dummy.matrix.clone());
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
// 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
|
|
|
|
|
);
|
2025-12-17 19:26:42 +00:00
|
|
|
}
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-17 19:26:42 +00:00
|
|
|
/**
|
2025-12-19 05:19:22 +00:00
|
|
|
* Helper to center the camera view on the grid.
|
|
|
|
|
* @param {Object} controls - The OrbitControls instance to update
|
2025-12-17 19:26:42 +00:00
|
|
|
*/
|
2025-12-19 05:19:22 +00:00
|
|
|
focusCamera(controls) {
|
|
|
|
|
if (controls && this.focusTarget) {
|
|
|
|
|
controls.target.copy(this.focusTarget.position);
|
|
|
|
|
controls.update();
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|
2025-12-19 05:19:22 +00:00
|
|
|
}
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
updateVoxel(x, y, z) {
|
|
|
|
|
this.update();
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|
|
|
|
|
}
|