锦州市网站建设_网站建设公司_在线客服_seo优化
2026/1/10 5:53:54 网站建设 项目流程

在 3D 编辑器开发中,镜面反射是一个既常见又充满挑战的功能。最近我实现了MirrorReflectionBehaviorEditor,一个基于 Babylon.js 的镜面反射行为编辑器。本文将深入剖析其核心实现,重点讲解MirrorTexture 的创建过程Transform 改变的检测机制,并分享开发中的关键思考。

一、功能概述

MirrorReflectionBehaviorEditor的主要功能是为任意网格(Mesh)添加实时镜面反射效果。它会:

  1. 自动检测网格平整度:确保只有平坦的表面才能生成正确的镜面反射

  2. 动态创建反射纹理:使用 Babylon.js 的MirrorTexture实时渲染反射场景

  3. 自适应变换更新:当物体移动、旋转或缩放时,自动更新反射平面

  4. 预览模式切换:支持编辑器中实时预览开关,不影响原始材质

核心亮点:整个系统完全响应式,所有参数修改都通过命令模式(Command Pattern)实现可撤销/重做。

二、MirrorTexture 创建过程详解

这是整个行为的核心。创建过程在_initializeMirrorTexture()方法中完成,涉及多个关键步骤:

1. 资源清理与准备

// 清理旧的镜面纹理 if (this._mirrorTexture) { this._mirrorTexture.dispose(); this._mirrorTexture = null; }

每次重新初始化时,必须先释放旧纹理,避免内存泄漏。这是 Babylon.js 开发的最佳实践。

2. 纹理实例化

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. 反射平面计算

this._updateReflectionPlane();

这一步调用专门的方法计算数学意义上的反射平面,后续详细讲解。

4. 动态模糊设置

this._setBlurKernel(this._blurKernel);

根据纹理尺寸动态调整模糊核大小,确保不同分辨率下模糊效果一致:

const kernel = blurKernel * this._textureSize / 1024; this._mirrorTexture.blurKernelX = kernel; this._mirrorTexture.blurKernelY = kernel;

5. 渲染列表管理

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. 原始材质备份

this._applyMirrorTexture();

不直接替换材质,而是只修改environmentTexture属性,保留材质的其他所有设置。这种非破坏性修改是编辑器架构的核心理念。

三、Transform 改变检测机制

当物体在场景中移动、旋转或缩放时,反射平面必须实时更新。但每帧都重新计算会造成性能浪费,因此需要精确的变更检测。

1. 变更检测的核心逻辑

_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(); } } }

2. 矩阵比较算法

_hasTransformChanged()是检测的核心,它比较世界矩阵的变换:

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()

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,实现预览开关时不销毁资源:

private _restoreOriginalTexture(): void { if (this._mesh && this._mesh.material instanceof PBRMetallicRoughnessMaterial) { this._mesh.material.environmentTexture = this._originalEnvironmentTexture; } }

3. 命令模式集成

所有参数修改通过CmdProperty包装,确保编辑器可撤销:

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

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

import BaseBehaviorEditor from "../BaseBehaviorEditor"; import type { GeneralType } from "./NodeGeneralBehavManagerEditor"; export default class GeneralBehaviorEditor extends BaseBehaviorEditor { public readonly generalType: GeneralType = "Base"; }

MirrorReflectionBehaviorEditor.ts

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;

六、开发心得与踩坑总结

  1. 性能优先:反射纹理是性能杀手。关闭 mipmap、动态渲染列表、变换检测优化,都是必要的性能保障。

  2. 数学严谨性:平面方程、法线变换、矩阵比较,任何数学细节的错误都会导致反射画面撕裂或错位。特别是法线需要 normalize 且方向要正确。

  3. 编辑器友好:非破坏性修改是核心原则。不能让用户应用反射后丢失原有材质配置,备份机制必不可少。

  4. 鲁棒性:从平整度检测、材质类型判断到观察者管理,每个环节都要考虑异常情况,避免崩溃。

  5. 浮点精度0.0001的误差阈值是经验值,太小会导致误判,太大则会漏掉微小但重要的变换。

这套系统已在我们的 3D 编辑器中稳定运行,支持设计师实时调整镜面效果,极大地提升了场景表现力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询