MirrorReflectionBehaviorEditor 开发心得:Babylon.js 镜面反射的实现与优化

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

一、功能概述

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

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

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

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

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

核心亮点:整个系统完全响应式,所有参数修改都通过命令模式(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;

六、开发心得与踩坑总结

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

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

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

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

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

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

相关推荐
ttod_qzstudio21 小时前
从Unity的C#到Babylon.js的typescript:“函数重载“变成“类型魔法“
typescript·c#·重载·babylon.js
ttod_qzstudio3 天前
Babylon.js:MirrorTexture平面反射的科学与艺术
babylonjs·mirrortexture
ttod_qzstudio6 天前
Babylon.js TransformNode.clone() 的隐形陷阱:当 null 不等于 null
babylon.js
ttod_qzstudio11 天前
备忘录之Babylon.js 子对象获取方法
babylon.js
ttod_qzstudio18 天前
深入理解 Babylon.js:TransformNode.setParent 与 parent 赋值的核心差异
babylon.js
ttod_qzstudio1 个月前
Babylon.js中欧拉角与四元数转换的完整指南
babylon.js
ttod_qzstudio2 个月前
Babylon.js 双面渲染迷雾:backFaceCulling、cullBackFaces 与 doubleSided 的三角关系解析
babylon.js·cull
ttod_qzstudio2 个月前
Babylon.js中PBRMetallicRoughnessMaterial材质系统深度解析:从基础到工程实践
babylon.js·pbr
ttod_qzstudio2 个月前
Babylon.js材质冻结的“双刃剑“:性能优化与IBL环境冲突的深度解析
nexttick·babylon.js