在 3D 编辑器开发中,镜面反射是一个既常见又充满挑战的功能。最近我实现了 MirrorReflectionBehaviorEditor,一个基于 Babylon.js 的镜面反射行为编辑器。本文将深入剖析其核心实现,重点讲解 MirrorTexture 的创建过程 和 Transform 改变的检测机制,并分享开发中的关键思考。
一、功能概述
MirrorReflectionBehaviorEditor 的主要功能是为任意网格(Mesh)添加实时镜面反射效果。它会:
-
自动检测网格平整度:确保只有平坦的表面才能生成正确的镜面反射
-
动态创建反射纹理 :使用 Babylon.js 的
MirrorTexture实时渲染反射场景 -
自适应变换更新:当物体移动、旋转或缩放时,自动更新反射平面
-
预览模式切换:支持编辑器中实时预览开关,不影响原始材质
核心亮点:整个系统完全响应式,所有参数修改都通过命令模式(Command Pattern)实现可撤销/重做。
二、MirrorTexture 创建过程详解
这是整个行为的核心。创建过程在 _initializeMirrorTexture() 方法中完成,涉及多个关键步骤:
1. 资源清理与准备
TypeScript
// 清理旧的镜面纹理
if (this._mirrorTexture) {
this._mirrorTexture.dispose();
this._mirrorTexture = null;
}
每次重新初始化时,必须先释放旧纹理,避免内存泄漏。这是 Babylon.js 开发的最佳实践。
2. 纹理实例化
TypeScript
this._mirrorTexture = new MirrorTexture(
`mirrorTexture_${this._mesh.name}`,
this._textureSize,
this.scene!,
false // 不生成 mipmap(性能考虑)
);
关键参数说明:
-
name: 唯一标识,便于调试
-
size: 纹理分辨率,支持 128/256/512/1024/2048 动态切换
-
scene: 目标场景
-
generateMipMaps: 关闭 mipmap 可显著提升性能,对镜面反射效果影响极小
3. 反射平面计算
TypeScript
this._updateReflectionPlane();
这一步调用专门的方法计算数学意义上的反射平面,后续详细讲解。
4. 动态模糊设置
TypeScript
this._setBlurKernel(this._blurKernel);
根据纹理尺寸动态调整模糊核大小,确保不同分辨率下模糊效果一致:
TypeScript
const kernel = blurKernel * this._textureSize / 1024;
this._mirrorTexture.blurKernelX = kernel;
this._mirrorTexture.blurKernelY = kernel;
5. 渲染列表管理
TypeScript
const allMeshes = this.scene.meshes.filter(m => m !== this._mesh && m.isEnabled());
this._mirrorTexture.renderList = allMeshes;
// 动态监听场景网格变化
this._sceneAddNewMeshObserver = this.scene.onNewMeshAddedObservable.add((m: AbstractMesh) => {
if(this._mirrorTexture && this._mirrorTexture.renderList && this._mirrorTexture.renderList.indexOf(m) === -1){
this._mirrorTexture.renderList?.push(m);
}
});
这是性能优化的关键:只渲染启用中的网格,且动态维护渲染列表,避免每次帧更新时重复计算。
6. 原始材质备份
TypeScript
this._applyMirrorTexture();
不直接替换材质,而是只修改 environmentTexture 属性,保留材质的其他所有设置。这种非破坏性修改是编辑器架构的核心理念。
三、Transform 改变检测机制
当物体在场景中移动、旋转或缩放时,反射平面必须实时更新。但每帧都重新计算会造成性能浪费,因此需要精确的变更检测。
1. 变更检测的核心逻辑
在 _updatePreview() 方法中实现:
TypeScript
protected _updatePreview(): void {
super._updatePreview();
if (this._isValid && this.enabled && this._mesh) {
// 检测材质是否被更换
if (this._mesh.material && this._lastAppliedMaterial && this._lastAppliedMaterial !== this._mesh.material) {
this._applyMirrorTexture();
}
// 检测变换是否改变
if (this._hasTransformChanged()) {
this._updateReflectionPlane();
}
}
}
2. 矩阵比较算法
_hasTransformChanged() 是检测的核心,它比较世界矩阵的变换:
TypeScript
private _hasTransformChanged(): boolean {
if (!this._mesh) return false;
const currentWorldMatrix = this._mesh.getWorldMatrix();
if (!this._lastWorldMatrix) {
this._lastWorldMatrix = new Float32Array(currentWorldMatrix.asArray());
return false;
}
const current = currentWorldMatrix.asArray();
const last = this._lastWorldMatrix;
// 只比较前15个元素(排除齐次坐标)
for (let i = 0; i < 15; i++) {
if (Math.abs(current[i] - last[i]) > 0.0001) {
this._lastWorldMatrix.set(current);
return true;
}
}
return false;
}
关键设计点:
-
惰性初始化:首次检测时保存矩阵,不触发更新
-
精确到小数点后4位:过滤浮点误差,避免微抖动引起的误判
-
只比较关键元素:索引 0-14 涵盖旋转、缩放、位移,索引 15(齐次坐标)恒为 1,无需比较
-
立即更新缓存 :检测到变化后,立即用
set()方法更新 Float32Array,避免创建新数组
3. 反射平面更新
当检测到变换改变时,调用 _updateReflectionPlane():
TypeScript
private _updateReflectionPlane(): void {
if (!this._mesh || !this._overallNormal || !this._mirrorTexture) {
return;
}
const boundingInfo = this._mesh.getBoundingBox();
const center = boundingInfo.boundingBox.center;
const worldMatrix = this._mesh.getWorldMatrix();
const worldCenter = Vector3.TransformCoordinates(center, worldMatrix);
const worldNormal = Vector3.TransformNormal(this._overallNormal, worldMatrix);
worldNormal.normalize();
// Babylon.js 平面方程:ax + by + cz + d = 0
// 注意:MirrorTexture 需要反向法线
const mirrorNormal = worldNormal.scale(-1);
const d = -Vector3.Dot(mirrorNormal, worldCenter);
this._reflectionPlane = new Plane(mirrorNormal.x, mirrorNormal.y, mirrorNormal.z, d);
this._mirrorTexture.mirrorPlane = this._reflectionPlane;
}
数学原理:
-
TransformCoordinates: 将局部坐标系中心点转换到世界坐标系
-
TransformNormal: 将法向量转换到世界坐标系(不受平移影响)
-
法线反向 :Babylon.js 的
MirrorTexture要求法线指向被反射的空间,因此需要取反 -
平面方程 :通过点法式
(normal, d)构造平面,确保反射几何正确
四、其他关键设计
1. 平整度检测
在 _checkFlatnessAndInitialize() 中,通过计算所有顶点法线与整体法线的最大夹角,确保表面足够平坦。这是避免反射扭曲的保障机制。
2. 预览模式与非破坏性编辑
通过备份 environmentTexture,实现预览开关时不销毁资源:
TypeScript
private _restoreOriginalTexture(): void {
if (this._mesh && this._mesh.material instanceof PBRMetallicRoughnessMaterial) {
this._mesh.material.environmentTexture = this._originalEnvironmentTexture;
}
}
3. 命令模式集成
所有参数修改通过 CmdProperty 包装,确保编辑器可撤销:
TypeScript
public cmdSetFlatnessThreshold(threshold: number): void {
threshold = Scalar.Clamp(threshold, this.minFlatnessThreshold, this.maxFlatnessThreshold);
if(Math.abs(threshold - this._flatnessThreshold) < 0.00001) return;
const cmd = new CmdProperty<number>(
this._setFlatnessThreshold.bind(this),
threshold,
this._flatnessThreshold
);
cmdEmitter.emit('execute', cmd);
}
五、完整代码
以下是三个相关文件的完整代码,无任何省略:
BaseBehaviorEditor.ts
TypeScript
import { Observable, Observer, Scene, TransformNode, type Behavior, type Nullable } from "@babylonjs/core";
import { cmdEmitter } from "../../../utils/EventBus";
import CmdProperty from "../../../Command/CmdProperty";
export default class BaseBehaviorEditor implements Behavior<TransformNode> {
private _uuid:string;
public get uuid(): string{return this._uuid;}
constructor() {
this._uuid = crypto.randomUUID();
}
/**
* 设置行为的 UUID(仅用于反序列化)
* ⚠️ 警告:此方法仅应在从 JSON 反序列化时调用,以保持引用关系的一致性
* 在正常创建行为时,UUID 由构造函数自动生成
* @param uuid 要设置的 UUID
*/
protected _setUUID(uuid: string): void {
this._uuid = uuid;
}
private _scene:Scene | null = null;
public get scene(): Scene | null { return this._scene; }
//
private _target: TransformNode | null = null;
public get target(): TransformNode | null { return this._target; }
//
private _enabled: boolean = true;
public get enabled(): boolean { return this._enabled; }
//
private _isPreview: boolean = false;
public get isPreview(): boolean { return this._isPreview; }
// 记录原来启用时的预览状态,用于在禁用后重新启用时恢复预览状态
private _isPreviewOnEnabled: boolean = false;
//
get name(): string { return "DriveBehaviorEditor"; }
public readonly cnName: string = "驱动行为";
init(): void {}
private _beforeRenderObserver:Nullable<Observer<Scene>> = null;
attach(target: TransformNode): void {
this._target = target;
this._scene = this._target.getScene();
this._isPreviewOnEnabled = this._isPreview;
if(this.scene){
this._beforeRenderObserver = this.scene.onBeforeRenderObservable.add(this._updatePreview.bind(this));
}
}
public readonly onDetachObservable = new Observable<void>();
detach(): void {
// 先移除渲染观察者(此时 scene 还存在)
if(this._beforeRenderObserver){
this.scene?.onBeforeRenderObservable.remove(this._beforeRenderObserver);
this._beforeRenderObserver = null;
}
// 通知观察者分离事件(此时场景和目标还未置空)
this.onDetachObservable.notifyObservers();
// 清空可观察对象
this.onSetPreviewObservable.clear();
this.onDetachObservable.clear();
// 最后置空引用
this._target = null;
this._scene = null;
}
protected _updatePreview(): void {}
public readonly onSetEnabledObservable = new Observable<boolean>();
public setEnabled(enabled: boolean): void {
if (this._enabled === enabled)return;
this._enabled = enabled;
if(this._enabled){
if(this._isPreviewOnEnabled){
this.setPreview(true);
}
}
else{
this._isPreviewOnEnabled = this._isPreview;
if(this._isPreview){
this.setPreview(false);
}
}
if(enabled){
if(this.scene){
this._beforeRenderObserver = this.scene.onBeforeRenderObservable.add(this._updatePreview.bind(this));
}
}
else{
if(this._beforeRenderObserver){
this.scene?.onBeforeRenderObservable.remove(this._beforeRenderObserver);
this._beforeRenderObserver = null;
}
}
this.onSetEnabledObservable.notifyObservers(enabled);
}
//
public cmdSetPreview(isPreview: boolean): void {
if(!this._enabled)return;
if(this._isPreview === isPreview)return;
const cmd = new CmdProperty<boolean>(
this.setPreview.bind(this),
isPreview,
this._isPreview);
cmdEmitter.emit('execute', cmd);
}
public readonly onSetPreviewObservable = new Observable<boolean>();
public setPreview(isPreview: boolean): void {
if(this._isPreview === isPreview)return;
this._isPreview = isPreview;
this.onSetPreviewObservable.notifyObservers(isPreview);
}
public getCopy(): BaseBehaviorEditor {
return new BaseBehaviorEditor();
}
public getJson(): string {
return '';
}
public setByJson(json:string){
console.log(json);
}
}
GeneralBehaviorEditor.ts
TypeScript
import BaseBehaviorEditor from "../BaseBehaviorEditor";
import type { GeneralType } from "./NodeGeneralBehavManagerEditor";
export default class GeneralBehaviorEditor extends BaseBehaviorEditor {
public readonly generalType: GeneralType = "Base";
}
MirrorReflectionBehaviorEditor.ts
TypeScript
import { TransformNode, Mesh, MirrorTexture, Plane, Vector3, Material, PBRMetallicRoughnessMaterial, Observable, BaseTexture, type Nullable, AbstractMesh, Observer, Scalar } from "@babylonjs/core";
import CmdProperty from "../../../../Command/CmdProperty";
import { cmdEmitter } from "../../../../utils/EventBus";
import { DTO_MirrorReflectionEditor } from "../../../../../../Shared/TScripts/DTO/DTO_EditorSystem";
import { DTO_MirrorReflection } from "../../../../../../Shared/TScripts/DTO/DTO_RuntimeSystem";
import GeneralBehaviorEditor from "./GeneralBehaviorEditor";
import type { GeneralType } from "./NodeGeneralBehavManagerEditor";
// 镜面反射行为,使用 MirrorTexture 实现
export default class MirrorReflectionBehaviorEditor extends GeneralBehaviorEditor {
// 必须实现:行为的唯一标识名称(用于注册表)
public static readonly behaviorName = "MirrorReflectionBehaviorEditor";
public readonly generalType: GeneralType = "EnvTex";
// 必须实现:创建行为实例的工厂方法
public static createInstance(): MirrorReflectionBehaviorEditor {
return new MirrorReflectionBehaviorEditor();
}
// 重写 name 属性
get name(): string { return "MirrorReflectionBehaviorEditor"; }
// 重写 cnName 属性
public readonly cnName: string = "镜面反射";
private _mesh:Mesh | null = null;
public get mesh(): Mesh | null { return this._mesh; }
// 平整度阈值(角度,范围:0.05 - 5)
public readonly minFlatnessThreshold: number = 0.05;
public readonly maxFlatnessThreshold: number = 5.0;
private _flatnessThreshold: number = 1.0;
public get flatnessThreshold(): number { return this._flatnessThreshold; }
// 反射纹理尺寸(范围:256 - 2048)
private _textureSize: MirrorTexSize = 512;
public get textureSize(): MirrorTexSize { return this._textureSize; }
public readonly minBlurKernel: number = 0;
public readonly maxBlurKernel: number = 64;
private _blurKernel: number = 0;
public get blurKernel(): number { return this._blurKernel; }
// 行为是否有效(平整度检查是否通过)
private _isValid: boolean = true;
public get isValid(): boolean { return this._isValid; }
// 镜面反射纹理
private _mirrorTexture: MirrorTexture | null = null;
// 计算得出的反射平面,这时数学意义上的抽象平面,并不是实际的网格平面
private _reflectionPlane: Plane | null = null;
// 整体反射朝向,与反射平面垂直
private _overallNormal: Vector3 | null = null;
// 原始环境纹理备份(保存材质原来的 environmentTexture)
private _originalEnvironmentTexture: Nullable<BaseTexture> = null;
// 记录上次应用时的材质,用于检测材质是否被更换
private _lastAppliedMaterial: Nullable<Material> = null;
constructor() {
super();
this.onSetPreviewObservable.add((isPreview: boolean) => {
if (isPreview) {
this._applyMirrorTexture();
} else {
this._restoreOriginalTexture();
}
});
}
// 重写 attach 方法,添加自定义逻辑
attach(target: TransformNode): void {
super.attach(target);
if (!(target instanceof Mesh)) {
console.warn(`MirrorReflectionBehavior can only be attached to Mesh, but got: ${target.constructor.name}`);
this._isValid = false;
return;
}
this._mesh = target as Mesh;
this.setPreview(true);
// 检查平整度并初始化镜面反射
this._checkFlatnessAndInitialize();
}
// 重写 detach 方法,添加清理逻辑
detach(): void {
this._cleanup();
super.detach();
}
// 保存上一次的世界矩阵,用于检测变换是否改变
private _lastWorldMatrix: Float32Array | null = null;
// 重写 _updatePreview 方法,在每帧检测材质是否被更换和变换是否改变
protected _updatePreview(): void {
super._updatePreview();
// 如果行为有效且启用
if (this._isValid && this.enabled && this._mesh) {
// 检测材质是否被更换
if (this._mesh.material && this._lastAppliedMaterial && this._lastAppliedMaterial !== this._mesh.material) {
this._applyMirrorTexture();
}
// 检测变换是否改变,如果改变则更新反射平面
if (this._hasTransformChanged()) {
this._updateReflectionPlane();
}
}
}
// 检测mesh的世界变换是否改变
private _hasTransformChanged(): boolean {
if (!this._mesh) return false;
const currentWorldMatrix = this._mesh.getWorldMatrix();
// 如果是第一次检测,保存当前矩阵
if (!this._lastWorldMatrix) {
this._lastWorldMatrix = new Float32Array(currentWorldMatrix.asArray());
return false;
}
// 比较矩阵是否改变(比较旋转/缩放和位置信息)
const current = currentWorldMatrix.asArray();
const last = this._lastWorldMatrix;
// 比较旋转/缩放部分(索引0-11)和位置部分(索引12-14)
// 索引15是齐次坐标,通常为1,不需要比较
for (let i = 0; i < 15; i++) {
if (Math.abs(current[i] - last[i]) > 0.0001) {
// 变换已改变,更新保存的矩阵
this._lastWorldMatrix.set(current);
return true;
}
}
return false;
}
public cmdSetFlatnessThreshold(threshold: number): void {
// 限制范围
threshold = Scalar.Clamp(threshold, this.minFlatnessThreshold, this.maxFlatnessThreshold);
if(Math.abs(threshold - this._flatnessThreshold) < 0.00001)return;
const cmd = new CmdProperty<number>(
this._setFlatnessThreshold.bind(this),
threshold,
this._flatnessThreshold
);
cmdEmitter.emit('execute', cmd);
}
public readonly onSetFlatnessThresholdObservable = new Observable<number>();
// 设置平整度阈值
private _setFlatnessThreshold(threshold: number): void {
// 限制范围
threshold = Scalar.Clamp(threshold, this.minFlatnessThreshold, this.maxFlatnessThreshold);
const oldThreshold = this._flatnessThreshold;
const needRecheck =
(this._isValid && threshold < oldThreshold) || // 当前有效且阈值变小
(!this._isValid && threshold > oldThreshold); // 当前失效且阈值变大
this._flatnessThreshold = threshold;
if (needRecheck && this.target) {
this._checkFlatnessAndInitialize();
}
this.onSetFlatnessThresholdObservable.notifyObservers(threshold);
}
public cmdSetTexSize(size: MirrorTexSize): void {
if(size === this._textureSize)return;
const cmd = new CmdProperty<MirrorTexSize>(
this._setTexSize.bind(this),
size,
this._textureSize
);
cmdEmitter.emit('execute', cmd);
}
public readonly onSetTexSizeObservable = new Observable<MirrorTexSize>();
// 设置反射纹理尺寸
private _setTexSize(size: MirrorTexSize): void {
this._textureSize = size;
// 如果已经初始化,需要重新创建纹理
if (this._mirrorTexture && this._isValid) {
this._initializeMirrorTexture();
}
this.onSetTexSizeObservable.notifyObservers(size);
}
public cmdSetBlurKernel(blurKernel: number): void {
blurKernel = Scalar.Clamp(blurKernel, this.minBlurKernel, this.maxBlurKernel);
if(Math.abs(blurKernel - this._blurKernel) < 0.00001)return;
const cmd = new CmdProperty<number>(
this._setBlurKernel.bind(this),
blurKernel,
this._blurKernel
);
cmdEmitter.emit('execute', cmd);
}
public readonly onSetBlurKernelObservable = new Observable<number>();
private _setBlurKernel(blurKernel: number): void {
this._blurKernel = Scalar.Clamp(blurKernel, this.minBlurKernel, this.maxBlurKernel);
if (this._mirrorTexture) {
const kernel = blurKernel * this._textureSize / 1024;
this._mirrorTexture.blurKernelX = kernel;
this._mirrorTexture.blurKernelY = kernel;
}
this.onSetBlurKernelObservable.notifyObservers(blurKernel);
}
// 检查平整度并初始化镜面反射
private _checkFlatnessAndInitialize(): void {
if (!this._mesh) {
this._isValid = false;
return;
}
// 获取网格的位置和法线数据
const positions = this._mesh.getVerticesData("position");
const normals = this._mesh.getVerticesData("normal");
if (!positions || !normals || positions.length === 0 || normals.length === 0) {
console.warn("Mesh does not have position or normal data");
this._isValid = false;
return;
}
// 计算整体法线方向(平均所有法线)
const normalSum = new Vector3(0, 0, 0);
for (let i = 0; i < normals.length; i += 3) {
normalSum.x += normals[i];
normalSum.y += normals[i + 1];
normalSum.z += normals[i + 2];
}
this._overallNormal = normalSum.normalize();
// 检查所有法线与整体法线的夹角
const thresholdRadians = this._flatnessThreshold * Math.PI / 180;
let maxDeviation = 0;
for (let i = 0; i < normals.length; i += 3) {
const normal = new Vector3(normals[i], normals[i + 1], normals[i + 2]);
normal.normalize();
// 计算与整体法线的夹角
const dotProduct = Vector3.Dot(normal, this._overallNormal);
const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));
maxDeviation = Math.max(maxDeviation, angle);
// 如果超过阈值,标记为无效
if (angle > thresholdRadians) {
this._isValid = false;
console.warn(`Mesh is not flat enough. Max deviation: ${(maxDeviation * 180 / Math.PI).toFixed(3)}°, Threshold: ${this._flatnessThreshold}°`);
this._cleanup();
return;
}
}
// 平整度检查通过
this._isValid = true;
// 初始化镜面纹理(会自动计算反射平面)
this._initializeMirrorTexture();
}
// 更新反射平面(当mesh的位置或朝向改变时调用)
private _updateReflectionPlane(): void {
if (!this._mesh || !this._overallNormal || !this._mirrorTexture) {
return;
}
// 计算反射平面(使用网格中心点和整体法线)
const boundingInfo = this._mesh.getBoundingInfo();
const center = boundingInfo.boundingBox.center;
// 将法线和中心点转换到世界坐标
const worldMatrix = this._mesh.getWorldMatrix();
const worldCenter = Vector3.TransformCoordinates(center, worldMatrix);
const worldNormal = Vector3.TransformNormal(this._overallNormal, worldMatrix);
worldNormal.normalize();
// 创建反射平面(Babylon.js 平面方程:ax + by + cz + d = 0)
// 注意:MirrorTexture 需要反向法线(指向被反射空间的方向)
// 所以需要对法线取反
const mirrorNormal = worldNormal.scale(-1);
const d = -Vector3.Dot(mirrorNormal, worldCenter);
this._reflectionPlane = new Plane(mirrorNormal.x, mirrorNormal.y, mirrorNormal.z, d);
// 更新镜面纹理的反射平面
this._mirrorTexture.mirrorPlane = this._reflectionPlane;
}
private _sceneAddNewMeshObserver: Observer<AbstractMesh> | null = null;
private _sceneRemoveMeshObserver: Observer<AbstractMesh> | null = null;
// 初始化镜面反射纹理
private _initializeMirrorTexture(): void {
if (!this.scene || !this._mesh) {
return;
}
// 清理旧的镜面纹理
if (this._mirrorTexture) {
this._mirrorTexture.dispose();
this._mirrorTexture = null;
}
// 创建镜面纹理
this._mirrorTexture = new MirrorTexture(
`mirrorTexture_${this._mesh.name}`,
this._textureSize,
this.scene,
false // 不生成 mipmap(性能考虑)
);
// 计算并设置反射平面
this._updateReflectionPlane();
// 设置模糊
this._setBlurKernel(this._blurKernel);
// 设置渲染列表(反射场景中的所有网格)
const allMeshes = this.scene.meshes.filter(m => m !== this._mesh && m.isEnabled());
this._mirrorTexture.renderList = allMeshes;
this._sceneAddNewMeshObserver = this.scene.onNewMeshAddedObservable.add((m: AbstractMesh) => {
if(this._mirrorTexture && this._mirrorTexture.renderList && this._mirrorTexture.renderList.indexOf(m) === -1){
this._mirrorTexture.renderList?.push(m);
}
});
this._sceneRemoveMeshObserver = this.scene.onMeshRemovedObservable.add((m: AbstractMesh) => {
if(this._mirrorTexture && this._mirrorTexture.renderList && this._mirrorTexture.renderList.indexOf(m) !== -1){
this._mirrorTexture.renderList?.splice(this._mirrorTexture.renderList.indexOf(m), 1);
}
});
// 备份原始材质并应用镜面纹理
this._applyMirrorTexture();
}
// 将镜面纹理应用到材质
private _applyMirrorTexture(): void {
if (!this._mesh || !this._mirrorTexture) {
return;
}
// 如果没有材质,创建一个 PBRMetallicRoughnessMaterial
if (!this._mesh.material) {
this._mesh.material = new PBRMetallicRoughnessMaterial(`mirrorMaterial_${this._mesh.name}`, this.scene!);
}
const material = this._mesh.material;
// 检测材质是否被更换(与上次应用时的材质不同)
if (this._lastAppliedMaterial && this._lastAppliedMaterial !== material) {
// 材质已更换,重置原始纹理备份
this._originalEnvironmentTexture = null;
}
// 备份原始的 environmentTexture(如果还没有备份)
if (this._originalEnvironmentTexture === null) {
if (material instanceof PBRMetallicRoughnessMaterial) {
this._originalEnvironmentTexture = material.environmentTexture;
}
}
// 只替换 environmentTexture,不改变材质的其他属性
if (material instanceof PBRMetallicRoughnessMaterial) {
material.environmentTexture = this._mirrorTexture;
}
// 记录本次应用的材质
this._lastAppliedMaterial = material;
}
// 恢复原始纹理(用于 Preview 切换,不销毁资源)
private _restoreOriginalTexture(): void {
if (this._mesh && this._mesh.material instanceof PBRMetallicRoughnessMaterial) {
this._mesh.material.environmentTexture = this._originalEnvironmentTexture;
}
}
// 清理资源(用于 detach,完全销毁)
private _cleanup(): void {
// 先恢复原始纹理
this._restoreOriginalTexture();
// 移除场景观察者
if(this._sceneAddNewMeshObserver){
if(this.scene){
this.scene.onNewMeshAddedObservable.remove(this._sceneAddNewMeshObserver);
}
this._sceneAddNewMeshObserver = null;
}
if(this._sceneRemoveMeshObserver){
if(this.scene){
this.scene.onMeshRemovedObservable.remove(this._sceneRemoveMeshObserver);
}
this._sceneRemoveMeshObserver = null;
}
// 清空备份引用
this._originalEnvironmentTexture = null;
this._lastAppliedMaterial = null;
// 释放镜面纹理
if (this._mirrorTexture) {
this._mirrorTexture.dispose();
this._mirrorTexture = null;
}
// 清空其他引用
this._reflectionPlane = null;
this._overallNormal = null;
this._lastWorldMatrix = null;
}
// 重写 setEnabled 方法
public setEnabled(enabled: boolean): void {
super.setEnabled(enabled);
if (enabled && this._isValid) {
this._applyMirrorTexture();
} else if (!enabled) {
this._restoreOriginalTexture();
}
}
// 重写复制方法
public getCopy(): MirrorReflectionBehaviorEditor {
const copy = new MirrorReflectionBehaviorEditor();
copy._flatnessThreshold = this._flatnessThreshold;
copy._textureSize = this._textureSize;
return copy;
}
public getDataRuntime(): DTO_MirrorReflection {
return new DTO_MirrorReflection(
this.uuid,
this._flatnessThreshold,
this._textureSize,
this._blurKernel
);
}
public getDataEditor(): DTO_MirrorReflectionEditor {
return new DTO_MirrorReflectionEditor(
this.getDataRuntime(),
this.isPreview
);
}
public setByDataEditor(info: DTO_MirrorReflectionEditor): void {
this._flatnessThreshold = info.mirrorReflection.flatnessThreshold;
this._textureSize = info.mirrorReflection.textureSize;
this._blurKernel = info.mirrorReflection.blurKernel;
this.setPreview(info.isPreview);
}
// 重写序列化方法
public getJson(): string {
const data = this.getDataEditor();
return JSON.stringify(data);
}
// 重写反序列化方法
public setByJson(json: string): void {
try {
const data = JSON.parse(json) as DTO_MirrorReflectionEditor;
this.setByDataEditor(data);
} catch (error) {
console.error("Failed to parse JSON:", error);
}
}
}
export type MirrorTexSize = 128 | 256 | 512 | 1024 | 2048;
六、开发心得与踩坑总结
-
性能优先:反射纹理是性能杀手。关闭 mipmap、动态渲染列表、变换检测优化,都是必要的性能保障。
-
数学严谨性:平面方程、法线变换、矩阵比较,任何数学细节的错误都会导致反射画面撕裂或错位。特别是法线需要 normalize 且方向要正确。
-
编辑器友好:非破坏性修改是核心原则。不能让用户应用反射后丢失原有材质配置,备份机制必不可少。
-
鲁棒性:从平整度检测、材质类型判断到观察者管理,每个环节都要考虑异常情况,避免崩溃。
-
浮点精度 :
0.0001的误差阈值是经验值,太小会导致误判,太大则会漏掉微小但重要的变换。
这套系统已在我们的 3D 编辑器中稳定运行,支持设计师实时调整镜面效果,极大地提升了场景表现力。