diff --git a/index.html b/index.html index bb4b2fc..cbd5059 100644 --- a/index.html +++ b/index.html @@ -45,7 +45,7 @@ const config = { container: document.querySelector('#renderDom'), modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'], - env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3,background: false }, + env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: false }, }; kernel.init(config); @@ -60,6 +60,20 @@ kernel.on('model:loaded', (data) => { console.log('模型加载完成', data); + + + + + }); + + kernel.on('all:ready', (data) => { + console.log('所有模块加载完成', data); + kernel.material.apply({ + target: 'Material__2', + attribute: 'alpha', + value: 0.5, + }); + }); @@ -70,7 +84,6 @@ - diff --git a/src/babylonjs/AppCamera.ts b/src/babylonjs/AppCamera.ts index 464aac0..5626135 100644 --- a/src/babylonjs/AppCamera.ts +++ b/src/babylonjs/AppCamera.ts @@ -21,13 +21,18 @@ export class AppCamera extends Monobehiver { const canvas = AppConfig.container; if (!scene || !canvas) return; - // 创建弧形旋转相机:水平角70度,垂直角80度,距离5,目标点(0,1,0) - this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(80), 5, new Vector3(0, 0, 0), scene); + // 创建弧形旋转相机:水平角70度,垂直角85度(接近上帝视角),距离5,目标点(0,2,0) + this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene); this.object.attachControl(canvas, true); this.object.minZ = 0.01; // 近裁剪面 this.object.wheelPrecision =999999; // 滚轮缩放精度 this.object.panningSensibility = 0; - this.object.position = new Vector3(-0, 0, 100); + + // 限制垂直角范围,实现上帝视角 + this.object.upperBetaLimit = Tools.ToRadians(60); // 最大垂直角(接近90度,避免万向锁) + this.object.lowerBetaLimit = Tools.ToRadians(60); // 最小垂直角 + + this.object.position = new Vector3(-0, 100, 0); this.setTarget(0, 2, 0); } @@ -43,8 +48,11 @@ export class AppCamera extends Monobehiver { /** 重置相机到默认位置 */ reset(): void { if (!this.object) return; - this.object.radius = 2; this.setTarget(0, 0, 0); - this.object.position = new Vector3(0, 1.5, 2); + this.object.radius = 5; + this.object.alpha = Tools.ToRadians(60); // 水平角 + this.object.beta = Tools.ToRadians(60); // 垂直角(上帝视角) + this.setTarget(0, 2, 0); + this.object.position = new Vector3(-0, 100, 0); } update(): void { diff --git a/src/babylonjs/AppModel.ts b/src/babylonjs/AppModel.ts index af1795f..ae86f3f 100644 --- a/src/babylonjs/AppModel.ts +++ b/src/babylonjs/AppModel.ts @@ -149,4 +149,29 @@ export class AppModel extends Monobehiver { this.skeletonMerged = false; } + /** + * 销毁指定模型 + * @param modelName 模型名称 + */ + destroyModel(modelName: string): void { + // 遍历模型字典,查找匹配的模型 + const keys = this.modelDic.Keys(); + for (const key of keys) { + if (key.includes(modelName)) { + const meshes = this.modelDic.Get(key); + if (meshes) { + // 销毁所有网格 + meshes.forEach(mesh => mesh?.dispose()); + // 从字典中移除 + this.modelDic.Remove(key); + // 从加载的网格列表中移除 + this.loadedMeshes = this.loadedMeshes.filter(mesh => !meshes.includes(mesh)); + console.log(`Model destroyed: ${modelName}`); + return; + } + } + } + console.warn(`Model not found: ${modelName}`); + } + } diff --git a/src/babylonjs/AppRay.ts b/src/babylonjs/AppRay.ts index 66edfcc..1f05fc1 100644 --- a/src/babylonjs/AppRay.ts +++ b/src/babylonjs/AppRay.ts @@ -119,6 +119,25 @@ class AppRay extends Monobehiver { console.error('清除高亮失败:', error) } } + + /** + * 渲染热点 + * @param hotspots 热点数据 + */ + renderHotspots(hotspots: any[]): void { + console.log('Rendering hotspots:', hotspots); + + // 这里需要根据实际的热点渲染逻辑实现 + // 示例实现: + // 1. 清除现有的热点 + // 2. 根据热点数据创建新的热点标记 + // 3. 为热点添加交互事件 + + hotspots.forEach((hotspot, index) => { + console.log(`Rendering hotspot ${index}:`, hotspot); + // 这里需要根据实际的热点数据结构实现 + }); + } } export { AppRay } diff --git a/src/babylonjs/GameManager.ts b/src/babylonjs/GameManager.ts new file mode 100644 index 0000000..1f05dc4 --- /dev/null +++ b/src/babylonjs/GameManager.ts @@ -0,0 +1,475 @@ +import { Mesh, PBRMaterial, Texture, AbstractMesh } from "@babylonjs/core"; +import { Monobehiver } from '../base/Monobehiver'; +import { Dictionary } from '../utils/Dictionary'; +import { AppConfig } from './AppConfig'; + +/** + * 游戏管理器类 - 负责管理游戏逻辑、材质和纹理 + */ +export class GameManager extends Monobehiver { + private materialDic: Dictionary; + private meshDic: Dictionary; + private oldTextureDic: Dictionary; + + // 记录加载失败的贴图 + 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.failedTextures = []; + } + + /** 初始化游戏管理器 */ + async Awake() { + const scene = this.mainApp.appScene?.object; + if (!scene) { + console.warn('Scene not found'); + return; + } + + // 初始化材质字典 + for (const mat of scene.materials) { + if (!this.materialDic.Has(mat.name)) { + // 初始化材质属性 + mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND; + this.materialDic.Set(mat.name, mat as PBRMaterial); + } + } + + // 初始化网格字典 + for (const mesh of scene.meshes) { + if (mesh instanceof Mesh) { + + this.meshDic.Set(mesh.name, mesh); + } + } + console.log('材质字典:', this.materialDic); + } + + /** 初始化设置材质 */ + 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); + } + } + + /** 获取公共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.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 target 目标对象 + * @param material 材质路径 + */ + applyMaterial(target: string, attribute: string, value: number | string): void { + // 这里需要根据实际的材质管理逻辑实现 + console.log(`Applying attribute ${attribute} to ${value}`); + + // 示例实现:根据目标和材质路径应用材质 + // 1. 查找目标网格 + const targetMaterials: PBRMaterial[] = []; + this.materialDic.Values().forEach(material => { + if (material.name.includes(target)) { + console.log(`${this.materialDic.Get(material.name)}`,material); + targetMaterials.push(material); + } + }); + + if (targetMaterials.length === 0) { + console.warn(`Target not found: ${target}`); + return; + } + + // 2. 处理材质路径 + // 这里可以根据材质路径加载对应的材质配置 + // 例如:paint/blue 可以映射到特定的材质配置 + + // 3. 应用材质到目标网格 + targetMaterials.forEach(material => { + if (material[attribute]) { + material[attribute] = value; + } + console.log(`Applying attribute ${attribute} to ${value} to mesh: ${material.name}`); + // 这里需要根据实际的材质系统实现 + }); + } +} diff --git a/src/babylonjs/MainApp.ts b/src/babylonjs/MainApp.ts index f51cb82..d892bb1 100644 --- a/src/babylonjs/MainApp.ts +++ b/src/babylonjs/MainApp.ts @@ -11,6 +11,7 @@ import { AppEnv } from './AppEnv'; import { AppModel } from './AppModel'; import { AppConfig } from './AppConfig'; import { AppRay } from './AppRay'; +import { GameManager } from './GameManager'; import { EventBridge } from '../event/bridge'; /** @@ -25,6 +26,7 @@ export class MainApp { appLight: AppLight; appEnv: AppEnv; appRay: AppRay; + gameManager: GameManager; constructor() { @@ -35,6 +37,7 @@ export class MainApp { this.appLight = new AppLight(this); this.appEnv = new AppEnv(this); this.appRay = new AppRay(this); + this.gameManager = new GameManager(this); window.addEventListener("resize", () => this.appEngin.handleResize()); } @@ -44,13 +47,15 @@ export class MainApp { * @param config 配置对象 */ loadAConfig(config: any): void { - AppConfig.container = config.container ; + AppConfig.container = config.container; AppConfig.modelUrlList = config.modelUrlList || []; AppConfig.env = config.env; } - loadModel(): void { - this.appModel.loadModel(); + async loadModel(): Promise { + await this.appModel.loadModel(); + await this.gameManager.Awake(); + EventBridge.allReady({ scene: this.appScene.object }); } /** 唤醒/初始化所有子模块 */ diff --git a/src/event/bridge.ts b/src/event/bridge.ts index 2726fa2..5063603 100644 --- a/src/event/bridge.ts +++ b/src/event/bridge.ts @@ -31,6 +31,9 @@ export class EventBridge { static sceneReady(payload: SceneReadyPayload): Emitter { return emit("scene:ready", payload); } + static allReady(payload: SceneReadyPayload): Emitter { + return emit("all:ready", payload); + } // Listeners static onModelLoadProgress(callback: (payload: ModelLoadProgressPayload) => void, context?: unknown): Emitter { @@ -52,7 +55,9 @@ export class EventBridge { static onSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter { return on("scene:ready", callback, context); } - + static onAllReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter { + return on("all:ready", callback, context); + } static onceSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter { return once("scene:ready", callback, context); } diff --git a/src/kernel/Adapter.ts b/src/kernel/Adapter.ts new file mode 100644 index 0000000..126d761 --- /dev/null +++ b/src/kernel/Adapter.ts @@ -0,0 +1,45 @@ +import { MainApp } from '../babylonjs/MainApp'; + +/** + * Kernel 转接器类 - 封装 mainApp 的功能,提供统一的 API 接口 + */ +export class KernelAdapter { + private mainApp: MainApp; + + constructor(mainApp: MainApp) { + this.mainApp = mainApp; + } + + /** 模型管理 */ + model = { + /** + * 销毁指定模型 + * @param modelName 模型名称 + */ + destroy: (modelName: string): void => { + this.mainApp.appModel.destroyModel(modelName); + } + }; + + /** 材质管理 */ + material = { + /** + * 应用材质 + * @param options 材质应用选项 + */ + apply: (options: { target: string; attribute: string,value:number|string }): void => { + this.mainApp.gameManager.applyMaterial(options.target, options.attribute,options.value); + } + }; + + /** 热点管理 */ + hotspot = { + /** + * 渲染热点 + * @param hotspots 热点数据 + */ + render: (hotspots: any[]): void => { + this.mainApp.appRay.renderHotspots(hotspots); + } + }; +} diff --git a/src/main.ts b/src/main.ts index 69e4ec2..6b898af 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import configurator, { ConfiguratorParams } from './components/conf'; import auth from './components/auth'; import { on, off, once, emit } from './event/bus'; import { EventBridge } from './event/bridge'; +import { KernelAdapter } from './kernel/Adapter'; declare global { interface Window { @@ -25,6 +26,7 @@ type InitParams = { }; let mainApp: MainApp | null = null; +let kernelAdapter: KernelAdapter | null = null; const kernel = { // 事件工具,提供给外部订阅/退订 @@ -37,6 +39,10 @@ const kernel = { if (!params) { console.error('params is required'); return; } mainApp = new MainApp(); + kernelAdapter = new KernelAdapter(mainApp); + + // 展开转接器的属性和方法到kernel对象 + Object.assign(kernel, kernelAdapter); const container = (typeof params.container === 'string' ? (document.querySelector(params.container) || document.getElementById(params.container)) @@ -52,7 +58,7 @@ const kernel = { await mainApp.Awake(); await mainApp.loadModel(); - }, + } }; if (!window.faceSDK) {