1
This commit is contained in:
198
src/babylonjs/AppDomTo3D.ts
Normal file
198
src/babylonjs/AppDomTo3D.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { Vector3, Matrix } from '@babylonjs/core/Maths/math.vector';
|
||||
import { Scene } from '@babylonjs/core/scene';
|
||||
import { Camera } from '@babylonjs/core/Cameras/camera';
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { PointerEventTypes, PointerInfo } from '@babylonjs/core';
|
||||
|
||||
interface DomElement {
|
||||
dom: HTMLElement;
|
||||
position: Vector3;
|
||||
offset?: { x: number; y: number };
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM 2D转3D坐标管理类
|
||||
* 将DOM元素固定在3D场景的特定坐标上
|
||||
*/
|
||||
export class AppDomTo3D extends Monobehiver {
|
||||
private domElements: Map<string, DomElement>;
|
||||
private scene: Scene | null;
|
||||
private camera: Camera | null;
|
||||
private pointerDownPos: Vector3;
|
||||
private pointerUpPos: Vector3;
|
||||
|
||||
constructor(mainApp: any) {
|
||||
super(mainApp);
|
||||
this.domElements = new Map();
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.pointerDownPos = Vector3.Zero();
|
||||
this.pointerUpPos = Vector3.Zero();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init(): void {
|
||||
this.scene = this.mainApp.appScene.object;
|
||||
this.camera = this.mainApp.appCamera.object;
|
||||
|
||||
// 监听场景点击事件,点击空白处隐藏DOM
|
||||
if (this.scene) {
|
||||
this.scene.onPointerObservable.add((pointerInfo: PointerInfo) => {
|
||||
const { type, event, pickInfo } = pointerInfo;
|
||||
|
||||
if (type === PointerEventTypes.POINTERDOWN) {
|
||||
this.pointerDownPos.set(event.clientX, 0, event.clientY);
|
||||
} else if (type === PointerEventTypes.POINTERUP) {
|
||||
this.pointerUpPos.set(event.clientX, 0, event.clientY);
|
||||
const distance = Vector3.Distance(this.pointerDownPos, this.pointerUpPos);
|
||||
|
||||
// 只有在没有移动的情况下才处理单击(距离小于5像素)
|
||||
if (distance < 5) {
|
||||
// 如果没有点击到任何物体,隐藏所有DOM
|
||||
if (!pickInfo || !pickInfo.hit) {
|
||||
this.hideAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加DOM元素到3D坐标
|
||||
* @param id 唯一标识符
|
||||
* @param dom DOM元素
|
||||
* @param position 3D坐标 [x, y, z]
|
||||
* @param offset 2D偏移量 { x, y },可选
|
||||
*/
|
||||
attach(id: string, dom: HTMLElement, position: [number, number, number], offset?: { x: number; y: number }): void {
|
||||
const vector3 = new Vector3(position[0], position[1], position[2]);
|
||||
|
||||
// 设置DOM样式
|
||||
dom.style.position = 'absolute';
|
||||
dom.style.pointerEvents = 'auto';
|
||||
dom.style.zIndex = '1000';
|
||||
dom.style.display = 'block';
|
||||
|
||||
// 存储DOM元素信息
|
||||
this.domElements.set(id, {
|
||||
dom,
|
||||
position: vector3,
|
||||
offset: offset || { x: 0, y: 0 },
|
||||
visible: true
|
||||
});
|
||||
|
||||
// 立即更新一次位置
|
||||
this.updateSingleDomPosition(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除DOM元素
|
||||
* @param id 唯一标识符
|
||||
*/
|
||||
detach(id: string): void {
|
||||
const element = this.domElements.get(id);
|
||||
if (element) {
|
||||
element.dom.style.display = 'none';
|
||||
this.domElements.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新DOM元素的3D坐标
|
||||
* @param id 唯一标识符
|
||||
* @param position 新的3D坐标 [x, y, z]
|
||||
*/
|
||||
updatePosition(id: string, position: [number, number, number]): void {
|
||||
const element = this.domElements.get(id);
|
||||
if (element) {
|
||||
element.position.set(position[0], position[1], position[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有DOM元素的位置
|
||||
*/
|
||||
updateDomPositions(): void {
|
||||
this.domElements.forEach((_, id) => {
|
||||
this.updateSingleDomPosition(id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个DOM元素的位置
|
||||
*/
|
||||
private updateSingleDomPosition(id: string): void {
|
||||
const element = this.domElements.get(id);
|
||||
if (!element || !this.scene || !this.camera) return;
|
||||
|
||||
const { dom, position, offset, visible } = element;
|
||||
|
||||
// 如果标记为不可见,直接隐藏
|
||||
if (!visible) {
|
||||
dom.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// 将3D坐标转换为2D屏幕坐标
|
||||
const engine = this.scene.getEngine();
|
||||
const width = engine.getRenderWidth();
|
||||
const height = engine.getRenderHeight();
|
||||
|
||||
// 使用正确的矩阵:单位矩阵 + 变换矩阵
|
||||
const worldMatrix = Matrix.Identity();
|
||||
const transformMatrix = this.scene.getTransformMatrix();
|
||||
const viewport = this.camera.viewport.toGlobal(width, height);
|
||||
|
||||
const screenPos = Vector3.Project(
|
||||
position,
|
||||
worldMatrix,
|
||||
transformMatrix,
|
||||
viewport
|
||||
);
|
||||
|
||||
// 检查是否在相机视野内
|
||||
if (screenPos.z < 0 || screenPos.z > 1) {
|
||||
console.log('DOM 不在视野内,隐藏');
|
||||
dom.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用偏移量并更新DOM位置
|
||||
dom.style.display = 'block';
|
||||
dom.style.left = `${screenPos.x + (offset?.x || 0)}px`;
|
||||
dom.style.top = `${screenPos.y + (offset?.y || 0)}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有DOM元素
|
||||
*/
|
||||
clean(): void {
|
||||
this.domElements.forEach((element) => {
|
||||
element.dom.style.display = 'none';
|
||||
});
|
||||
this.domElements.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已附加的DOM元素ID列表
|
||||
*/
|
||||
getAttachedIds(): string[] {
|
||||
return Array.from(this.domElements.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏所有DOM元素
|
||||
*/
|
||||
hideAll(): void {
|
||||
console.log('hideAll 被调用,当前元素数量:', this.domElements.size);
|
||||
this.domElements.forEach((element, id) => {
|
||||
console.log('隐藏元素:', id, element.dom);
|
||||
element.visible = false;
|
||||
element.dom.style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Load
|
||||
import '@babylonjs/loaders/glTF';
|
||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||
import { Scene } from '@babylonjs/core/scene';
|
||||
import { ActionManager, ExecuteCodeAction } from '@babylonjs/core/Actions';
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { Dictionary } from '../utils/Dictionary';
|
||||
import { AppConfig } from './AppConfig';
|
||||
@ -15,75 +14,37 @@ type LoadResult = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type ModelConfig = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 模型管理类- 负责加载、缓存和管理3D模型
|
||||
* 模型管理类 - 负责加载、缓存和管理3D模型
|
||||
*/
|
||||
export class AppModel extends Monobehiver {
|
||||
private modelDic: Dictionary<AbstractMesh>;
|
||||
private modelDic: Dictionary<AbstractMesh[]>;
|
||||
private loadedMeshes: AbstractMesh[];
|
||||
private skeletonManager: any;
|
||||
private outfitManager: any;
|
||||
private isLoading: boolean;
|
||||
private skeletonMerged: boolean;
|
||||
|
||||
constructor(mainApp: any) {
|
||||
super(mainApp);
|
||||
this.modelDic = new Dictionary<AbstractMesh>();
|
||||
this.modelDic = new Dictionary<AbstractMesh[]>();
|
||||
this.loadedMeshes = [];
|
||||
this.skeletonManager = null;
|
||||
this.outfitManager = null;
|
||||
this.isLoading = false;
|
||||
this.skeletonMerged = false;
|
||||
}
|
||||
|
||||
/** 初始化子管理器(占位:实际实现已移除) */
|
||||
initManagers(): void {
|
||||
// 这里原本会初始化 SkeletonManager 和 OutfitManager,已留空以避免恢复已删除的实现
|
||||
// 预留接口
|
||||
}
|
||||
|
||||
/** 加载配置中的所有模型 */
|
||||
async loadModel(): Promise<void> {
|
||||
if (!AppConfig.modelUrlList?.length || this.isLoading) return;
|
||||
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const total = AppConfig.modelUrlList.length;
|
||||
EventBridge.modelLoadProgress({ loaded: 0, total, urls: AppConfig.modelUrlList, progress: 0, percentage: 0 });
|
||||
for (let i = 0; i < AppConfig.modelUrlList.length; i++) {
|
||||
const url = AppConfig.modelUrlList[i];
|
||||
const handleProgress = (event: ISceneLoaderProgressEvent): void => {
|
||||
const currentProgress = event.lengthComputable && event.total > 0
|
||||
? Math.min(1, event.loaded / event.total)
|
||||
: 0;
|
||||
const overallProgress = Math.min(1, (i + currentProgress) / total);
|
||||
EventBridge.modelLoadProgress({
|
||||
loaded: i + currentProgress,
|
||||
total,
|
||||
url,
|
||||
progress: overallProgress,
|
||||
percentage: Number((overallProgress * 100).toFixed(2)),
|
||||
detail: {
|
||||
url,
|
||||
lengthComputable: event.lengthComputable,
|
||||
loadedBytes: event.loaded,
|
||||
totalBytes: event.total
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const result = await this.loadSingleModel(url, handleProgress);
|
||||
const overallProgress = Math.min(1, (i + 1) / total);
|
||||
EventBridge.modelLoadProgress({
|
||||
loaded: i + 1,
|
||||
total,
|
||||
url,
|
||||
success: result.success,
|
||||
progress: overallProgress,
|
||||
percentage: Number((overallProgress * 100).toFixed(2))
|
||||
});
|
||||
if (!result.success) {
|
||||
EventBridge.modelLoadError({ url, error: result.error });
|
||||
}
|
||||
}
|
||||
await this.loadMultipleModels(AppConfig.modelUrlList);
|
||||
EventBridge.modelLoaded({ urls: AppConfig.modelUrlList });
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
@ -91,19 +52,74 @@ export class AppModel extends Monobehiver {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载单个模型
|
||||
* @param modelUrl 模型URL
|
||||
* 批量加载模型(内部方法)
|
||||
* @param urls 模型URL数组
|
||||
*/
|
||||
async loadSingleModel(modelUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void): Promise<LoadResult> {
|
||||
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: Scene | null = this.mainApp.appScene.object;
|
||||
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: '未找到网格' };
|
||||
|
||||
// 存储根网格(通常是第一个网格)
|
||||
const rootMesh = result.meshes[0];
|
||||
this.loadedMeshes.push(...result.meshes);
|
||||
return { success: true, meshes: result.meshes, skeletons: result.skeletons };
|
||||
} catch (e: any) {
|
||||
@ -112,9 +128,20 @@ export class AppModel extends Monobehiver {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 克隆模型材质,避免多个模型共享同名材质
|
||||
* @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;
|
||||
@ -129,64 +156,64 @@ export class AppModel extends Monobehiver {
|
||||
}
|
||||
|
||||
/** 获取缓存的网格 */
|
||||
getCachedMeshes(url: string): AbstractMesh | undefined {
|
||||
return this.modelDic.Get(url);
|
||||
getCachedMeshes(name: string): AbstractMesh[] | undefined {
|
||||
return this.modelDic.Get(name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** 清理所有资源 */
|
||||
clean(): void {
|
||||
this.modelDic.Clear();
|
||||
this.loadedMeshes.forEach(m => m?.dispose());
|
||||
this.loadedMeshes = [];
|
||||
this.skeletonManager?.clean();
|
||||
this.outfitManager?.clean();
|
||||
this.isLoading = false;
|
||||
this.skeletonMerged = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模型到场景
|
||||
* @param modelName 模型名称(用于后续引用)
|
||||
* @param modelUrl 模型URL路径
|
||||
* 添加模型到场景(支持单个或批量)
|
||||
* @param modelName 模型名称 或 模型配置数组
|
||||
* @param modelUrl 模型URL(单个模型时使用)
|
||||
*/
|
||||
async add(modelName: string, modelUrl: string): Promise<LoadResult> {
|
||||
// 检查是否已经存在该模型
|
||||
const existingMesh = this.modelDic.Get(modelName);
|
||||
if (existingMesh && !existingMesh.isDisposed()) {
|
||||
console.log(`模型 ${modelName} 已存在,直接显示`);
|
||||
existingMesh.setEnabled(true);
|
||||
existingMesh.getChildMeshes().forEach(child => child.setEnabled(true));
|
||||
return { success: true, meshes: [existingMesh, ...existingMesh.getChildMeshes()] };
|
||||
async add(
|
||||
modelName: string | ModelConfig[],
|
||||
modelUrl?: string
|
||||
): Promise<LoadResult | { success: boolean; results: LoadResult[] }> {
|
||||
// 批量加载
|
||||
if (Array.isArray(modelName)) {
|
||||
return await this.addMultiple(modelName);
|
||||
}
|
||||
|
||||
const handleProgress = (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: modelUrl,
|
||||
progress,
|
||||
percentage: Number((progress * 100).toFixed(2)),
|
||||
detail: {
|
||||
url: modelUrl,
|
||||
lengthComputable: event.lengthComputable,
|
||||
loadedBytes: event.loaded,
|
||||
totalBytes: event.total
|
||||
}
|
||||
});
|
||||
};
|
||||
// 单个加载
|
||||
if (!modelUrl) {
|
||||
return { success: false, error: '缺少模型URL参数' };
|
||||
}
|
||||
|
||||
const result = await this.loadSingleModel(modelUrl, handleProgress);
|
||||
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) {
|
||||
// 使用modelName作为key存储根网格
|
||||
const rootMesh = result.meshes[0];
|
||||
this.modelDic.Set(modelName, rootMesh);
|
||||
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 });
|
||||
@ -196,33 +223,128 @@ export class AppModel extends Monobehiver {
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换模型:销毁旧模型并加载新模型
|
||||
* @param modelName 要替换的模型名称
|
||||
* @param newModelUrl 新模型的URL路径
|
||||
* 批量添加模型
|
||||
*/
|
||||
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 查找所属的模型名称
|
||||
* @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 移除所属的整个模型
|
||||
* @param mesh 网格对象
|
||||
* @returns 是否成功移除
|
||||
*/
|
||||
remove(mesh: AbstractMesh): boolean {
|
||||
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> {
|
||||
// 先销毁旧模型
|
||||
this.remove(modelName);
|
||||
|
||||
// 加载新模型
|
||||
return await this.add(modelName, newModelUrl);
|
||||
this.removeByName(modelName);
|
||||
return await this.addSingle(modelName, newModelUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁指定模型
|
||||
* @param modelName 模型名称
|
||||
*/
|
||||
remove(modelName: string): void {
|
||||
const mesh = this.modelDic.Get(modelName);
|
||||
if (mesh) {
|
||||
// 隐藏网格而不是销毁,以便后续可以重新显示
|
||||
mesh.setEnabled(false);
|
||||
mesh.getChildMeshes().forEach(child => child.setEnabled(false));
|
||||
|
||||
console.log(`Model hidden: ${modelName}`);
|
||||
} else {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
StandardMaterial,
|
||||
HighlightLayer,
|
||||
PointerInfo,
|
||||
ElasticEase,
|
||||
} from '@babylonjs/core'
|
||||
import { MainApp } from './MainApp'
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
@ -80,11 +81,25 @@ class AppRay extends Monobehiver {
|
||||
// const isSpriteHotspotClick = this.mainApp.appHotspot?.handleSpritePick();
|
||||
// if (isSpriteHotspotClick) return;
|
||||
|
||||
if (pickInfo && pickInfo.pickedMesh) {
|
||||
if (pickInfo && pickInfo.hit && pickInfo.pickedMesh && pickInfo.pickedPoint) {
|
||||
// 检查是否点击的是热点
|
||||
if (pickInfo.pickedMesh.metadata?.type === 'hotspot') {
|
||||
return;
|
||||
}
|
||||
|
||||
const materialName = pickInfo.pickedMesh.material?.name || '';
|
||||
EventBridge.modelClick({
|
||||
meshName: pickInfo.pickedMesh.name,
|
||||
pickedMesh: pickInfo.pickedMesh,
|
||||
pickedPoint: pickInfo.pickedPoint,
|
||||
materialName: materialName,
|
||||
});
|
||||
}
|
||||
else{
|
||||
console.log(1111);
|
||||
|
||||
this.mainApp.appDomTo3D.hideAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -72,28 +72,35 @@ export class GameManager extends Monobehiver {
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化材质字典
|
||||
// 初始化材质和网格字典
|
||||
this.updateDictionaries();
|
||||
|
||||
this.cacheRollerDoorMeshes();
|
||||
console.log('材质字典:', this.materialDic);
|
||||
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;
|
||||
|
||||
// 更新材质字典
|
||||
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) {
|
||||
|
||||
if (mesh instanceof Mesh && !this.meshDic.Has(mesh.name)) {
|
||||
this.meshDic.Set(mesh.name, mesh);
|
||||
}
|
||||
}
|
||||
this.cacheRollerDoorMeshes();
|
||||
console.log('材质字典:', this.materialDic);
|
||||
this.setRollerDoorScale("Box006.001", new Vector3(0.12,0.02,0.118));
|
||||
|
||||
// 单独设置 Box005.001 的缩放为 (1, 2, 1)
|
||||
this.setRollerDoorScale("Box005.001", new Vector3(0.13,0.02,0.12));
|
||||
}
|
||||
|
||||
/** 初始化设置材质 */
|
||||
@ -760,11 +767,14 @@ export class GameManager extends Monobehiver {
|
||||
|
||||
// 3. 应用材质到目标网格
|
||||
targetMaterials.forEach(material => {
|
||||
if (material[attribute]) {
|
||||
if (attribute === 'baseColor' && typeof value === 'string') {
|
||||
// 如果是 baseColor 且值是字符串(16进制颜色),转换为 Color3
|
||||
material.albedoColor = Color3.FromHexString(value);
|
||||
console.log(`Applying baseColor ${value} to material: ${material.name}`);
|
||||
} else if (material[attribute]) {
|
||||
material[attribute] = value;
|
||||
console.log(`Applying attribute ${attribute} to ${value} to mesh: ${material.name}`);
|
||||
}
|
||||
console.log(`Applying attribute ${attribute} to ${value} to mesh: ${material.name}`);
|
||||
// 这里需要根据实际的材质系统实现
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import { AppRay } from './AppRay';
|
||||
import { GameManager } from './GameManager';
|
||||
import { EventBridge } from '../event/bridge';
|
||||
import { AppHotspot } from './AppHotspot';
|
||||
import { AppDomTo3D } from './AppDomTo3D';
|
||||
|
||||
/**
|
||||
* 主应用类 - 3D场景的核心控制器
|
||||
@ -28,6 +29,7 @@ export class MainApp {
|
||||
appEnv: AppEnv;
|
||||
appRay: AppRay;
|
||||
appHotspot: AppHotspot;
|
||||
appDomTo3D: AppDomTo3D;
|
||||
gameManager: GameManager;
|
||||
|
||||
|
||||
@ -40,6 +42,7 @@ export class MainApp {
|
||||
this.appEnv = new AppEnv(this);
|
||||
this.appRay = new AppRay(this);
|
||||
this.appHotspot = new AppHotspot(this);
|
||||
this.appDomTo3D = new AppDomTo3D(this);
|
||||
this.gameManager = new GameManager(this);
|
||||
|
||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||
@ -58,6 +61,7 @@ export class MainApp {
|
||||
async loadModel(): Promise<void> {
|
||||
await this.appModel.loadModel();
|
||||
await this.gameManager.Awake();
|
||||
console.log(1111111111111111111111);
|
||||
EventBridge.allReady({ scene: this.appScene.object });
|
||||
}
|
||||
|
||||
@ -68,7 +72,8 @@ export class MainApp {
|
||||
this.appCamera.Awake();
|
||||
this.appLight.Awake();
|
||||
this.appEnv.Awake();
|
||||
this.appRay.Awake()
|
||||
this.appRay.Awake();
|
||||
this.appDomTo3D.init();
|
||||
this.appModel.initManagers();
|
||||
this.update();
|
||||
EventBridge.sceneReady({ scene: this.appScene.object });
|
||||
@ -80,6 +85,7 @@ export class MainApp {
|
||||
this.appEngin.object.runRenderLoop(() => {
|
||||
this.appScene.object?.render();
|
||||
this.appCamera.update();
|
||||
this.appDomTo3D.updateDomPositions();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user