Implement lightmap-based rendering for performant lighting illusion

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>
This commit is contained in:
Michael Mainguy 2025-11-13 09:20:40 -06:00
parent 100c5e612c
commit 3f02fc7ea5
4 changed files with 140 additions and 11 deletions

View File

@ -18,6 +18,7 @@ import log from "loglevel";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import {xyztovec} from "./vectorConversion"; import {xyztovec} from "./vectorConversion";
import {AnimatedLineTexture} from "../../util/animatedLineTexture"; import {AnimatedLineTexture} from "../../util/animatedLineTexture";
import {LightmapGenerator} from "../../util/lightmapGenerator";
export function buildMeshFromDiagramEntity(entity: DiagramEntity, scene: Scene): AbstractMesh { export function buildMeshFromDiagramEntity(entity: DiagramEntity, scene: Scene): AbstractMesh {
const logger = log.getLogger('buildMeshFromDiagramEntity'); const logger = log.getLogger('buildMeshFromDiagramEntity');
@ -190,11 +191,13 @@ export function buildMissingMaterial(name: string, scene: Scene, color: string):
if (existingMaterial) { if (existingMaterial) {
return (existingMaterial as StandardMaterial); return (existingMaterial as StandardMaterial);
} }
const colorObj = Color3.FromHexString(color);
const newMaterial = new StandardMaterial(name, scene); const newMaterial = new StandardMaterial(name, scene);
newMaterial.id = name; newMaterial.id = name;
newMaterial.emissiveColor = Color3.FromHexString(color); newMaterial.diffuseColor = colorObj;
newMaterial.disableLighting = true; newMaterial.disableLighting = false;
// newMaterial.diffuseColor = Color3.FromHexString(color); newMaterial.lightmapTexture = LightmapGenerator.generateLightmapForColor(colorObj, scene);
newMaterial.useLightmapAsShadowmap = false;
newMaterial.alpha = 1; newMaterial.alpha = 1;
return newMaterial; return newMaterial;
} }

View File

