immersive2/src/objects/spinner.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

111 lines
3.5 KiB
TypeScript

import {
AbstractMesh,
Animation,
DynamicTexture,
GPUParticleSystem,
MeshBuilder,
ParticleSystem,
Scene,
SphereParticleEmitter,
StandardMaterial,
Texture,
Vector3
} from "@babylonjs/core";
import {DefaultScene} from "../defaultScene";
export class Spinner {
private readonly _scene: Scene;
private spinner: AbstractMesh;
private particleSystem: ParticleSystem;
constructor() {
this._scene = DefaultScene.Scene;
this.build();
}
public get position(): Vector3 {
return this.spinner.position;
}
public show() {
if (!this.spinner || !this.particleSystem) {
this.build();
}
this.spinner.setEnabled(true);
this.particleSystem.start();
}
public hide() {
this.spinner.setEnabled(false);
this.particleSystem.stop(true);
this.spinner.dispose();
this.particleSystem.dispose();
this.spinner = null;
this.particleSystem = null;
}
private build() {
const camera = this._scene.activeCamera;
if (!camera) {
return;
}
const spinner: AbstractMesh = MeshBuilder.CreateSphere("spinner", {diameter: .5}, this._scene);
const material = new StandardMaterial("spinner", this._scene);
material.metadata = { isUI: true }; // Mark as UI to prevent rendering mode modifications
const text = new DynamicTexture("spinner", {width: 1024, height: 1024}, this._scene, false);
text.drawText("Please Wait", 250, 500, "bold 150px Segoe UI", "white", "transparent", true, true);
spinner.rotation.z = Math.PI;
material.emissiveTexture = text;
material.emissiveColor.set(.5, .5, 0);
material.disableLighting = true;
// material.diffuseTexture = text;
// material.diffuseColor.set(.5, .5, 0);
const rotate = new Animation("rotate", "rotation.y", 10,
Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
const keys = [];
keys.push({
frame: 0,
value: 0
});
keys.push({
frame: 30,
value: Math.PI * 2
});
rotate.setKeys(keys);
spinner.animations.push(rotate);
this._scene.beginAnimation(spinner, 0, 30, true);
material.alpha = .9;
spinner.material = material;
let particleSystem;
if (GPUParticleSystem.IsSupported) {
particleSystem = new GPUParticleSystem("particles", {capacity: 1024}, this._scene);
particleSystem.activeParticleCount = 512;
} else {
particleSystem = new ParticleSystem("particles", 512, this._scene);
}
particleSystem.emitRate = 512;
const emitter = new SphereParticleEmitter(.2);
emitter.radiusRange = .1;
particleSystem.particleEmitterType = emitter;
particleSystem.particleTexture = new Texture("/assets/textures/flare.png", this._scene);
particleSystem.minEmitPower = .1;
particleSystem.maxEmitPower = .25;
particleSystem.minLifeTime = .1;
particleSystem.maxLifeTime = .8;
particleSystem.minSize = 0.01;
particleSystem.maxSize = 0.05;
particleSystem.emitter = spinner;
particleSystem.parent = spinner;
const ray = camera.position.add(camera.getForwardRay(1).direction.scale(2));
spinner.position = ray;
this.spinner = spinner;
this.spinner.setEnabled(false);
this.particleSystem = particleSystem;
}
}