aether-shards/src/grid/VoxelManager.js

316 lines
9.9 KiB
JavaScript

/**
* @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<number, THREE.InstancedMesh>} */
this.meshes = new Map();
// Default Material Definitions (Fallback)
// Store actual Material instances, not just configs
/** @type {Record<number, THREE.MeshStandardMaterial | THREE.MeshStandardMaterial[]>} */
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();
}
}