This commit is contained in:
2026-03-11 11:56:46 +08:00
parent ae59fbe68b
commit 7fdbf19951
9 changed files with 613 additions and 12 deletions

View File

@ -45,7 +45,7 @@
const config = { const config = {
container: document.querySelector('#renderDom'), container: document.querySelector('#renderDom'),
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'], 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); kernel.init(config);
@ -60,6 +60,20 @@
kernel.on('model:loaded', (data) => { kernel.on('model:loaded', (data) => {
console.log('模型加载完成', 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 @@
</script> </script>
</body> </body>

View File

@ -21,13 +21,18 @@ export class AppCamera extends Monobehiver {
const canvas = AppConfig.container; const canvas = AppConfig.container;
if (!scene || !canvas) return; if (!scene || !canvas) return;
// 创建弧形旋转相机水平角70度垂直角80度距离5目标点(0,1,0) // 创建弧形旋转相机水平角70度垂直角85度接近上帝视角距离5目标点(0,2,0)
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(80), 5, new Vector3(0, 0, 0), scene); 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.attachControl(canvas, true);
this.object.minZ = 0.01; // 近裁剪面 this.object.minZ = 0.01; // 近裁剪面
this.object.wheelPrecision =999999; // 滚轮缩放精度 this.object.wheelPrecision =999999; // 滚轮缩放精度
this.object.panningSensibility = 0; 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); this.setTarget(0, 2, 0);
} }
@ -43,8 +48,11 @@ export class AppCamera extends Monobehiver {
/** 重置相机到默认位置 */ /** 重置相机到默认位置 */
reset(): void { reset(): void {
if (!this.object) return; if (!this.object) return;
this.object.radius = 2; this.setTarget(0, 0, 0); this.object.radius = 5;
this.object.position = new Vector3(0, 1.5, 2); 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 { update(): void {

View File

@ -149,4 +149,29 @@ export class AppModel extends Monobehiver {
this.skeletonMerged = false; 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}`);
}
} }

View File

@ -119,6 +119,25 @@ class AppRay extends Monobehiver {
console.error('清除高亮失败:', error) 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 } export { AppRay }

View File

@ -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<PBRMaterial>;
private meshDic: Dictionary<any>;
private oldTextureDic: Dictionary<any>;
// 记录加载失败的贴图
private failedTextures: Array<{
path: string;
materialName?: string;
textureType?: string;
error?: string;
timestamp: Date;
}>;
constructor(mainApp: any) {
super(mainApp);
this.materialDic = new Dictionary<PBRMaterial>();
this.meshDic = new Dictionary<any>();
this.oldTextureDic = new Dictionary<any>();
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<void>[] = [];
// 处理每个组件
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<any>): Promise<void> {
return new Promise<void>((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<void> {
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}`);
// 这里需要根据实际的材质系统实现
});
}
}

View File

@ -11,6 +11,7 @@ import { AppEnv } from './AppEnv';
import { AppModel } from './AppModel'; import { AppModel } from './AppModel';
import { AppConfig } from './AppConfig'; import { AppConfig } from './AppConfig';
import { AppRay } from './AppRay'; import { AppRay } from './AppRay';
import { GameManager } from './GameManager';
import { EventBridge } from '../event/bridge'; import { EventBridge } from '../event/bridge';
/** /**
@ -25,6 +26,7 @@ export class MainApp {
appLight: AppLight; appLight: AppLight;
appEnv: AppEnv; appEnv: AppEnv;
appRay: AppRay; appRay: AppRay;
gameManager: GameManager;
constructor() { constructor() {
@ -35,6 +37,7 @@ export class MainApp {
this.appLight = new AppLight(this); this.appLight = new AppLight(this);
this.appEnv = new AppEnv(this); this.appEnv = new AppEnv(this);
this.appRay = new AppRay(this); this.appRay = new AppRay(this);
this.gameManager = new GameManager(this);
window.addEventListener("resize", () => this.appEngin.handleResize()); window.addEventListener("resize", () => this.appEngin.handleResize());
} }
@ -44,13 +47,15 @@ export class MainApp {
* @param config 配置对象 * @param config 配置对象
*/ */
loadAConfig(config: any): void { loadAConfig(config: any): void {
AppConfig.container = config.container ; AppConfig.container = config.container;
AppConfig.modelUrlList = config.modelUrlList || []; AppConfig.modelUrlList = config.modelUrlList || [];
AppConfig.env = config.env; AppConfig.env = config.env;
} }
loadModel(): void { async loadModel(): Promise<void> {
this.appModel.loadModel(); await this.appModel.loadModel();
await this.gameManager.Awake();
EventBridge.allReady({ scene: this.appScene.object });
} }
/** 唤醒/初始化所有子模块 */ /** 唤醒/初始化所有子模块 */

View File

@ -31,6 +31,9 @@ export class EventBridge {
static sceneReady(payload: SceneReadyPayload): Emitter { static sceneReady(payload: SceneReadyPayload): Emitter {
return emit("scene:ready", payload); return emit("scene:ready", payload);
} }
static allReady(payload: SceneReadyPayload): Emitter {
return emit("all:ready", payload);
}
// Listeners // Listeners
static onModelLoadProgress(callback: (payload: ModelLoadProgressPayload) => void, context?: unknown): Emitter { 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 { static onSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
return on("scene:ready", callback, context); 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 { static onceSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
return once("scene:ready", callback, context); return once("scene:ready", callback, context);
} }

45
src/kernel/Adapter.ts Normal file
View File

@ -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);
}
};
}

View File

@ -4,6 +4,7 @@ import configurator, { ConfiguratorParams } from './components/conf';
import auth from './components/auth'; import auth from './components/auth';
import { on, off, once, emit } from './event/bus'; import { on, off, once, emit } from './event/bus';
import { EventBridge } from './event/bridge'; import { EventBridge } from './event/bridge';
import { KernelAdapter } from './kernel/Adapter';
declare global { declare global {
interface Window { interface Window {
@ -25,6 +26,7 @@ type InitParams = {
}; };
let mainApp: MainApp | null = null; let mainApp: MainApp | null = null;
let kernelAdapter: KernelAdapter | null = null;
const kernel = { const kernel = {
// 事件工具,提供给外部订阅/退订 // 事件工具,提供给外部订阅/退订
@ -37,6 +39,10 @@ const kernel = {
if (!params) { console.error('params is required'); return; } if (!params) { console.error('params is required'); return; }
mainApp = new MainApp(); mainApp = new MainApp();
kernelAdapter = new KernelAdapter(mainApp);
// 展开转接器的属性和方法到kernel对象
Object.assign(kernel, kernelAdapter);
const container = (typeof params.container === 'string' const container = (typeof params.container === 'string'
? (document.querySelector(params.container) || document.getElementById(params.container)) ? (document.querySelector(params.container) || document.getElementById(params.container))
@ -52,7 +58,7 @@ const kernel = {
await mainApp.Awake(); await mainApp.Awake();
await mainApp.loadModel(); await mainApp.loadModel();
}, }
}; };
if (!window.faceSDK) { if (!window.faceSDK) {