Replace emissive-only rendering with diffuse + lightmap system to achieve realistic lighting appearance without dynamic light overhead. - Create LightmapGenerator class with canvas-based radial gradient generation - Generate one lightmap per color (16 total) using top-left directional light simulation - Cache lightmaps in static Map for reuse across all instances - Preload all lightmaps at toolbox initialization for instant availability - Update buildColor() to use diffuseColor + lightmapTexture instead of emissiveColor - Update buildMissingMaterial() to use lightmap-based rendering - Enable lighting calculations (disableLighting = false) to apply lightmaps Lightmap details: - 512x512 resolution RGBA textures - Radial gradient: center (color × 1.5), mid (base color), edge (color × 0.3) - Simulates top-left key light with smooth falloff - Total memory: ~16 MB for all lightmaps - Zero per-frame performance cost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
173 lines
6.3 KiB
TypeScript
173 lines
6.3 KiB
TypeScript
import {AbstractMesh, Color3, InstancedMesh, Node, Observable, Scene, TransformNode, Vector3, WebXRDefaultExperience} from "@babylonjs/core";
|
|
import {buildColor} from "./functions/buildColor";
|
|
import log from "loglevel";
|
|
import {Handle} from "../objects/handle";
|
|
import {DefaultScene} from "../defaultScene";
|
|
import {Button} from "../objects/Button";
|
|
import {LightmapGenerator} from "../util/lightmapGenerator";
|
|
|
|
const colors: string[] = [
|
|
"#222222", "#8b4513", "#006400", "#778899",
|
|
"#4b0082", "#ff0000", "#ffa500", "#ffff00",
|
|
"#00ff00", "#00ffff", "#0000ff", "#ff00ff",
|
|
"#1e90ff", "#98fb98", "#ffe4b5", "#ff69b4"
|
|
]
|
|
|
|
|
|
export class Toolbox {
|
|
public readonly _toolboxBaseNode: TransformNode;
|
|
private readonly _tools: Map<string, InstancedMesh> = new Map<string, InstancedMesh>();
|
|
private readonly _logger = log.getLogger('Toolbox');
|
|
private readonly _handle: Handle;
|
|
private readonly _scene: Scene;
|
|
private _xr?: WebXRDefaultExperience;
|
|
|
|
constructor(readyObservable: Observable<boolean>) {
|
|
this._scene = DefaultScene.Scene;
|
|
this._toolboxBaseNode = new TransformNode("toolbox", this._scene);
|
|
this._handle = new Handle(this._toolboxBaseNode, 'Toolbox');
|
|
this._toolboxBaseNode.position.y = .2;
|
|
this._toolboxBaseNode.scaling = new Vector3(0.6, 0.6, 0.6);
|
|
|
|
// Preload lightmaps for all toolbox colors for better first-render performance
|
|
LightmapGenerator.preloadLightmaps(colors, this._scene);
|
|
|
|
this.buildToolbox().then(() => {
|
|
readyObservable.notifyObservers(true);
|
|
this._logger.info('Toolbox built');
|
|
});
|
|
Toolbox._instance = this;
|
|
}
|
|
|
|
public setXR(xr: WebXRDefaultExperience): void {
|
|
this._xr = xr;
|
|
this.setupXRButton();
|
|
}
|
|
private index = 0;
|
|
private colorPicker: TransformNode;
|
|
private changing = false;
|
|
|
|
public static _instance: Toolbox;
|
|
|
|
public static get instance() {
|
|
return Toolbox._instance;
|
|
}
|
|
|
|
public get handleMesh(): TransformNode {
|
|
return this._handle.transformNode;
|
|
}
|
|
|
|
public isTool(mesh: AbstractMesh) {
|
|
return this._tools.has(mesh.id);
|
|
}
|
|
|
|
private async buildToolbox() {
|
|
this.setupPointerObservable();
|
|
await this.buildColorPicker();
|
|
if (this._toolboxBaseNode.parent) {
|
|
const platform = this._scene.getMeshById("platform");
|
|
if (platform) {
|
|
this.assignHandleParentAndStore(platform);
|
|
} else {
|
|
const observer = this._scene.onNewMeshAddedObservable.add((mesh: AbstractMesh) => {
|
|
if (mesh && mesh.id == "platform") {
|
|
this.assignHandleParentAndStore(mesh);
|
|
this._scene.onNewMeshAddedObservable.remove(observer);
|
|
}
|
|
}, -1, false, this, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private setupPointerObservable() {
|
|
this._scene.onPointerObservable.add((pointerInfo) => {
|
|
const pickedMesh = pointerInfo?.pickInfo?.pickedMesh;
|
|
if (pointerInfo.type == 1 &&
|
|
pickedMesh?.metadata?.tool == 'color') {
|
|
if (this.changing) {
|
|
this._logger.debug('changing');
|
|
this.colorPicker.setEnabled(true);
|
|
return;
|
|
} else {
|
|
const active = pickedMesh?.parent.getChildren(this.nodePredicate, true);
|
|
for (const node of active) {
|
|
node.setEnabled(false);
|
|
}
|
|
const nodes = pickedMesh?.metadata?.tools;
|
|
if (nodes) {
|
|
for (const node of nodes) {
|
|
this._scene.getNodeById(node)?.setEnabled(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private nodePredicate = (node: Node) => {
|
|
return node.getClassName() == "InstancedMesh" &&
|
|
node.isEnabled(false) == true
|
|
};
|
|
|
|
private async buildColorPicker() {
|
|
let initial = true;
|
|
const colorArray: Promise<Node>[] = [];
|
|
for (const c of colors) {
|
|
colorArray.push(buildColor(Color3.FromHexString(c), this._scene, this._toolboxBaseNode, this.index++, this._tools));
|
|
/*if (initial) {
|
|
initial = false;
|
|
for (const id of cnode.metadata.tools) {
|
|
this._scene.getNodeById(id)?.setEnabled(true);
|
|
}
|
|
|
|
}*/
|
|
}
|
|
const out = await Promise.all(colorArray);
|
|
for (const id of out[0].metadata.tools) {
|
|
this._scene.getNodeById(id)?.setEnabled(true);
|
|
}
|
|
}
|
|
|
|
private assignHandleParentAndStore(mesh: TransformNode) {
|
|
const offset = new Vector3(-.50, 1.6, .38);
|
|
const rotation = new Vector3(.5, -.6, .18);
|
|
|
|
const handle = this._handle;
|
|
handle.transformNode.parent = mesh;
|
|
if (!handle.idStored) {
|
|
handle.transformNode.position = offset;
|
|
handle.transformNode.rotation = rotation;
|
|
}
|
|
|
|
}
|
|
|
|
private setupXRButton() {
|
|
if (!this._xr) {
|
|
this._logger.warn('XR not available, exit XR button will not be created');
|
|
return;
|
|
}
|
|
|
|
this._xr.baseExperience.onStateChangedObservable.add((state) => {
|
|
if (state == 2) { // WebXRState.IN_XR
|
|
const button = Button.CreateButton("exitXr", "exitXr", this._scene, {});
|
|
|
|
// Position button at bottom-right of toolbox, matching handle size and orientation
|
|
button.transform.position.x = 0.5; // Right side
|
|
button.transform.position.y = -0.35; // Below color grid
|
|
button.transform.position.z = 0; // Coplanar with toolbox
|
|
button.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly
|
|
button.transform.scaling = new Vector3(.2, .2, .2); // Match handle height
|
|
button.transform.parent = this._toolboxBaseNode;
|
|
|
|
button.onPointerObservable.add((evt) => {
|
|
this._logger.debug(evt);
|
|
if (evt.sourceEvent.type == 'pointerdown') {
|
|
this._xr.baseExperience.exitXRAsync();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|