Files
zhengte.babylonjs-sdk/src/babylonjs/AppModel.ts

750 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<AbstractMesh[]>;
private modelMetadataDic: Dictionary<ModelMetadata>;
private loadedMeshes: AbstractMesh[];
private isLoading: boolean;
constructor(mainApp: any) {
super(mainApp);
this.modelDic = new Dictionary<AbstractMesh[]>();
this.modelMetadataDic = new Dictionary<ModelMetadata>();
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 modelId 模型ID
*/
private cloneMaterials(meshes: AbstractMesh[], modelId: string): void {
const scene = this.mainApp.appScene.object;
const clonedMaterials = new Map<string, any>();
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<AbstractMesh>(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<LoadResult | { success: boolean; results: LoadResult[] }> {
// 批量加载
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<LoadResult> {
// 检查是否已存在
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);
meshes.forEach(mesh => {
console.log(mesh.uniqueId);
console.log(mesh.name);
});
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<LoadResult> {
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): void {
const meshes = this.modelDic.Get(modelName);
if (!meshes?.length) {
console.warn(`Model not found: ${modelName}`);
return;
}
this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true));
this.modelDic.Remove(modelName);
this.modelMetadataDic.Remove(modelName);
this.mainApp.gameManager?.updateDictionaries();
}
/**
* 清除所有已添加的模型并释放内存
* 主要用于切换尺寸后清除不适用的配件
*/
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<AbstractMesh>(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);
}
}
}