Skip to content

反射与幽灵粒子

背景:水面关掉后不恢复;两套反射机制互斥失效。幽灵粒子模拟在 ADR-083 落地后出现性能问题。 过程:TextureFactory 统一贴图创建;PlanarReflectionEngine 统一反射;ghost-particle-sim 性能优化。


一、不会回来的水面

王逸打开环境渲染页面,切换到"地面反射",开。

地面亮了。水面黑了。

他关掉地面反射。水面没有恢复。

他重新打开应用,再试一次。开地面反射——水面灭。关地面反射——水面还是灭的。重启也不恢复。

buildGroundReflection 里有一行 disableWaterReflection()。它销毁了水面的 RenderTargetTexture,把水面的反射混合系数设为零。但 disableGroundReflection 里没有任何恢复逻辑。

"你打开它的时候销毁了水面,"王逸对代码说,"但你关上它的时候,忘了把它找回来。"

这是一个单向门。进去就回不来了。

王逸在 disableGroundReflection 里加了一句 _restoreWaterReflection()。单向门变成了双向门。

但更深层的问题是:水面反射和地面反射用的是两套完全不同的机制。水面是手写 _mirrorCam,地面是 MirrorTexture。两套机制,互不兼容,互相对抗性存在。

修一个 bug,解决不了问题。问题在架构里。


二、十一支画笔

王逸在 env-impl.ts 里搜索 new Texture

十一处。

十一处创建贴图的代码,散落在十一行里。没有工厂,没有复用,没有释放登记。

天空兜底 1×1 toDataURL→Texture。地面 512 canvas→toDataURL→Texture。地面边缘渐隐 256 canvas→toDataURL→Texture。粒子 canvas。焦散 canvas。高度图 canvas。还有五处外部贴图加载。

每一处都是同样的模式:document.createElement('canvas')getContext('2d') → 画 → toDataURL()new Texture()

十一处。没有共享代码。没有复用逻辑。没有释放登记。

王逸想象了一下——如果场景销毁时忘了调一处 dispose,那张 Texture 就永远留在 GPU 内存里。不是内存泄漏,是显存泄漏。更隐蔽,更难发现。

"十一支画笔,"王逸说,"应该只有一个调色盘。"

src/scene/env/texture-factory.tsTextureFactory

同一张贴图被请求两次,第二次直接返回缓存。场景销毁时 disposeAll(),所有贴图一起释放。


三、一套镜子

水面用 RenderTargetTexture 加镜像相机。地面用 MirrorTexture。两套机制互斥,双双失效。

王逸决定两套都删了。换成一套:PlanarReflectionEngine

typescript
class PlanarReflectionEngine {
    private renderTarget: RenderTargetTexture;
    private mirrorCam: FreeCamera;

    constructor(scene, mirrorPlane) {
        this.mirrorCam = new FreeCamera('mirrorCam', Vector3.Zero(), scene);
        this.renderTarget = new RenderTargetTexture('reflection', 1024, scene);
        this.mirrorCam.position = mirrorPlane.normal.scale(-1);
        this.mirrorCam.setTarget(Vector3.Zero());
    }

    update(mainCamera) {
        this.mirrorCam.position = mainCamera.position.clone();
        this.mirrorCam.setTarget(mainCamera.getTarget());
        this.mirrorCam.position.y = -this.mirrorCam.position.y;
    }

    render() {
        this.renderTarget.render();
    }
}

水面用这套。地面用这套。同一套代码,同一个 renderTarget 逻辑。

disableWaterReflection()buildGroundReflection 的互斥逻辑删了。水面和地面可以同时有反射。


四、幽灵粒子

ghost-particle-sim 落地后,王逸注意到一个问题:粒子系统在没有外部风力时,粒子仍然在每一帧做不必要的位移计算。

updateParticles() 函数无条件执行——即使风力为零,即使粒子已经静止。

"粒子停了,但代码还在算。" 王逸说。

他在粒子系统里加了一个静止检测:如果所有粒子的速度都低于阈值,跳过位移计算。

性能提升不明显——只是优化。但"不该算的就不算"是一种态度。


尾声

水面和地面同时闪耀。

十一支画笔收进一个调色盘。

幽灵粒子停了,但代码里的"不该算的就不算"留下了。


教训:两套机制并存时,"没有 bug"不等于"架构正确"。消费型操作(销毁、覆盖)如果不可逆,就是破坏性操作。互斥应该是"切换",不是"永久关闭"。