From 3f02fc7ea5414a316ed4144ba12749d867eea83b Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Thu, 13 Nov 2025 09:20:40 -0600 Subject: [PATCH] Implement lightmap-based rendering for performant lighting illusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../functions/buildMeshFromDiagramEntity.ts | 9 +- src/toolbox/functions/buildColor.ts | 12 +- src/toolbox/toolbox.ts | 5 + src/util/lightmapGenerator.ts | 125 ++++++++++++++++++ 4 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 src/util/lightmapGenerator.ts diff --git a/src/diagram/functions/buildMeshFromDiagramEntity.ts b/src/diagram/functions/buildMeshFromDiagramEntity.ts index ac8fe86..71e6c05 100644 --- a/src/diagram/functions/buildMeshFromDiagramEntity.ts +++ b/src/diagram/functions/buildMeshFromDiagramEntity.ts @@ -18,6 +18,7 @@ import log from "loglevel"; import {v4 as uuidv4} from 'uuid'; import {xyztovec} from "./vectorConversion"; import {AnimatedLineTexture} from "../../util/animatedLineTexture"; +import {LightmapGenerator} from "../../util/lightmapGenerator"; export function buildMeshFromDiagramEntity(entity: DiagramEntity, scene: Scene): AbstractMesh { const logger = log.getLogger('buildMeshFromDiagramEntity'); @@ -190,11 +191,13 @@ export function buildMissingMaterial(name: string, scene: Scene, color: string): if (existingMaterial) { return (existingMaterial as StandardMaterial); } + const colorObj = Color3.FromHexString(color); const newMaterial = new StandardMaterial(name, scene); newMaterial.id = name; - newMaterial.emissiveColor = Color3.FromHexString(color); - newMaterial.disableLighting = true; - // newMaterial.diffuseColor = Color3.FromHexString(color); + newMaterial.diffuseColor = colorObj; + newMaterial.disableLighting = false; + newMaterial.lightmapTexture = LightmapGenerator.generateLightmapForColor(colorObj, scene); + newMaterial.useLightmapAsShadowmap = false; newMaterial.alpha = 1; return newMaterial; } \ No newline at end of file diff --git a/src/toolbox/functions/buildColor.ts b/src/toolbox/functions/buildColor.ts index 0bffeaf..1ceb69b 100644 --- a/src/toolbox/functions/buildColor.ts +++ b/src/toolbox/functions/buildColor.ts @@ -11,21 +11,17 @@ import { import {enumKeys} from "../../util/functions/enumKeys"; import {ToolType} from "../types/toolType"; import {buildTool} from "./buildTool"; +import {LightmapGenerator} from "../../util/lightmapGenerator"; export async function buildColor(color: Color3, scene: Scene, parent: TransformNode, index: number, toolMap: Map): Promise { const width = .1; const height = .1; const material = new StandardMaterial("material-" + color.toHexString(), scene); - material.emissiveColor = color; material.diffuseColor = color; - material.disableLighting = true; - // material.diffuseColor = color; - // material.ambientColor = color; - //material.roughness = 1; + material.disableLighting = false; + material.lightmapTexture = LightmapGenerator.generateLightmapForColor(color, scene); + material.useLightmapAsShadowmap = false; material.specularPower = 64; - // material.ambientColor = color; - //material.roughness = .1; - //material.maxSimultaneousLights = 2; const colorBoxMesh = MeshBuilder.CreatePlane("toolbox-color-" + color.toHexString(), { width: width, diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 0d8a191..2fa9490 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -4,6 +4,7 @@ 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", @@ -27,6 +28,10 @@ export class Toolbox { 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'); diff --git a/src/util/lightmapGenerator.ts b/src/util/lightmapGenerator.ts new file mode 100644 index 0000000..07402f1 --- /dev/null +++ b/src/util/lightmapGenerator.ts @@ -0,0 +1,125 @@ +import {Color3, DynamicTexture, Scene} from "@babylonjs/core"; +import {DefaultScene} from "../defaultScene"; + +export class LightmapGenerator { + private static lightmapCache: Map = 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; + } +}