@ -11,21 +11,17 @@ import {
import {enumKeys} from "../../util/functions/enumKeys"; import {enumKeys} from "../../util/functions/enumKeys";
import {ToolType} from "../types/toolType"; import {ToolType} from "../types/toolType";
import {buildTool} from "./buildTool"; import {buildTool} from "./buildTool";
import {LightmapGenerator} from "../../util/lightmapGenerator";
export async function buildColor(color: Color3, scene: Scene, parent: TransformNode, index: number, toolMap: Map<string, AbstractMesh>): Promise<Node> { export async function buildColor(color: Color3, scene: Scene, parent: TransformNode, index: number, toolMap: Map<string, AbstractMesh>): Promise<Node> {
const width = .1; const width = .1;
const height = .1; const height = .1;
const material = new StandardMaterial("material-" + color.toHexString(), scene); const material = new StandardMaterial("material-" + color.toHexString(), scene);
material.emissiveColor = color;
material.diffuseColor = color; material.diffuseColor = color;
material.disableLighting = true; material.disableLighting = false;
// material.diffuseColor = color; material.lightmapTexture = LightmapGenerator.generateLightmapForColor(color, scene);
// material.ambientColor = color; material.useLightmapAsShadowmap = false;
//material.roughness = 1;
material.specularPower = 64; material.specularPower = 64;
// material.ambientColor = color;
//material.roughness = .1;
//material.maxSimultaneousLights = 2;
const colorBoxMesh = MeshBuilder.CreatePlane("toolbox-color-" + color.toHexString(), { const colorBoxMesh = MeshBuilder.CreatePlane("toolbox-color-" + color.toHexString(), {
width: width, width: width,

View File

@ -4,6 +4,7 @@ import log from "loglevel";
import {Handle} from "../objects/handle"; import {Handle} from "../objects/handle";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {Button} from "../objects/Button"; import {Button} from "../objects/Button";
import {LightmapGenerator} from "../util/lightmapGenerator";
const colors: string[] = [ const colors: string[] = [
"#222222", "#8b4513", "#006400", "#778899", "#222222", "#8b4513", "#006400", "#778899",
@ -27,6 +28,10 @@ export class Toolbox {
this._handle = new Handle(this._toolboxBaseNode, 'Toolbox'); this._handle = new Handle(this._toolboxBaseNode, 'Toolbox');
this._toolboxBaseNode.position.y = .2; this._toolboxBaseNode.position.y = .2;
this._toolboxBaseNode.scaling = new Vector3(0.6, 0.6, 0.6); 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(() => { this.buildToolbox().then(() => {
readyObservable.notifyObservers(true); readyObservable.notifyObservers(true);
this._logger.info('Toolbox built'); this._logger.info('Toolbox built');

View File

@ -0,0 +1,125 @@
import {Color3, DynamicTexture, Scene} from "@babylonjs/core";
import {DefaultScene} from "../defaultScene";
export class LightmapGenerator {
private static lightmapCache: Map<string, DynamicTexture> = new Map();
private static readonly DEFAULT_RESOLUTION = 512;
/**
* Generates or retrieves cached lightmap for a given color
* @param color The base color for the lightmap
* @param scene The BabylonJS scene
* @param resolution Texture resolution (default: 512)
* @returns DynamicTexture with baked lighting
*/
public static generateLightmapForColor(
color: Color3,
scene: Scene,
resolution: number = LightmapGenerator.DEFAULT_RESOLUTION
): DynamicTexture {
const colorKey = color.toHexString();
// Return cached lightmap if available
if (this.lightmapCache.has(colorKey)) {
return this.lightmapCache.get(colorKey)!;
}
// Create new lightmap
const lightmap = this.createLightmap(color, scene, resolution);
this.lightmapCache.set(colorKey, lightmap);
return lightmap;
}
/**
* Pre-generates lightmaps for all specified colors
* Call during initialization for better first-render performance
* @param colors Array of hex color strings
* @param scene The BabylonJS scene
*/
public static preloadLightmaps(colors: string[], scene: Scene): void {
colors.forEach(colorHex => {
const color = Color3.FromHexString(colorHex);
this.generateLightmapForColor(color, scene);
});
}
/**
* Creates a lightmap texture with simulated lighting
* Uses radial gradient to simulate top-left directional light
* @param color Base color
* @param scene BabylonJS scene
* @param resolution Texture size
* @returns DynamicTexture with baked lighting gradient
*/
private static createLightmap(
color: Color3,
scene: Scene,
resolution: number
): DynamicTexture {
const texture = new DynamicTexture(
`lightmap-${color.toHexString()}`,
resolution,
scene,
false // generateMipMaps
);
const ctx = texture.getContext();
const canvas = ctx.canvas.getContext('2d') as CanvasRenderingContext2D;
// Create radial gradient simulating directional light from top-left
// Offset the gradient center to create directional effect
const centerX = resolution * 0.4; // Offset left
const centerY = resolution * 0.4; // Offset up
const radius = resolution * 0.8; // Larger radius for smoother falloff
const gradient = canvas.createRadialGradient(
centerX, centerY, 0,
centerX, centerY, radius
);
// Calculate lit and shadow colors
// Lit area: 1.5x brighter than base color (clamped to 1.0)
const litColor = new Color3(
Math.min(color.r * 1.5, 1.0),
Math.min(color.g * 1.5, 1.0),
Math.min(color.b * 1.5, 1.0)
);
// Shadow area: 0.3x darker than base color
const shadowColor = color.scale(0.3);
// Mid-tone: base color unchanged
const midColor = color;
// Build gradient with multiple stops for smoother transition
gradient.addColorStop(0, litColor.toHexString()); // Center: bright
gradient.addColorStop(0.5, midColor.toHexString()); // Mid: base color
gradient.addColorStop(1.0, shadowColor.toHexString()); // Edge: dark
// Fill canvas with gradient
canvas.fillStyle = gradient;
canvas.fillRect(0, 0, resolution, resolution);
// Update texture with canvas content
texture.update();
return texture;
}
/**
* Clears the lightmap cache
* Useful for memory management or when regenerating lightmaps
*/
public static clearCache(): void {
this.lightmapCache.forEach(texture => texture.dispose());
this.lightmapCache.clear();
}
/**
* Gets the current cache size
* @returns Number of cached lightmaps
*/
public static getCacheSize(): number {
return this.lightmapCache.size;
}
}