381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader';
|
||
import '@babylonjs/loaders/glTF';
|
||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||
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';
|
||
|
||
type LoadResult = {
|
||
success: boolean;
|
||
meshes?: AbstractMesh[];
|
||
skeletons?: unknown[];
|
||
error?: string;
|
||
};
|
||
|
||
type ModelConfig = {
|
||
name: string;
|
||
url: string;
|
||
};
|
||
|
||
/**
|
||
* 模型管理类 - 负责加载、缓存和管理3D模型
|
||
*/
|
||
export class AppModel extends Monobehiver {
|
||
private modelDic: Dictionary<AbstractMesh[]>;
|
||
private loadedMeshes: AbstractMesh[];
|
||
private isLoading: boolean;
|
||
|
||
constructor(mainApp: any) {
|
||
super(mainApp);
|
||
this.modelDic = new Dictionary<AbstractMesh[]>();
|
||
this.loadedMeshes = [];
|
||
this.isLoading = false;
|
||
}
|
||
|
||
initManagers(): void {
|
||
// 预留接口
|
||
}
|
||
|
||
/** 加载配置中的所有模型 */
|
||
async loadModel(): Promise<void> {
|
||
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<void> {
|
||
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<LoadResult> {
|
||
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 modelName 模型名称
|
||
*/
|
||
private cloneMaterials(meshes: AbstractMesh[], modelName: string): void {
|
||
meshes.forEach(mesh => {
|
||
if (mesh.material) {
|
||
const originalMaterial = mesh.material;
|
||
const clonedMaterial = originalMaterial.clone(`${originalMaterial.name}_${modelName}`);
|
||
mesh.material = clonedMaterial;
|
||
}
|
||
});
|
||
}
|
||
/** 为网格设置阴影(投射和接收) */
|
||
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 modelName 模型名称 或 模型配置数组
|
||
* @param modelUrl 模型URL(单个模型时使用)
|
||
*/
|
||
async add(
|
||
modelName: string | ModelConfig[],
|
||
modelUrl?: string
|
||
): Promise<LoadResult | { success: boolean; results: LoadResult[] }> {
|
||
// 批量加载
|
||
if (Array.isArray(modelName)) {
|
||
return await this.addMultiple(modelName);
|
||
}
|
||
|
||
// 单个加载
|
||
if (!modelUrl) {
|
||
return { success: false, error: '缺少模型URL参数' };
|
||
}
|
||
|
||
return await this.addSingle(modelName, modelUrl);
|
||
}
|
||
|
||
/**
|
||
* 添加单个模型
|
||
*/
|
||
private async addSingle(modelName: string, modelUrl: string): Promise<LoadResult> {
|
||
// 检查是否已存在
|
||
const existingMeshes = this.modelDic.Get(modelName);
|
||
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
||
console.log(`模型 ${modelName} 已存在,直接显示`);
|
||
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, modelName);
|
||
this.modelDic.Set(modelName, result.meshes);
|
||
|
||
// 更新 GameManager 的字典
|
||
this.mainApp.gameManager?.updateDictionaries();
|
||
|
||
EventBridge.modelLoaded({ urls: [modelUrl] });
|
||
} else {
|
||
EventBridge.modelLoadError({ url: modelUrl, error: result.error });
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 批量添加模型
|
||
*/
|
||
private async addMultiple(models: ModelConfig[]): 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 { name, url } = models[i];
|
||
|
||
const result = await this.loadSingleModel(url, (event) => {
|
||
this.emitProgress(i, total, url, event);
|
||
});
|
||
|
||
if (result.success && result.meshes) {
|
||
this.cloneMaterials(result.meshes, name);
|
||
this.modelDic.Set(name, result.meshes);
|
||
}
|
||
|
||
results.push(result);
|
||
this.emitProgress(i + 1, total, url, null, result.success);
|
||
}
|
||
|
||
// 批量加载完成后统一更新字典
|
||
this.mainApp.gameManager?.updateDictionaries();
|
||
|
||
EventBridge.modelLoaded({ urls: models.map(m => m.url) });
|
||
|
||
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 modelName 模型名称
|
||
* @param newModelUrl 新模型URL
|
||
*/
|
||
async replaceModel(modelName: string, newModelUrl: string): Promise<LoadResult> {
|
||
console.log( modelName,this.modelDic);
|
||
|
||
this.removeByName(modelName);
|
||
return await this.addSingle(modelName, newModelUrl);
|
||
}
|
||
|
||
/**
|
||
* 销毁指定模型
|
||
* @param modelName 模型名称
|
||
*/
|
||
removeByName(modelName: string): void {
|
||
const meshes = this.modelDic.Get(modelName);
|
||
if (!meshes?.length) {
|
||
console.warn(`Model not found: ${modelName}`);
|
||
return;
|
||
}
|
||
|
||
meshes.forEach(mesh => mesh.dispose());
|
||
this.modelDic.Remove(modelName);
|
||
console.log(`Model removed: ${modelName}`);
|
||
}
|
||
}
|