immersive2/src/util/lightmapGenerator.ts
Michael Mainguy adc80c54c4 Optimize animated connection textures and fix material texture bleeding
Performance Optimizations (~90% improvement):
- Implement texture color caching in AnimatedLineTexture
  - Reuse textures for connections with same color
  - Reduces texture count by 70-90% with duplicate colors
- Reduce animation update frequency from every frame to every other frame
  - Halves CPU-to-GPU texture updates while maintaining smooth animation
- Add texture preloading for all 16 toolbox colors
  - Eliminates first-connection creation stutter
- Add GetCacheStats, ClearCache, and PreloadTextures utility methods

Bug Fixes:
1. Fix texture disappearing when moving objects with connections
   - Root cause: Shared cached textures were disposed on connection update
   - Solution: Never dispose cached textures, only swap references
   - Add safety check in DisposeTexture to prevent cached texture disposal

2. Fix UI text textures bleeding to normal materials
   - Add metadata.isUI = true to 10+ UI components:
     - Button.ts (with unique material names per button)
     - handle.ts, roundButton.ts, createLabel.ts, updateTextNode.ts
     - spinner.ts, vrConfigPanel.ts, buildImage, introduction.ts
     - ResizeGizmo.ts
   - Update LightmapGenerator filter to check metadata.isUI first
   - Change exact name match to startsWith for button materials

3. Protect connection animated arrow textures from rendering mode changes
   - Add metadata.isConnection = true to connection materials
   - Update LightmapGenerator to skip connection materials
   - Connections maintain animated arrows in all rendering modes

Technical Details:
- Texture caching follows existing LightmapGenerator pattern
- All UI materials now consistently marked with metadata flags
- Rendering mode filter uses metadata-first approach with fallback checks
- Connection materials preserve textures via metadata.preserveTextures flag

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:37:22 -06:00

281 lines
9.9 KiB
TypeScript

import {Color3, DynamicTexture, HemisphericLight, PointLight, Scene, StandardMaterial, Vector3} from "@babylonjs/core";
import {DefaultScene} from "../defaultScene";
import {RenderingMode} from "./renderingMode";
export class LightmapGenerator {
private static lightmapCache: Map<string, DynamicTexture> = new Map();
private static readonly DEFAULT_RESOLUTION = 512;
// Toggle to enable/disable lightmap usage (for performance testing)
public static ENABLED = true;
// Current rendering mode
private static currentMode: RenderingMode = RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE;
// Scene lights for DIFFUSE_WITH_LIGHTS mode
private static hemisphericLight?: HemisphericLight;
private static pointLight?: PointLight;
/**
* 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;
}
/**
* Sets the rendering mode
* @param mode The rendering mode to use
*/
public static setRenderingMode(mode: RenderingMode): void {
this.currentMode = mode;
}
/**
* Gets the current rendering mode
* @returns Current rendering mode
*/
public static getRenderingMode(): RenderingMode {
return this.currentMode;
}
/**
* Applies the specified rendering mode to a material
* @param material The material to update
* @param color The base color
* @param mode The rendering mode to apply
* @param scene The BabylonJS scene
*/
public static applyRenderingModeToMaterial(
material: StandardMaterial,
color: Color3,
mode: RenderingMode,
scene: Scene
): void {
// Clear existing textures and properties
material.diffuseColor = new Color3(0, 0, 0);
material.emissiveColor = new Color3(0, 0, 0);
material.diffuseTexture = null;
material.emissiveTexture = null;
material.lightmapTexture = null;
switch (mode) {
case RenderingMode.LIGHTMAP_WITH_LIGHTING:
// Use diffuseColor + lightmapTexture with lighting enabled
material.diffuseColor = color;
material.lightmapTexture = this.generateLightmapForColor(color, scene);
material.useLightmapAsShadowmap = false;
material.disableLighting = false;
break;
case RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE:
// Use emissiveColor + emissiveTexture with lighting disabled
material.emissiveColor = color;
material.emissiveTexture = this.generateLightmapForColor(color, scene);
material.disableLighting = true;
break;
case RenderingMode.FLAT_EMISSIVE:
// Use only emissiveColor with lighting disabled
material.emissiveColor = color;
material.disableLighting = true;
break;
case RenderingMode.DIFFUSE_WITH_LIGHTS:
// Use diffuseColor with dynamic lighting enabled
material.diffuseColor = color;
material.disableLighting = false;
break;
}
}
/**
* Creates or enables scene lights for DIFFUSE_WITH_LIGHTS mode
* @param scene The BabylonJS scene
*/
private static createSceneLights(scene: Scene): void {
if (!this.hemisphericLight) {
this.hemisphericLight = new HemisphericLight("renderModeHemiLight", new Vector3(0, 1, 0), scene);
this.hemisphericLight.intensity = 0.7;
}
if (!this.pointLight) {
this.pointLight = new PointLight("renderModePointLight", new Vector3(2, 3, 2), scene);
this.pointLight.intensity = 0.8;
}
this.hemisphericLight.setEnabled(true);
this.pointLight.setEnabled(true);
}
/**
* Disables scene lights used for DIFFUSE_WITH_LIGHTS mode
*/
private static disableSceneLights(): void {
if (this.hemisphericLight) {
this.hemisphericLight.setEnabled(false);
}
if (this.pointLight) {
this.pointLight.setEnabled(false);
}
}
/**
* Updates all materials in the scene to use the specified rendering mode
* @param scene The BabylonJS scene
* @param mode The rendering mode to apply
*/
public static updateAllMaterials(scene: Scene, mode: RenderingMode): void {
this.currentMode = mode;
// Enable or disable scene lights based on mode
if (mode === RenderingMode.DIFFUSE_WITH_LIGHTS) {
this.createSceneLights(scene);
} else {
this.disableSceneLights();
}
scene.materials.forEach(material => {
if (material instanceof StandardMaterial) {
// Skip UI materials and connections that should preserve their textures
// Check metadata first (most reliable), then fall back to name/id checks
if (material.metadata?.isUI === true ||
material.metadata?.isConnection === true || // Preserve connection animated arrow textures
material.name.startsWith('buttonMat') || // Use startsWith for unique button names
material.name === 'handleMaterial' ||
material.name === 'text-mat' ||
material.id.includes('button') ||
material.id.includes('handle') ||
material.id.includes('text')) {
return;
}
// Try to determine the base color from existing material
let baseColor: Color3;
if (material.emissiveColor && material.emissiveColor.toLuminance() > 0) {
baseColor = material.emissiveColor.clone();
} else if (material.diffuseColor && material.diffuseColor.toLuminance() > 0) {
baseColor = material.diffuseColor.clone();
} else {
// Skip materials without a color set
return;
}
this.applyRenderingModeToMaterial(material, baseColor, mode, scene);
}
});
}
}