import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader'; import '@babylonjs/loaders/glTF'; import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; import { Mesh } from '@babylonjs/core/Meshes/mesh'; import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector'; import { Scene } from '@babylonjs/core/scene'; import { Monobehiver } from '../base/Monobehiver'; import { Dictionary } from '../utils/Dictionary'; import { AppConfig } from './AppConfig'; import { EventBridge } from '../event/bridge'; import { DragConfig } from './AppModelDrag'; type LoadResult = { success: boolean; meshes?: AbstractMesh[]; skeletons?: unknown[]; error?: string; }; type ModelConfig = { name: string; url: string; }; type ModelControlType = 'rotation' | 'color'; type ModelTransform = { position?: { x: number; y: number; z: number }; rotation?: { x: number; y: number; z: number }; scale?: { x: number; y: number; z: number }; }; type ModelMetadata = { modelName: string; modelId: string; modelUrl: string; modelControlType?: ModelControlType; drag?: DragConfig; transform?: ModelTransform; }; /** * 模型管理类 - 负责加载、缓存和管理3D模型 */ export class AppModel extends Monobehiver { private modelDic: Dictionary; private modelMetadataDic: Dictionary; private loadedMeshes: AbstractMesh[]; private isLoading: boolean; constructor(mainApp: any) { super(mainApp); this.modelDic = new Dictionary(); this.modelMetadataDic = new Dictionary(); this.loadedMeshes = []; this.isLoading = false; } initManagers(): void { // 预留接口 } /** 加载配置中的所有模型 */ async loadModel(): Promise { if (!AppConfig.modelUrlList?.length || this.isLoading) return; this.isLoading = true; try { await this.loadMultipleModels(AppConfig.modelUrlList); EventBridge.modelLoaded({ urls: AppConfig.modelUrlList }); } finally { this.isLoading = false; } } /** * 批量加载模型(内部方法) * @param urls 模型URL数组 */ private async loadMultipleModels(urls: string[]): Promise { const total = urls.length; EventBridge.modelLoadProgress({ loaded: 0, total, urls, progress: 0, percentage: 0 }); for (let i = 0; i < urls.length; i++) { const url = urls[i]; const result = await this.loadSingleModel(url, (event) => { this.emitProgress(i, total, url, event); }); this.emitProgress(i + 1, total, url, null, result.success); if (!result.success) { EventBridge.modelLoadError({ url, error: result.error }); } } } /** * 发送加载进度事件 */ private emitProgress( loaded: number, total: number, url: string, event: ISceneLoaderProgressEvent | null, success?: boolean ): void { const currentProgress = event?.lengthComputable && event.total > 0 ? Math.min(1, event.loaded / event.total) : 0; const overallProgress = Math.min(1, (loaded + (event ? currentProgress : 0)) / total); EventBridge.modelLoadProgress({ loaded: loaded + (event ? currentProgress : 0), total, url, success, progress: overallProgress, percentage: Number((overallProgress * 100).toFixed(2)), detail: event ? { url, lengthComputable: event.lengthComputable, loadedBytes: event.loaded, totalBytes: event.total } : undefined }); } /** * 加载单个模型文件 * @param modelUrl 模型URL * @param onProgress 进度回调 */ private async loadSingleModel( modelUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void ): Promise { try { const scene = this.mainApp.appScene.object; if (!scene) return { success: false, error: '场景未初始化' }; const result = await ImportMeshAsync(modelUrl, scene, { onProgress }); if (!result?.meshes?.length) return { success: false, error: '未找到网格' }; this.loadedMeshes.push(...result.meshes); return { success: true, meshes: result.meshes, skeletons: result.skeletons }; } catch (e: any) { console.error(`模型加载失败: ${modelUrl}`, e); return { success: false, error: e?.message }; } } /** * 克隆模型材质,避免多个模型共享同名材质 * @param meshes 网格数组 * @param modelId 模型ID */ private cloneMaterials(meshes: AbstractMesh[], modelId: string): void { const scene = this.mainApp.appScene.object; const clonedMaterials = new Map(); meshes.forEach(mesh => { if (mesh.material) { const originalMaterial = mesh.material; const originalName = originalMaterial.name; // 如果该材质还没有被克隆过,则克隆它 if (!clonedMaterials.has(originalName)) { const newName = `${originalName}_${modelId}`; const clonedMaterial = originalMaterial.clone(newName); clonedMaterials.set(originalName, clonedMaterial); } // 应用克隆的材质 mesh.material = clonedMaterials.get(originalName); } }); } /** 为网格设置阴影(投射和接收) */ private createModelRoot(modelId: string, meshes: AbstractMesh[]): AbstractMesh[] { const scene = this.mainApp.appScene.object; const root = new Mesh(`${modelId}__root`, scene); const meshSet = new Set(meshes); root.position.copyFrom(this.getMeshesBoundingCenter(meshes)); meshes.forEach(mesh => { if (!mesh.parent || !meshSet.has(mesh.parent as AbstractMesh)) { mesh.setParent(root, true, true); } }); this.loadedMeshes.push(root); return [root, ...meshes]; } private getMeshesBoundingCenter(meshes: AbstractMesh[]): Vector3 { const renderableMeshes = meshes.filter(mesh => !mesh.isDisposed() && mesh.getTotalVertices() > 0); if (!renderableMeshes.length) return Vector3.Zero(); const min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); const max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); renderableMeshes.forEach(mesh => { mesh.computeWorldMatrix(true); const boundingBox = mesh.getBoundingInfo().boundingBox; min.minimizeInPlace(boundingBox.minimumWorld); max.maximizeInPlace(boundingBox.maximumWorld); }); return min.add(max).scaleInPlace(0.5); } setupShadows(meshes: AbstractMesh[]): void { const appLight = this.mainApp.appLight; if (!appLight) return; meshes.forEach(mesh => { if (mesh.getTotalVertices() > 0) { appLight.addShadowCaster(mesh); mesh.receiveShadows = true; } }); } /** 获取缓存的网格 */ getCachedMeshes(name: string): AbstractMesh[] | undefined { return this.modelDic.Get(name); } /** 清理所有资源 */ clean(): void { this.modelDic.Clear(); this.loadedMeshes.forEach(m => m?.dispose()); this.loadedMeshes = []; this.isLoading = false; } /** * 添加模型到场景(支持单个或批量) * @param modelConfig 模型配置对象 或 模型配置数组 */ async add( modelConfig: ModelMetadata | ModelMetadata[] ): Promise { // 批量加载 if (Array.isArray(modelConfig)) { return await this.addMultiple(modelConfig); } // 单个加载 return await this.addSingle( modelConfig.modelName, modelConfig.modelId, modelConfig.modelUrl, modelConfig.modelControlType, modelConfig.drag, modelConfig.transform ); } /** * 添加单个模型 */ private async addSingle(modelName: string, modelId: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise { // 检查是否已存在 const existingMeshes = this.modelDic.Get(modelName + '_' + modelId); if (existingMeshes?.length && !existingMeshes[0].isDisposed()) { console.log(`模型 ${modelName + modelId} 已存在,直接显示`); this.showMeshes(existingMeshes); return { success: true, meshes: existingMeshes }; } // 加载模型 const result = await this.loadSingleModel(modelUrl, (event) => { this.emitSingleProgress(modelUrl, event); }); if (result.success && result.meshes) { // 克隆材质,确保每个模型有独立的材质 this.cloneMaterials(result.meshes, modelId); // 存储模型 result.meshes = this.createModelRoot(modelName + '_' + modelId, result.meshes); this.modelDic.Set(modelName + '_' + modelId, result.meshes); // 存储元数据 this.modelMetadataDic.Set(modelName + '_' + modelId, { modelName: modelName, modelId: modelId, modelUrl: modelUrl, modelControlType: modelControlType, drag: drag, transform: transform }); // 应用 transform if (transform) { this.applyTransform(modelName + '_' + modelId, transform); } // 配置拖拽功能 if (drag) { this.mainApp.appModelDrag?.configureDrag(modelName + '_' + modelId, drag); } // 更新 GameManager 的字典 this.mainApp.gameManager?.updateDictionaries(); EventBridge.modelLoaded({ urls: [modelUrl] }); } else { EventBridge.modelLoadError({ url: modelUrl, error: result.error }); } return result; } /** * 批量添加模型 */ private async addMultiple(models: ModelMetadata[]): Promise<{ success: boolean; results: LoadResult[] }> { const total = models.length; const results: LoadResult[] = []; EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 }); for (let i = 0; i < models.length; i++) { const { modelName, modelId, modelUrl, modelControlType, drag, transform } = models[i]; const result = await this.loadSingleModel(modelUrl, (event) => { this.emitProgress(i, total, modelUrl, event); }); if (result.success && result.meshes) { // 克隆材质,确保每个模型有独立的材质 this.cloneMaterials(result.meshes, modelId); result.meshes = this.createModelRoot(modelName, result.meshes); this.modelDic.Set(modelName + '_' + modelId, result.meshes); // 存储元数据 this.modelMetadataDic.Set(modelName + '_' + modelId, { modelName: modelName, modelId: modelId, modelUrl: modelUrl, modelControlType: modelControlType, drag: drag, transform: transform }); // 应用 transform if (transform) { this.applyTransform(modelName + '_' + modelId, transform); } // 配置拖拽功能 if (drag) { this.mainApp.appModelDrag?.configureDrag(modelName + '_' + modelId, drag); } } results.push(result); this.emitProgress(i + 1, total, modelUrl, null, result.success); } // 批量加载完成后统一更新字典 this.mainApp.gameManager?.updateDictionaries(); EventBridge.modelLoaded({ urls: models.map(m => m.modelUrl) }); return { success: results.every(r => r.success), results }; } /** * 显示网格 */ private showMeshes(meshes: AbstractMesh[]): void { meshes.forEach(mesh => { mesh.setEnabled(true); mesh.getChildMeshes().forEach(child => child.setEnabled(true)); }); } /** * 发送单个模型加载进度 */ private emitSingleProgress(url: string, event: ISceneLoaderProgressEvent): void { const progress = event.lengthComputable && event.total > 0 ? Math.min(1, event.loaded / event.total) : 0; EventBridge.modelLoadProgress({ loaded: progress, total: 1, url, progress, percentage: Number((progress * 100).toFixed(2)), detail: { url, lengthComputable: event.lengthComputable, loadedBytes: event.loaded, totalBytes: event.total } }); } /** * 根据 mesh 名称查找 mesh 对象 * @param meshName mesh 名称 * @returns mesh 对象,未找到返回 undefined */ private findMeshByName(meshName: string): AbstractMesh | undefined { const keys = this.modelDic.Keys(); for (const key of keys) { const meshes = this.modelDic.Get(key); const found = meshes?.find(m => m.name === meshName); if (found) return found; } return undefined; } /** * 根据 mesh 查找所属的模型名称 * @param mesh 网格对象 * @returns 模型名称,未找到返回 undefined */ findModelNameByMesh(mesh: AbstractMesh): string | undefined { const keys = this.modelDic.Keys(); for (const key of keys) { const meshes = this.modelDic.Get(key); if (meshes?.some(m => m === mesh || m.uniqueId === mesh.uniqueId)) { return key; } } return undefined; } /** * 根据 mesh 或 mesh 名称移除所属的整个模型 * @param meshOrName 网格对象或网格名称 * @returns 是否成功移除 */ remove(meshOrName: AbstractMesh | string): boolean { let mesh: AbstractMesh | undefined; // 判断传入的是对象还是字符串 if (typeof meshOrName === 'string') { mesh = this.findMeshByName(meshOrName); if (!mesh) { console.warn(`未找到名为 ${meshOrName} 的网格`); return false; } } else { mesh = meshOrName; } const modelName = this.findModelNameByMesh(mesh); if (modelName) { this.removeByName(modelName); return true; } console.warn('未找到该 mesh 所属的模型'); return false; } /** * 替换模型 * @param modelConfig 模型配置对象 */ async replaceModel(modelConfig: ModelMetadata): Promise { this.removeByName(modelConfig.modelId); return await this.addSingle( modelConfig.modelName, modelConfig.modelId, modelConfig.modelUrl, modelConfig.modelControlType, modelConfig.drag, modelConfig.transform ); } /** * 销毁指定模型 * @param modelName 模型名称 */ removeByName(modelName: string): boolean { const meshes = this.modelDic.Get(modelName); if (!meshes?.length) { console.warn(`Model not found: ${modelName}`); return false; } this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true)); this.modelDic.Remove(modelName); this.modelMetadataDic.Remove(modelName); this.mainApp.gameManager?.updateDictionaries(); return true; } /** * 清除所有已添加的模型并释放内存 * 主要用于切换尺寸后清除不适用的配件 */ removeAll(): void { const modelNames = this.modelDic.Keys(); modelNames.forEach(modelName => { const meshes = this.modelDic.Get(modelName); if (meshes?.length) { this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true)); } }); this.modelDic.Clear(); this.modelMetadataDic.Clear(); this.mainApp.gameManager?.updateDictionaries(); console.log('所有模型已清除,内存已释放'); } /** * 获取模型元数据 * @param modelName 模型名称 */ getModelMetadata(modelName: string): ModelMetadata | undefined { return this.modelMetadataDic.Get(modelName); } /** * 根据网格查找模型元数据 * @param mesh 网格对象 */ getMetadataByMesh(mesh: AbstractMesh): ModelMetadata | undefined { const modelName = this.findModelNameByMesh(mesh); if (modelName) { return this.modelMetadataDic.Get(modelName); } return undefined; } private getModelTransformTargets(meshes: AbstractMesh[]): AbstractMesh[] { const meshSet = new Set(meshes); const rootMeshes = meshes.filter(mesh => !mesh.parent || !meshSet.has(mesh.parent as AbstractMesh)); return rootMeshes.length ? rootMeshes : meshes.slice(0, 1); } getModelTransformTargetByMesh(mesh: AbstractMesh): AbstractMesh | undefined { const modelName = this.findModelNameByMesh(mesh); if (!modelName) return mesh; const meshes = this.modelDic.Get(modelName); if (!meshes?.length) return mesh; return this.getModelTransformTargets(meshes)[0] ?? mesh; } getModelMeshesByMesh(mesh: AbstractMesh): AbstractMesh[] { const modelName = this.findModelNameByMesh(mesh); if (!modelName) return [mesh]; const meshes = this.modelDic.Get(modelName); return meshes?.length ? meshes : [mesh]; } /** * 设置模型旋转 * @param modelId 模型ID * @param rotation 旋转向量 {x, y, z}(默认使用角度) * @param useDegrees 是否使用角度(默认true) */ setRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void { const meshes = this.modelDic.Get(modelId); if (!meshes?.length) { console.warn(`Model not found: ${modelId}`); return; } // 如果使用角度,转换为弧度 const toRadians = (degrees: number) => degrees * Math.PI / 180; const rotationValues = useDegrees ? { x: toRadians(rotation.x), y: toRadians(rotation.y), z: toRadians(rotation.z) } : rotation; this.getModelTransformTargets(meshes).forEach(mesh => { if (mesh.rotationQuaternion) { mesh.rotationQuaternion = Quaternion.FromEulerAngles( rotationValues.x, rotationValues.y, rotationValues.z ); return; } mesh.rotation.set(rotationValues.x, rotationValues.y, rotationValues.z); }); } /** * 累加模型旋转 * @param modelId 模型ID * @param rotation 旋转向量 {x, y, z}(默认使用角度) * @param useDegrees 是否使用角度(默认true) */ addRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void { const meshes = this.modelDic.Get(modelId); if (!meshes?.length) { console.warn(`Model not found: ${modelId}`); return; } // 如果使用角度,转换为弧度 const toRadians = (degrees: number) => degrees * Math.PI / 180; const rotationValues = useDegrees ? { x: toRadians(rotation.x), y: toRadians(rotation.y), z: toRadians(rotation.z) } : rotation; this.getModelTransformTargets(meshes).forEach(mesh => { mesh.addRotation(rotationValues.x, rotationValues.y, rotationValues.z); }); } /** * 设置模型位置 * @param modelId 模型ID * @param position 位置向量 {x, y, z} */ setPosition(modelId: string, position: { x: number; y: number; z: number }): void { const meshes = this.modelDic.Get(modelId); if (!meshes?.length) { console.warn(`Model not found: ${modelId}`); return; } this.getModelTransformTargets(meshes).forEach(mesh => { mesh.position.x = position.x; mesh.position.y = position.y; mesh.position.z = position.z; }); } /** * 设置模型缩放 * @param modelId 模型ID * @param scale 缩放向量 {x, y, z} */ setScale(modelId: string, scale: { x: number; y: number; z: number }): void { const meshes = this.modelDic.Get(modelId); if (!meshes?.length) { console.warn(`Model not found: ${modelId}`); return; } this.getModelTransformTargets(meshes).forEach(mesh => { mesh.scaling.x = scale.x; mesh.scaling.y = scale.y; mesh.scaling.z = scale.z; }); } /** * 将模型放置到指定的放置区域 * @param modelId 模型ID * @param zoneInfo 放置区域信息 * @param offsetDistance 距离墙面的偏移距离(默认0.1,正数向外) */ placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void { const meshes = this.modelDic.Get(modelId); if (!meshes?.length) { console.warn(`Model not found: ${modelId}`); return; } // 计算放置位置:中心点 + 法线方向的偏移 const targetPosition = zoneInfo.center.add(zoneInfo.normal.scale(offsetDistance)); // 计算旋转角度:让模型面向墙面(法线的反方向) const targetDirection = zoneInfo.normal.scale(-1); const angle = Math.atan2(targetDirection.x, targetDirection.z); this.getModelTransformTargets(meshes).forEach(mesh => { // 设置位置 mesh.position.copyFrom(targetPosition); // 设置旋转(只旋转Y轴,让模型面向墙面) if (mesh.rotationQuaternion) { mesh.rotationQuaternion = Quaternion.FromEulerAngles(0, angle, 0); } else { mesh.rotation.set(0, angle, 0); } }); } /** * 检查模型是否存在 * @param modelId 模型ID * @returns 模型是否存在 */ exists(modelId: string): boolean { return this.modelDic.Has(modelId); } /** * 应用 transform 到模型 * @param modelId 模型ID * @param transform 变换信息 */ private applyTransform(modelId: string, transform: ModelTransform): void { // 应用位置 if (transform.position) { this.setPosition(modelId, transform.position); } // 应用旋转(角度制) if (transform.rotation) { this.setRotation(modelId, transform.rotation, true); } // 应用缩放 if (transform.scale) { this.setScale(modelId, transform.scale); } } }