import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3, TransformNode } from "@babylonjs/core"; import { Observer } from "@babylonjs/core/Misc/observable"; import { Nullable } from "@babylonjs/core/types"; import { Monobehiver } from '../base/Monobehiver'; import { Dictionary } from '../utils/Dictionary'; import { AppConfig } from './AppConfig'; type RollerDoorOptions = { /** 目标升起高度,缺省为初始 y + 3 */ upY?: number; /** 落下终点,缺省为初始 y */ downY?: number; /** 运动速度(单位/秒),缺省 1 */ speed?: number; /** 自定义门体网格名列表,不传使用默认两个卷帘门 */ meshNames?: string[]; }; /** * 游戏管理器类 - 负责管理游戏逻辑、材质和纹理 */ export class GameManager extends Monobehiver { private materialDic: Dictionary; private meshDic: Dictionary; private oldTextureDic: Dictionary; private rollerDoorMeshes: AbstractMesh[]; private rollerDoorGroup: AbstractMesh | null; private rollerDoorInitialY: Map; private rollerDoorObserver: Nullable>; private rollerDoorIsOpen: boolean; private rollerDoorNames: string[]; private yClipPlane: Plane | null; private yClipTargets: string[] | null; private clipPlaneVisualization: Mesh | null; // 记录加载失败的贴图 private failedTextures: Array<{ path: string; materialName?: string; textureType?: string; error?: string; timestamp: Date; }>; constructor(mainApp: any) { super(mainApp); this.materialDic = new Dictionary(); this.meshDic = new Dictionary(); this.oldTextureDic = new Dictionary(); this.rollerDoorMeshes = []; this.rollerDoorGroup = null; this.rollerDoorInitialY = new Map(); this.rollerDoorObserver = null; this.rollerDoorIsOpen = false; this.rollerDoorNames = ["Box006.001", "Box005.001"]; this.yClipPlane = null; this.yClipTargets = null; this.clipPlaneVisualization = null; this.failedTextures = []; } /** 调试:返回当前场景中所有网格名称 */ listMeshNames(): string[] { return this.meshDic.Keys(); } /** 初始化游戏管理器 */ async Awake() { const scene = this.mainApp.appScene?.object; if (!scene) { console.warn('Scene not found'); return; } // 初始化材质和网格字典 this.updateDictionaries(); this.cacheRollerDoorMeshes(); this.setRollerDoorScale("Box006.001", new Vector3(0.12, 0.02, 0.118)); this.setRollerDoorScale("Box005.001", new Vector3(0.13, 0.02, 0.12)); } /** * 更新材质和网格字典(从场景中同步) */ updateDictionaries(): void { const scene = this.mainApp.appScene?.object; if (!scene) return; this.materialDic.Clear(); this.meshDic.Clear(); // 更新材质字典 for (const mat of scene.materials) { if (!(mat as any)._isDisposed && !this.materialDic.Has(mat.name)) { this.materialDic.Set(mat.name, mat as PBRMaterial); } } // 更新网格字典 for (const mesh of scene.meshes) { if (mesh instanceof Mesh && !mesh.isDisposed() && !this.meshDic.Has(mesh.name)) { this.meshDic.Set(mesh.name, mesh); } const mat = mesh.material; if (mat instanceof PBRMaterial && !(mat as any)._isDisposed) { this.materialDic.Set(mat.name, mat); } } } /** 初始化设置材质 */ async initSetMaterial(oldObject: any) { if (!oldObject?.Component?.length) return; const { degreeId, Component } = oldObject; let degreeTextureDic = this.oldTextureDic.Get(degreeId) || {}; const texturePromises: Promise[] = []; // 处理每个组件 for (const component of Component) { const { name, albedoTexture, bumpTexture, alphaTexture, aoTexture, } = component; if (!name) continue; // 获取材质 const mat = this.materialDic.Get(name); if (!mat) { continue; } // 获取或初始化纹理字典 const textureDic = degreeTextureDic[name] || { albedo: null, bump: null, alpha: null, ao: null }; // 定义纹理任务 const textureTasks = [ { key: "albedo", path: albedoTexture, property: "albedoTexture" }, { key: "bump", path: bumpTexture, property: "bumpTexture" }, { key: "alpha", path: alphaTexture, property: "opacityTexture" }, { key: "ao", path: aoTexture, property: "ambientTexture" } ]; // 处理每个纹理任务 for (const task of textureTasks) { const { key, path, property } = task; if (!path) continue; const fullPath = this.getPublicUrl() + path; let texture = textureDic[key]; if (!texture) { try { texture = this.createTextureWithFallback(fullPath); if (!texture) { // 记录失败的贴图信息 this.failedTextures.push({ path: fullPath, materialName: name, textureType: key, error: '贴图创建失败', timestamp: new Date() }); continue; } // 设置非ktx2格式的vScale if (!fullPath.toLowerCase().endsWith('.ktx2')) { texture.vScale = -1; } textureDic[key] = texture; } catch (error: any) { // 记录失败的贴图信息 this.failedTextures.push({ path: fullPath, materialName: name, textureType: key, error: error.message || error.toString(), timestamp: new Date() }); continue; } } // 将纹理赋值任务加入队列 texturePromises.push( this.handleTextureAssignment(mat, textureDic, key, (texture) => { (mat as any)[property] = texture; }) ); } // 更新纹理字典 degreeTextureDic[name] = textureDic; } // 等待所有纹理任务完成 try { await Promise.all(texturePromises); // 在所有贴图加载完成后设置材质属性 for (const component of Component) { const { name, transparencyMode, bumpTextureLevel } = component; if (!name) continue; const mat = this.materialDic.Get(name); if (!mat) continue; mat.transparencyMode = transparencyMode; if (mat.bumpTexture) { mat.bumpTexture.level = bumpTextureLevel; } // 应用新的PBR材质属性 this.applyPBRProperties(mat, component); } } catch (error) { console.error('Error loading textures:', error); } finally { if (this.mainApp.appDom?.load3D) { this.mainApp.appDom.load3D.style.display = "none"; } } // 保存更新后的纹理字典 this.oldTextureDic.Set(degreeId, degreeTextureDic); } /** * 应用PBR材质属性 * @param mat - PBR材质对象 * @param component - 配置组件对象 */ private applyPBRProperties(mat: PBRMaterial, component: any) { // 定义PBR属性映射任务 const pbrTasks = [ { key: "fresnel", value: component.fresnel, apply: (value: number) => { mat.indexOfRefraction = value; } }, { key: "clearcoat", value: component.clearcoat, apply: (value: number) => { mat.clearCoat.isEnabled = true; mat.clearCoat.intensity = value; } }, { key: "clearcoatRoughness", value: component.clearcoatRoughness, apply: (value: number) => { mat.clearCoat.roughness = value; } }, { key: "roughness", value: component.roughness, apply: (value: number) => { mat.roughness = value; } }, { key: "metallic", value: component.metallic, apply: (value: number) => { mat.metallic = value; } }, { key: "alpha", value: component.alpha, apply: (value: number) => { mat.alpha = value; } }, { key: "environmentIntensity", value: component.environmentIntensity, apply: (value: number) => { mat.environmentIntensity = value; } }, { key: "baseColor", value: component.baseColor, apply: (value: any) => { if (value && typeof value === 'object') { const { r, g, b } = value; if (r !== null && r !== undefined && g !== null && g !== undefined && b !== null && b !== undefined) { mat.albedoColor.set(r, g, b); } } } } ]; // 处理每个PBR属性任务 for (const task of pbrTasks) { if (task.value !== null && task.value !== undefined) { try { task.apply(task.value); } catch (error) { console.warn('Error applying PBR property:', task.key, error); } } } } /** 通用的批量卸载贴图资源的方法 */ private clearTextures(textureDic: Dictionary): Promise { return new Promise((resolve) => { textureDic.Values().forEach((textures) => { for (const key in textures) { const texture = textures[key]; if (texture && texture instanceof Texture) { texture.dispose(); } } }); textureDic.Clear(); resolve(); }); } /** 处理纹理赋值 */ private async handleTextureAssignment(mat: any, oldtextureDic: any, textureKey: string, assignCallback: (texture: Texture) => void) { const texture = oldtextureDic[textureKey]; if (texture) { await this.checkTextureLoadedWithPromise(texture); assignCallback(texture); } } /** 检查纹理是否加载完成 */ private checkTextureLoadedWithPromise(texture: Texture): Promise { return new Promise((resolve) => { if (texture.isReady()) { resolve(); } else { texture.onLoadObservable.addOnce(() => { resolve(); }); } }); } /** 重置相机位置 */ reSet() { if (this.mainApp.appCamera?.object?.position) { this.mainApp.appCamera.object.position.set(160, 50, 0); } } /** 卷帘门开合:再次调用会反向动作 */ toggleRollerDoor(options?: RollerDoorOptions): void { this.setRollerDoorState(!this.rollerDoorIsOpen, options); } /** 直接设置卷帘门状态 */ setRollerDoorState(open: boolean, options?: RollerDoorOptions): void { const scene = this.mainApp.appScene?.object; if (!scene) { console.warn('Scene not found for roller door'); return; } this.cacheRollerDoorMeshes(options?.meshNames); if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) { console.warn('Roller door group or meshes not found'); return; } const speed = Math.max(options?.speed ?? 1, 0.01); // 计算目标高度 let targetY: number; if (open) { // 上升时:如果指定了绝对高度就用绝对高度,否则用相对高度 if (options?.upY !== undefined) { targetY = options.upY; } else { // 找到所有门中最高的初始位置,让所有门都升到这个高度+3 const maxBaseY = Math.max(...this.rollerDoorMeshes.map(m => this.rollerDoorInitialY.get(m.name) ?? m.position.y )); targetY = maxBaseY + 3; } } else { // 下降时:回到初始位置 targetY = 0; } // 检查是否已经在目标位置 if (Math.abs(this.rollerDoorGroup.position.y - targetY) < 0.001) { this.rollerDoorIsOpen = open; return; } this.rollerDoorIsOpen = open; this.stopRollerDoorAnimation(); this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => { const dt = scene.getEngine().getDeltaTime() / 1000; const current = this.rollerDoorGroup!.position.y; const direction = targetY >= current ? 1 : -1; // 使用固定速度变量 const step = speed * dt; let next = current + direction * step; if ((direction > 0 && next >= targetY) || (direction < 0 && next <= targetY)) { next = targetY; this.stopRollerDoorAnimation(); this.rollerDoorIsOpen = open; console.log('Roller door animation finished'); } // 移动透明盒子 this.rollerDoorGroup!.position.y = next; // 打印每个卷帘门的当前位置 // console.log('Roller door positions:'); // for (const mesh of this.rollerDoorMeshes) { // console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`); // } }); } /** 当前卷帘门是否开启 */ isRollerDoorOpen(): boolean { return this.rollerDoorIsOpen; } /** * 设置卷帘门的缩放 * @param meshName - 卷帘门网格名称 * @param scale - 缩放值(可以是单个数字或 Vector3) */ setRollerDoorScale(meshName: string, scale: number | Vector3): void { const mesh = this.meshDic.Get(meshName); if (mesh) { if (typeof scale === 'number') { mesh.scaling.set(scale, scale, scale); } else { mesh.scaling.copyFrom(scale); } console.log(`Set scale for ${meshName}:`, mesh.scaling.asArray()); } else { console.warn(`Roller door mesh not found: ${meshName}`); } } /** * 设置所有卷帘门的缩放 * @param scale - 缩放值(可以是单个数字或 Vector3) */ setAllRollerDoorsScale(scale: number | Vector3): void { this.rollerDoorMeshes.forEach(mesh => { if (typeof scale === 'number') { mesh.scaling.set(scale, scale, scale); } else { mesh.scaling.copyFrom(scale); } console.log(`Set scale for ${mesh.name}:`, mesh.scaling.asArray()); }); } /** * 设置基于 Y 轴的剖切平面,keepAbove=true 时保留平面以上部分 * onlyMeshNames 指定只作用于哪些网格,其他网格不受影响 */ setYAxisClip( height: number, keepAbove = true, onlyMeshNames?: string[], excludeMeshNames?: string[] ): void { const scene = this.mainApp.appScene?.object; if (!scene) { console.warn('Scene not found for clipping'); return; } const normal = new Vector3(0, keepAbove ? 1 : -1, 0); this.yClipPlane = Plane.FromPositionAndNormal(new Vector3(0, height, 0), normal); // 如果指定了特定网格,只对这些网格应用剖切 if (onlyMeshNames?.length) { this.applyClipPlaneToMeshes(this.yClipPlane, onlyMeshNames); } else { // 否则使用场景级别的剖切,作用于所有网格 scene.clipPlane = this.yClipPlane; } console.log('[clipping] Scene clipPlane set:', { height, keepAbove, normal: normal.asArray(), targets: onlyMeshNames || 'all' }); } /** 关闭 Y 轴剖切 */ clearYAxisClip(): void { const scene = this.mainApp.appScene?.object; if (scene) { scene.clipPlane = null; } this.yClipPlane = null; this.yClipTargets = null; // 清除所有网格材质上的 clipPlane this.meshDic.Values().forEach((mesh) => { const mat = mesh.material as any; if (mat && 'clipPlane' in mat) { mat.clipPlane = null; } }); } private cacheRollerDoorMeshes(customNames?: string[]): void { const scene = this.mainApp.appScene?.object; if (!scene) return; const names = customNames?.length ? customNames : this.rollerDoorNames; this.rollerDoorMeshes = []; // 创建或获取 group 作为父级 if (!this.rollerDoorGroup) { // 创建一个 AbstractMesh 作为组 // 使用 TransformNode 代替 AbstractMesh,因为 AbstractMesh 是抽象类无法实例化 this.rollerDoorGroup = new TransformNode('rollerDoorGroup', scene) as any; // 确保 group 的初始位置为 (0, 0, 0) this.rollerDoorGroup.position.set(0, 0, 0); } for (const name of names) { const mesh = this.meshDic.Get(name); if (mesh) { this.rollerDoorMeshes.push(mesh); // 保存网格的当前位置作为初始位置 if (!this.rollerDoorInitialY.has(name)) { this.rollerDoorInitialY.set(name, mesh.position.y); } // 保存网格的世界位置和缩放 const worldPosition = mesh.getAbsolutePosition(); const worldScaling = new Vector3(mesh.scaling.x, mesh.scaling.y, mesh.scaling.z); // 将网格添加到 group 中 mesh.parent = this.rollerDoorGroup; // 调整网格的局部位置和缩放,保持世界位置和大小不变 mesh.setAbsolutePosition(worldPosition); mesh.scaling.copyFrom(worldScaling); } else { console.warn(`Roller door mesh not found: ${name}`); } } } private stopRollerDoorAnimation(): void { const scene = this.mainApp.appScene?.object; if (scene && this.rollerDoorObserver) { scene.onBeforeRenderObservable.remove(this.rollerDoorObserver); } this.rollerDoorObserver = null; } /** 将 clipPlane 只作用到指定网格的材质 */ private applyClipPlaneToMeshes(plane: Plane, targetNames: string[]): void { const targetSet = new Set(targetNames); let appliedCount = 0; this.meshDic.Values().forEach((mesh) => { const mat = mesh.material as any; if (!mat) { console.log('[clipping] Mesh has no material:', mesh.name); return; } if (targetSet.has(mesh.name)) { // 目标网格:应用剖切 mat.clipPlane = plane; appliedCount++; console.log('[clipping] Applied to mesh:', mesh.name, 'material:', mat.name); } else { // 非目标网格:清除剖切 mat.clipPlane = null; } }); console.log('[clipping] Total meshes processed:', this.meshDic.Keys().length, 'Applied to:', appliedCount); if (appliedCount === 0) { console.warn('[clipping] No meshes found with names:', targetNames); console.log('[clipping] Available mesh names:', this.meshDic.Keys()); } } /** 获取公共URL */ private getPublicUrl(): string { // 尝试从环境变量获取 if (import.meta && import.meta.env && import.meta.env.VITE_PUBLIC_URL) { return import.meta.env.VITE_PUBLIC_URL; } // 默认返回空字符串 return ''; } /** 清理资源 */ dispose() { this.stopRollerDoorAnimation(); this.clearYAxisClip(); this.rollerDoorMeshes = []; this.rollerDoorInitialY.clear(); this.rollerDoorIsOpen = false; // 清理 rollerDoorGroup if (this.rollerDoorGroup && this.rollerDoorGroup.dispose) { this.rollerDoorGroup.dispose(); this.rollerDoorGroup = null; } // 清理所有材质资源 this.materialDic.Values().forEach((material) => { if (material && material.dispose) { material.dispose(); } }); this.materialDic.Clear(); // 清理所有贴图资源 this.clearTextures(this.oldTextureDic); // 清理所有网格 this.meshDic.Values().forEach((mesh) => { if (mesh && mesh.dispose) { mesh.dispose(); } }); this.meshDic.Clear(); // 清空失败贴图记录 this.failedTextures = []; } /** 更新 */ update() { } /** 尝试创建贴图的方法,支持多种格式回退 */ private createTextureWithFallback(texturePath: string): Texture | null { const failureReasons: string[] = []; try { const texture = new Texture(texturePath); if (texture) { return texture; } else { failureReasons.push(`原始路径创建失败: ${texturePath}`); throw new Error('Texture creation returned null'); } } catch (error: any) { const errorMessage = error.message || error.toString(); // 特别处理KTX错误 if (errorMessage.includes('KTX identifier') || errorMessage.includes('missing KTX') || (texturePath.toLowerCase().endsWith('.ktx2') && errorMessage)) { this.failedTextures.push({ path: texturePath, textureType: 'KTX2', error: `KTX错误: ${errorMessage}`, timestamp: new Date() }); } failureReasons.push(`原始路径加载异常: ${texturePath} - ${errorMessage}`); // 如果是ktx2文件加载失败,尝试查找对应的jpg/png文件 if (texturePath.toLowerCase().endsWith('.ktx2')) { // 尝试jpg格式 const jpgPath = texturePath.replace(/\.ktx2$/i, '.jpg'); try { const jpgTexture = new Texture(jpgPath); if (jpgTexture) { return jpgTexture; } } catch (jpgError: any) { failureReasons.push(`JPG回退失败: ${jpgPath} - ${jpgError}`); } // 尝试png格式 const pngPath = texturePath.replace(/\.ktx2$/i, '.png'); try { const pngTexture = new Texture(pngPath); if (pngTexture) { return pngTexture; } } catch (pngError: any) { failureReasons.push(`PNG回退失败: ${pngPath} - ${pngError}`); } } // 所有格式都失败,记录详细失败信息 this.failedTextures.push({ path: texturePath, textureType: '回退机制', error: failureReasons.join('; '), timestamp: new Date() }); return null; } } /** * 应用材质属性 * @param options 材质配置选项 */ applyMaterial(options: { target: string; modelId?: string; albedoColor?: string; albedoTexture?: string; normalMap?: string; metallicTexture?: string; roughness?: number; metallic?: number; }): void { this.updateDictionaries(); // 查找目标材质(支持精确匹配和前缀匹配) const targetMaterials: PBRMaterial[] = []; // 如果提供了 modelId,只查找该模型的材质 if (options.modelId) { // 获取该模型的所有 meshes const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId); if (!modelMeshes || modelMeshes.length === 0) { console.warn(`Model not found: ${options.modelId}`); return; } // 遍历该模型的所有 mesh,查找匹配的材质 modelMeshes.forEach((mesh: AbstractMesh) => { if (mesh.material && mesh.material instanceof PBRMaterial) { const material = mesh.material as PBRMaterial; if (material.name === options.target || material.name.startsWith(`${options.target}_`)) { // 避免重复添加 if (!targetMaterials.includes(material)) { targetMaterials.push(material); } } } }); } else { // 没有提供 modelId,全局查找(保持向后兼容) this.materialDic.Values().forEach(material => { if (material.name === options.target || material.name.startsWith(`${options.target}_`)) { targetMaterials.push(material); } }); } if (targetMaterials.length === 0) { console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`); return; } // 应用材质属性到目标材质 targetMaterials.forEach(material => { // 应用颜色 if (options.albedoColor) { const color = Color3.FromHexString(options.albedoColor); material.albedoColor.copyFrom(color); } // 应用反照率纹理(颜色贴图) if (options.albedoTexture !== undefined) { if (options.albedoTexture) { material.albedoTexture = new Texture(options.albedoTexture); } else { // 传入空字符串或 null 时清空贴图 material.albedoTexture = null; } } // 应用法线贴图 if (options.normalMap !== undefined) { if (options.normalMap) { material.bumpTexture = new Texture(options.normalMap); } else { // 传入空字符串或 null 时清空贴图 material.bumpTexture = null; } } // 应用金属度贴图 if (options.metallicTexture !== undefined) { if (options.metallicTexture) { material.metallicTexture = new Texture(options.metallicTexture); } else { // 传入空字符串或 null 时清空贴图 material.metallicTexture = null; } } // 应用粗糙度值 if (options.roughness !== undefined) { material.roughness = options.roughness; } // 应用金属度值 if (options.metallic !== undefined) { material.metallic = options.metallic; } // 强制刷新材质 material.markDirty(); }); } }