优化Gamemanager
This commit is contained in:
@ -1,749 +0,0 @@
|
||||
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(modelId, transform);
|
||||
}
|
||||
|
||||
// 配置拖拽功能
|
||||
if (drag) {
|
||||
this.mainApp.appModelDrag?.configureDrag(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
349
src/babylonjs/GameManager.full.ts
Normal file
349
src/babylonjs/GameManager.full.ts
Normal file
@ -0,0 +1,349 @@
|
||||
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3 } from "@babylonjs/core";
|
||||
import { Observer } from "@babylonjs/core/Misc/observable";
|
||||
import { Nullable } from "@babylonjs/core/types";
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { Dictionary } from '../utils/Dictionary';
|
||||
|
||||
type RollerDoorOptions = {
|
||||
upY?: number;
|
||||
downY?: number;
|
||||
speed?: number;
|
||||
meshNames?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏管理器类 - 负责材质管理和场景控制
|
||||
*
|
||||
* 核心功能:
|
||||
* - 材质管理(applyMaterial)
|
||||
* - 卷帘门动画(待确认是否实际使用)
|
||||
* - Y轴剖切(待确认是否实际使用)
|
||||
*/
|
||||
export class GameManager extends Monobehiver {
|
||||
private materialDic: Dictionary<PBRMaterial>;
|
||||
private meshDic: Dictionary<any>;
|
||||
|
||||
// 卷帘门相关(如未使用可删除)
|
||||
private rollerDoorMeshes: AbstractMesh[];
|
||||
private rollerDoorGroup: AbstractMesh | null;
|
||||
private rollerDoorInitialY: Map<string, number>;
|
||||
private rollerDoorObserver: Nullable<Observer<Scene>>;
|
||||
private rollerDoorIsOpen: boolean;
|
||||
private rollerDoorNames: string[];
|
||||
|
||||
// Y轴剖切相关(如未使用可删除)
|
||||
private yClipPlane: Plane | null;
|
||||
private yClipTargets: string[] | null;
|
||||
|
||||
constructor(mainApp: any) {
|
||||
super(mainApp);
|
||||
this.materialDic = new Dictionary<PBRMaterial>();
|
||||
this.meshDic = new Dictionary<any>();
|
||||
this.rollerDoorMeshes = [];
|
||||
this.rollerDoorGroup = null;
|
||||
this.rollerDoorInitialY = new Map();
|
||||
this.rollerDoorObserver = null;
|
||||
this.rollerDoorIsOpen = false;
|
||||
this.rollerDoorNames = ["Box006.001", "Box005.001"];
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
}
|
||||
|
||||
/** 调试:返回当前场景中所有网格名称 */
|
||||
listMeshNames(): string[] {
|
||||
return this.meshDic.Keys();
|
||||
}
|
||||
|
||||
/** 初始化游戏管理器 */
|
||||
async Awake() {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateDictionaries();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新材质和网格字典(从场景中同步)
|
||||
*/
|
||||
updateDictionaries(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) return;
|
||||
|
||||
this.materialDic.Clear();
|
||||
this.meshDic.Clear();
|
||||
|
||||
// 更新材质字典
|
||||
for (const mat of scene.materials) {
|
||||
if (!(mat as any)._isDisposed && !this.materialDic.Has(mat.name)) {
|
||||
this.materialDic.Set(mat.name, mat as PBRMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新网格字典
|
||||
for (const mesh of scene.meshes) {
|
||||
if (mesh instanceof Mesh && !mesh.isDisposed() && !this.meshDic.Has(mesh.name)) {
|
||||
this.meshDic.Set(mesh.name, mesh);
|
||||
}
|
||||
|
||||
const mat = mesh.material;
|
||||
if (mat instanceof PBRMaterial && !(mat as any)._isDisposed) {
|
||||
this.materialDic.Set(mat.name, mat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用材质属性
|
||||
* @param options 材质配置选项
|
||||
*/
|
||||
applyMaterial(options: {
|
||||
target: string;
|
||||
modelId?: string;
|
||||
albedoColor?: string;
|
||||
albedoTexture?: string;
|
||||
normalMap?: string;
|
||||
metallicTexture?: string;
|
||||
roughness?: number;
|
||||
metallic?: number;
|
||||
}): void {
|
||||
this.updateDictionaries();
|
||||
|
||||
const targetMaterials: PBRMaterial[] = [];
|
||||
|
||||
// 如果提供了 modelId,只查找该模型的材质
|
||||
if (options.modelId) {
|
||||
const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId);
|
||||
|
||||
if (!modelMeshes || modelMeshes.length === 0) {
|
||||
console.warn(`Model not found: ${options.modelId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
modelMeshes.forEach((mesh: AbstractMesh) => {
|
||||
if (mesh.material && mesh.material instanceof PBRMaterial) {
|
||||
const material = mesh.material as PBRMaterial;
|
||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||
if (!targetMaterials.includes(material)) {
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 没有提供 modelId,全局查找
|
||||
this.materialDic.Values().forEach(material => {
|
||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (targetMaterials.length === 0) {
|
||||
console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用材质属性
|
||||
targetMaterials.forEach(material => {
|
||||
// 应用颜色
|
||||
if (options.albedoColor) {
|
||||
const color = Color3.FromHexString(options.albedoColor);
|
||||
material.albedoColor.copyFrom(color);
|
||||
}
|
||||
|
||||
// 应用反照率纹理
|
||||
if (options.albedoTexture !== undefined) {
|
||||
if (options.albedoTexture) {
|
||||
material.albedoTexture = new Texture(options.albedoTexture, this.mainApp.appScene.object);
|
||||
} else {
|
||||
material.albedoTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用法线贴图
|
||||
if (options.normalMap !== undefined) {
|
||||
if (options.normalMap) {
|
||||
material.bumpTexture = new Texture(options.normalMap, this.mainApp.appScene.object);
|
||||
} else {
|
||||
material.bumpTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用金属度贴图
|
||||
if (options.metallicTexture !== undefined) {
|
||||
if (options.metallicTexture) {
|
||||
material.metallicTexture = new Texture(options.metallicTexture, this.mainApp.appScene.object);
|
||||
} else {
|
||||
material.metallicTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用粗糙度
|
||||
if (options.roughness !== undefined) {
|
||||
if (material.roughness !== options.roughness) {
|
||||
material.roughness = options.roughness;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用金属度
|
||||
if (options.metallic !== undefined) {
|
||||
if (material.metallic !== options.metallic) {
|
||||
material.metallic = options.metallic;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 卷帘门开合:再次调用会反向动作 */
|
||||
toggleRollerDoor(options?: RollerDoorOptions): void {
|
||||
this.setRollerDoorState(!this.rollerDoorIsOpen, options);
|
||||
}
|
||||
|
||||
/** 直接设置卷帘门状态 */
|
||||
setRollerDoorState(open: boolean, options?: RollerDoorOptions): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found for roller door');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cacheRollerDoorMeshes(options?.meshNames);
|
||||
|
||||
if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) {
|
||||
console.warn('Roller door group or meshes not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const upY = options?.upY ?? (this.rollerDoorInitialY.get(this.rollerDoorMeshes[0].name) || 0) + 3;
|
||||
const downY = options?.downY ?? (this.rollerDoorInitialY.get(this.rollerDoorMeshes[0].name) || 0);
|
||||
const speed = options?.speed ?? 1;
|
||||
|
||||
const targetY = open ? upY : downY;
|
||||
|
||||
if (this.rollerDoorObserver) {
|
||||
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||
this.rollerDoorObserver = null;
|
||||
}
|
||||
|
||||
this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.rollerDoorGroup) return;
|
||||
|
||||
const delta = scene.getEngine().getDeltaTime() / 1000;
|
||||
const step = speed * delta;
|
||||
const currentY = this.rollerDoorGroup.position.y;
|
||||
|
||||
if (Math.abs(currentY - targetY) < step) {
|
||||
this.rollerDoorGroup.position.y = targetY;
|
||||
if (this.rollerDoorObserver) {
|
||||
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||
this.rollerDoorObserver = null;
|
||||
}
|
||||
this.rollerDoorIsOpen = open;
|
||||
} else {
|
||||
this.rollerDoorGroup.position.y += (targetY > currentY ? step : -step);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询卷帘门当前是否已开启 */
|
||||
isRollerDoorOpen(): boolean {
|
||||
return this.rollerDoorIsOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存卷帘门网格
|
||||
*/
|
||||
private cacheRollerDoorMeshes(meshNames?: string[]): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) return;
|
||||
|
||||
const targetNames = meshNames || this.rollerDoorNames;
|
||||
|
||||
this.rollerDoorMeshes = scene.meshes.filter(mesh =>
|
||||
targetNames.includes(mesh.name)
|
||||
) as AbstractMesh[];
|
||||
|
||||
if (this.rollerDoorMeshes.length === 0) return;
|
||||
|
||||
// 记录初始Y坐标
|
||||
this.rollerDoorMeshes.forEach(mesh => {
|
||||
if (!this.rollerDoorInitialY.has(mesh.name)) {
|
||||
this.rollerDoorInitialY.set(mesh.name, mesh.position.y);
|
||||
}
|
||||
});
|
||||
|
||||
// 创建父节点统一控制
|
||||
if (!this.rollerDoorGroup) {
|
||||
this.rollerDoorGroup = this.rollerDoorMeshes[0];
|
||||
for (let i = 1; i < this.rollerDoorMeshes.length; i++) {
|
||||
this.rollerDoorMeshes[i].setParent(this.rollerDoorGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Y轴剖切:保留指定高度以上或以下的部分
|
||||
*/
|
||||
setYAxisClip(
|
||||
height: number,
|
||||
keepAbove: boolean = true,
|
||||
onlyMeshNames?: string[],
|
||||
excludeMeshNames?: string[]
|
||||
): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) return;
|
||||
|
||||
this.yClipPlane = new Plane(0, keepAbove ? -1 : 1, 0, keepAbove ? height : -height);
|
||||
this.yClipTargets = onlyMeshNames || null;
|
||||
|
||||
scene.meshes.forEach(mesh => {
|
||||
if (excludeMeshNames && excludeMeshNames.includes(mesh.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.yClipTargets && !this.yClipTargets.includes(mesh.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mesh.material) {
|
||||
const materials = Array.isArray(mesh.material)
|
||||
? (mesh.material as any[])
|
||||
: [mesh.material];
|
||||
|
||||
materials.forEach(mat => {
|
||||
if (!mat.clipPlane) {
|
||||
mat.clipPlane = this.yClipPlane;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭Y轴剖切
|
||||
*/
|
||||
clearYAxisClip(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene || !this.yClipPlane) return;
|
||||
|
||||
scene.meshes.forEach(mesh => {
|
||||
if (mesh.material) {
|
||||
const materials = Array.isArray(mesh.material)
|
||||
? (mesh.material as any[])
|
||||
: [mesh.material];
|
||||
|
||||
materials.forEach(mat => {
|
||||
if (mat.clipPlane === this.yClipPlane) {
|
||||
mat.clipPlane = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
}
|
||||
}
|
||||
846
src/babylonjs/GameManager.old.ts
Normal file
846
src/babylonjs/GameManager.old.ts
Normal file
@ -0,0 +1,846 @@
|
||||
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3, TransformNode } from "@babylonjs/core";
|
||||
import { Observer } from "@babylonjs/core/Misc/observable";
|
||||
import { Nullable } from "@babylonjs/core/types";
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { Dictionary } from '../utils/Dictionary';
|
||||
import { AppConfig } from './AppConfig';
|
||||
|
||||
type RollerDoorOptions = {
|
||||
/** 目标升起高度,缺省为初始 y + 3 */
|
||||
upY?: number;
|
||||
/** 落下终点,缺省为初始 y */
|
||||
downY?: number;
|
||||
/** 运动速度(单位/秒),缺省 1 */
|
||||
speed?: number;
|
||||
/** 自定义门体网格名列表,不传使用默认两个卷帘门 */
|
||||
meshNames?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏管理器类 - 负责管理游戏逻辑、材质和纹理
|
||||
*/
|
||||
export class GameManager extends Monobehiver {
|
||||
private materialDic: Dictionary<PBRMaterial>;
|
||||
private meshDic: Dictionary<any>;
|
||||
private oldTextureDic: Dictionary<any>;
|
||||
private rollerDoorMeshes: AbstractMesh[];
|
||||
private rollerDoorGroup: AbstractMesh | null;
|
||||
private rollerDoorInitialY: Map<string, number>;
|
||||
private rollerDoorObserver: Nullable<Observer<Scene>>;
|
||||
private rollerDoorIsOpen: boolean;
|
||||
private rollerDoorNames: string[];
|
||||
private yClipPlane: Plane | null;
|
||||
private yClipTargets: string[] | null;
|
||||
private clipPlaneVisualization: Mesh | null;
|
||||
|
||||
// 记录加载失败的贴图
|
||||
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.rollerDoorMeshes = [];
|
||||
this.rollerDoorGroup = null;
|
||||
this.rollerDoorInitialY = new Map();
|
||||
this.rollerDoorObserver = null;
|
||||
this.rollerDoorIsOpen = false;
|
||||
this.rollerDoorNames = ["Box006.001", "Box005.001"];
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
this.clipPlaneVisualization = null;
|
||||
this.failedTextures = [];
|
||||
}
|
||||
|
||||
/** 调试:返回当前场景中所有网格名称 */
|
||||
listMeshNames(): string[] {
|
||||
return this.meshDic.Keys();
|
||||
}
|
||||
|
||||
/** 初始化游戏管理器 */
|
||||
async Awake() {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化材质和网格字典
|
||||
this.updateDictionaries();
|
||||
|
||||
// this.cacheRollerDoorMeshes();
|
||||
|
||||
// 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;
|
||||
|
||||
this.materialDic.Clear();
|
||||
this.meshDic.Clear();
|
||||
|
||||
// 更新材质字典
|
||||
for (const mat of scene.materials) {
|
||||
if (!(mat as any)._isDisposed && !this.materialDic.Has(mat.name)) {
|
||||
this.materialDic.Set(mat.name, mat as PBRMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新网格字典
|
||||
for (const mesh of scene.meshes) {
|
||||
if (mesh instanceof Mesh && !mesh.isDisposed() && !this.meshDic.Has(mesh.name)) {
|
||||
this.meshDic.Set(mesh.name, mesh);
|
||||
}
|
||||
|
||||
const mat = mesh.material;
|
||||
if (mat instanceof PBRMaterial && !(mat as any)._isDisposed) {
|
||||
this.materialDic.Set(mat.name, mat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化设置材质 */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 卷帘门开合:再次调用会反向动作 */
|
||||
toggleRollerDoor(options?: RollerDoorOptions): void {
|
||||
this.setRollerDoorState(!this.rollerDoorIsOpen, options);
|
||||
}
|
||||
|
||||
/** 直接设置卷帘门状态 */
|
||||
setRollerDoorState(open: boolean, options?: RollerDoorOptions): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found for roller door');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cacheRollerDoorMeshes(options?.meshNames);
|
||||
|
||||
if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) {
|
||||
console.warn('Roller door group or meshes not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const speed = Math.max(options?.speed ?? 1, 0.01);
|
||||
|
||||
// 计算目标高度
|
||||
let targetY: number;
|
||||
if (open) {
|
||||
// 上升时:如果指定了绝对高度就用绝对高度,否则用相对高度
|
||||
if (options?.upY !== undefined) {
|
||||
targetY = options.upY;
|
||||
} else {
|
||||
// 找到所有门中最高的初始位置,让所有门都升到这个高度+3
|
||||
const maxBaseY = Math.max(...this.rollerDoorMeshes.map(m =>
|
||||
this.rollerDoorInitialY.get(m.name) ?? m.position.y
|
||||
));
|
||||
targetY = maxBaseY + 3;
|
||||
}
|
||||
} else {
|
||||
// 下降时:回到初始位置
|
||||
targetY = 0;
|
||||
}
|
||||
|
||||
// 检查是否已经在目标位置
|
||||
if (Math.abs(this.rollerDoorGroup.position.y - targetY) < 0.001) {
|
||||
this.rollerDoorIsOpen = open;
|
||||
return;
|
||||
}
|
||||
|
||||
this.rollerDoorIsOpen = open;
|
||||
this.stopRollerDoorAnimation();
|
||||
|
||||
this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => {
|
||||
const dt = scene.getEngine().getDeltaTime() / 1000;
|
||||
const current = this.rollerDoorGroup!.position.y;
|
||||
const direction = targetY >= current ? 1 : -1;
|
||||
|
||||
// 使用固定速度变量
|
||||
const step = speed * dt;
|
||||
let next = current + direction * step;
|
||||
|
||||
if ((direction > 0 && next >= targetY) || (direction < 0 && next <= targetY)) {
|
||||
next = targetY;
|
||||
this.stopRollerDoorAnimation();
|
||||
this.rollerDoorIsOpen = open;
|
||||
console.log('Roller door animation finished');
|
||||
}
|
||||
|
||||
// 移动透明盒子
|
||||
this.rollerDoorGroup!.position.y = next;
|
||||
|
||||
// 打印每个卷帘门的当前位置
|
||||
// console.log('Roller door positions:');
|
||||
// for (const mesh of this.rollerDoorMeshes) {
|
||||
// console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`);
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
/** 当前卷帘门是否开启 */
|
||||
isRollerDoorOpen(): boolean {
|
||||
return this.rollerDoorIsOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置卷帘门的缩放
|
||||
* @param meshName - 卷帘门网格名称
|
||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||
*/
|
||||
setRollerDoorScale(meshName: string, scale: number | Vector3): void {
|
||||
const mesh = this.meshDic.Get(meshName);
|
||||
if (mesh) {
|
||||
if (typeof scale === 'number') {
|
||||
mesh.scaling.set(scale, scale, scale);
|
||||
} else {
|
||||
mesh.scaling.copyFrom(scale);
|
||||
}
|
||||
console.log(`Set scale for ${meshName}:`, mesh.scaling.asArray());
|
||||
} else {
|
||||
console.warn(`Roller door mesh not found: ${meshName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置所有卷帘门的缩放
|
||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||
*/
|
||||
setAllRollerDoorsScale(scale: number | Vector3): void {
|
||||
this.rollerDoorMeshes.forEach(mesh => {
|
||||
if (typeof scale === 'number') {
|
||||
mesh.scaling.set(scale, scale, scale);
|
||||
} else {
|
||||
mesh.scaling.copyFrom(scale);
|
||||
}
|
||||
console.log(`Set scale for ${mesh.name}:`, mesh.scaling.asArray());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基于 Y 轴的剖切平面,keepAbove=true 时保留平面以上部分
|
||||
* onlyMeshNames 指定只作用于哪些网格,其他网格不受影响
|
||||
*/
|
||||
setYAxisClip(
|
||||
height: number,
|
||||
keepAbove = true,
|
||||
onlyMeshNames?: string[],
|
||||
excludeMeshNames?: string[]
|
||||
): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found for clipping');
|
||||
return;
|
||||
}
|
||||
const normal = new Vector3(0, keepAbove ? 1 : -1, 0);
|
||||
this.yClipPlane = Plane.FromPositionAndNormal(new Vector3(0, height, 0), normal);
|
||||
|
||||
// 如果指定了特定网格,只对这些网格应用剖切
|
||||
if (onlyMeshNames?.length) {
|
||||
this.applyClipPlaneToMeshes(this.yClipPlane, onlyMeshNames);
|
||||
} else {
|
||||
// 否则使用场景级别的剖切,作用于所有网格
|
||||
scene.clipPlane = this.yClipPlane;
|
||||
}
|
||||
|
||||
console.log('[clipping] Scene clipPlane set:', { height, keepAbove, normal: normal.asArray(), targets: onlyMeshNames || 'all' });
|
||||
}
|
||||
|
||||
/** 关闭 Y 轴剖切 */
|
||||
clearYAxisClip(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (scene) {
|
||||
scene.clipPlane = null;
|
||||
}
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
|
||||
// 清除所有网格材质上的 clipPlane
|
||||
this.meshDic.Values().forEach((mesh) => {
|
||||
const mat = mesh.material as any;
|
||||
if (mat && 'clipPlane' in mat) {
|
||||
mat.clipPlane = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cacheRollerDoorMeshes(customNames?: string[]): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) return;
|
||||
|
||||
const names = customNames?.length ? customNames : this.rollerDoorNames;
|
||||
this.rollerDoorMeshes = [];
|
||||
|
||||
// 创建或获取 group 作为父级
|
||||
if (!this.rollerDoorGroup) {
|
||||
// 创建一个 AbstractMesh 作为组
|
||||
// 使用 TransformNode 代替 AbstractMesh,因为 AbstractMesh 是抽象类无法实例化
|
||||
this.rollerDoorGroup = new TransformNode('rollerDoorGroup', scene) as any;
|
||||
// 确保 group 的初始位置为 (0, 0, 0)
|
||||
this.rollerDoorGroup.position.set(0, 0, 0);
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
const mesh = this.meshDic.Get(name);
|
||||
if (mesh) {
|
||||
this.rollerDoorMeshes.push(mesh);
|
||||
|
||||
// 保存网格的当前位置作为初始位置
|
||||
if (!this.rollerDoorInitialY.has(name)) {
|
||||
this.rollerDoorInitialY.set(name, mesh.position.y);
|
||||
}
|
||||
|
||||
// 保存网格的世界位置和缩放
|
||||
const worldPosition = mesh.getAbsolutePosition();
|
||||
const worldScaling = new Vector3(mesh.scaling.x, mesh.scaling.y, mesh.scaling.z);
|
||||
|
||||
// 将网格添加到 group 中
|
||||
mesh.parent = this.rollerDoorGroup;
|
||||
|
||||
// 调整网格的局部位置和缩放,保持世界位置和大小不变
|
||||
mesh.setAbsolutePosition(worldPosition);
|
||||
mesh.scaling.copyFrom(worldScaling);
|
||||
} else {
|
||||
console.warn(`Roller door mesh not found: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private stopRollerDoorAnimation(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (scene && this.rollerDoorObserver) {
|
||||
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||
}
|
||||
this.rollerDoorObserver = null;
|
||||
}
|
||||
|
||||
/** 将 clipPlane 只作用到指定网格的材质 */
|
||||
private applyClipPlaneToMeshes(plane: Plane, targetNames: string[]): void {
|
||||
const targetSet = new Set(targetNames);
|
||||
let appliedCount = 0;
|
||||
|
||||
this.meshDic.Values().forEach((mesh) => {
|
||||
const mat = mesh.material as any;
|
||||
if (!mat) {
|
||||
console.log('[clipping] Mesh has no material:', mesh.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetSet.has(mesh.name)) {
|
||||
// 目标网格:应用剖切
|
||||
mat.clipPlane = plane;
|
||||
appliedCount++;
|
||||
console.log('[clipping] Applied to mesh:', mesh.name, 'material:', mat.name);
|
||||
} else {
|
||||
// 非目标网格:清除剖切
|
||||
mat.clipPlane = null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[clipping] Total meshes processed:', this.meshDic.Keys().length, 'Applied to:', appliedCount);
|
||||
if (appliedCount === 0) {
|
||||
console.warn('[clipping] No meshes found with names:', targetNames);
|
||||
console.log('[clipping] Available mesh names:', this.meshDic.Keys());
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取公共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.stopRollerDoorAnimation();
|
||||
this.clearYAxisClip();
|
||||
this.rollerDoorMeshes = [];
|
||||
this.rollerDoorInitialY.clear();
|
||||
this.rollerDoorIsOpen = false;
|
||||
|
||||
// 清理 rollerDoorGroup
|
||||
if (this.rollerDoorGroup && this.rollerDoorGroup.dispose) {
|
||||
this.rollerDoorGroup.dispose();
|
||||
this.rollerDoorGroup = null;
|
||||
}
|
||||
|
||||
// 清理所有材质资源
|
||||
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 options 材质配置选项
|
||||
*/
|
||||
applyMaterial(options: {
|
||||
target: string;
|
||||
modelId?: string;
|
||||
albedoColor?: string;
|
||||
albedoTexture?: string;
|
||||
normalMap?: string;
|
||||
metallicTexture?: string;
|
||||
roughness?: number;
|
||||
metallic?: number;
|
||||
}): void {
|
||||
this.updateDictionaries();
|
||||
|
||||
// 查找目标材质(支持精确匹配和前缀匹配)
|
||||
const targetMaterials: PBRMaterial[] = [];
|
||||
|
||||
// 如果提供了 modelId,只查找该模型的材质
|
||||
if (options.modelId) {
|
||||
// 获取该模型的所有 meshes
|
||||
const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId);
|
||||
|
||||
if (!modelMeshes || modelMeshes.length === 0) {
|
||||
console.warn(`Model not found: ${options.modelId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历该模型的所有 mesh,查找匹配的材质
|
||||
modelMeshes.forEach((mesh: AbstractMesh) => {
|
||||
if (mesh.material && mesh.material instanceof PBRMaterial) {
|
||||
const material = mesh.material as PBRMaterial;
|
||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||
// 避免重复添加
|
||||
if (!targetMaterials.includes(material)) {
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
// 没有提供 modelId,全局查找(保持向后兼容)
|
||||
this.materialDic.Values().forEach(material => {
|
||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||
console.log(material.name);
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (targetMaterials.length === 0) {
|
||||
console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`);
|
||||
return;
|
||||
}
|
||||
console.log(options);
|
||||
// 应用材质属性到目标材质
|
||||
targetMaterials.forEach(material => {
|
||||
// 应用颜色
|
||||
if (options.albedoColor) {
|
||||
const color = Color3.FromHexString(options.albedoColor);
|
||||
material.albedoColor.copyFrom(color);
|
||||
}
|
||||
|
||||
|
||||
//应用反照率纹理(颜色贴图)
|
||||
if (options.albedoTexture !== undefined) {
|
||||
if (options.albedoTexture) {
|
||||
material.albedoTexture = new Texture(options.albedoTexture);
|
||||
} else {
|
||||
// 传入空字符串或 null 时清空贴图
|
||||
material.albedoTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用粗糙度
|
||||
if (options.roughness !== undefined) {
|
||||
if (material.roughness !== options.roughness) {
|
||||
material.roughness = options.roughness;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用金属度
|
||||
if (options.metallic !== undefined) {
|
||||
if (material.metallic !== options.metallic) {
|
||||
material.metallic = options.metallic;
|
||||
}
|
||||
}
|
||||
// 强制刷新材质
|
||||
material.markDirty();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,62 +1,18 @@
|
||||
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3, TransformNode } from "@babylonjs/core";
|
||||
import { Observer } from "@babylonjs/core/Misc/observable";
|
||||
import { Nullable } from "@babylonjs/core/types";
|
||||
import { Mesh, PBRMaterial, Texture, AbstractMesh, Color3 } from "@babylonjs/core";
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
import { Dictionary } from '../utils/Dictionary';
|
||||
import { AppConfig } from './AppConfig';
|
||||
|
||||
type RollerDoorOptions = {
|
||||
/** 目标升起高度,缺省为初始 y + 3 */
|
||||
upY?: number;
|
||||
/** 落下终点,缺省为初始 y */
|
||||
downY?: number;
|
||||
/** 运动速度(单位/秒),缺省 1 */
|
||||
speed?: number;
|
||||
/** 自定义门体网格名列表,不传使用默认两个卷帘门 */
|
||||
meshNames?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏管理器类 - 负责管理游戏逻辑、材质和纹理
|
||||
* 游戏管理器类 - 负责材质管理
|
||||
*/
|
||||
export class GameManager extends Monobehiver {
|
||||
private materialDic: Dictionary<PBRMaterial>;
|
||||
private meshDic: Dictionary<any>;
|
||||
private oldTextureDic: Dictionary<any>;
|
||||
private rollerDoorMeshes: AbstractMesh[];
|
||||
private rollerDoorGroup: AbstractMesh | null;
|
||||
private rollerDoorInitialY: Map<string, number>;
|
||||
private rollerDoorObserver: Nullable<Observer<Scene>>;
|
||||
private rollerDoorIsOpen: boolean;
|
||||
private rollerDoorNames: string[];
|
||||
private yClipPlane: Plane | null;
|
||||
private yClipTargets: string[] | null;
|
||||
private clipPlaneVisualization: Mesh | null;
|
||||
|
||||
// 记录加载失败的贴图
|
||||
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.rollerDoorMeshes = [];
|
||||
this.rollerDoorGroup = null;
|
||||
this.rollerDoorInitialY = new Map();
|
||||
this.rollerDoorObserver = null;
|
||||
this.rollerDoorIsOpen = false;
|
||||
this.rollerDoorNames = ["Box006.001", "Box005.001"];
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
this.clipPlaneVisualization = null;
|
||||
this.failedTextures = [];
|
||||
}
|
||||
|
||||
/** 调试:返回当前场景中所有网格名称 */
|
||||
@ -72,13 +28,7 @@ export class GameManager extends Monobehiver {
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化材质和网格字典
|
||||
this.updateDictionaries();
|
||||
|
||||
// this.cacheRollerDoorMeshes();
|
||||
|
||||
// this.setRollerDoorScale("Box006.001", new Vector3(0.12, 0.02, 0.118));
|
||||
// this.setRollerDoorScale("Box005.001", new Vector3(0.13, 0.02, 0.12));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,640 +61,6 @@ export class GameManager extends Monobehiver {
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化设置材质 */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 卷帘门开合:再次调用会反向动作 */
|
||||
toggleRollerDoor(options?: RollerDoorOptions): void {
|
||||
this.setRollerDoorState(!this.rollerDoorIsOpen, options);
|
||||
}
|
||||
|
||||
/** 直接设置卷帘门状态 */
|
||||
setRollerDoorState(open: boolean, options?: RollerDoorOptions): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found for roller door');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cacheRollerDoorMeshes(options?.meshNames);
|
||||
|
||||
if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) {
|
||||
console.warn('Roller door group or meshes not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const speed = Math.max(options?.speed ?? 1, 0.01);
|
||||
|
||||
// 计算目标高度
|
||||
let targetY: number;
|
||||
if (open) {
|
||||
// 上升时:如果指定了绝对高度就用绝对高度,否则用相对高度
|
||||
if (options?.upY !== undefined) {
|
||||
targetY = options.upY;
|
||||
} else {
|
||||
// 找到所有门中最高的初始位置,让所有门都升到这个高度+3
|
||||
const maxBaseY = Math.max(...this.rollerDoorMeshes.map(m =>
|
||||
this.rollerDoorInitialY.get(m.name) ?? m.position.y
|
||||
));
|
||||
targetY = maxBaseY + 3;
|
||||
}
|
||||
} else {
|
||||
// 下降时:回到初始位置
|
||||
targetY = 0;
|
||||
}
|
||||
|
||||
// 检查是否已经在目标位置
|
||||
if (Math.abs(this.rollerDoorGroup.position.y - targetY) < 0.001) {
|
||||
this.rollerDoorIsOpen = open;
|
||||
return;
|
||||
}
|
||||
|
||||
this.rollerDoorIsOpen = open;
|
||||
this.stopRollerDoorAnimation();
|
||||
|
||||
this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => {
|
||||
const dt = scene.getEngine().getDeltaTime() / 1000;
|
||||
const current = this.rollerDoorGroup!.position.y;
|
||||
const direction = targetY >= current ? 1 : -1;
|
||||
|
||||
// 使用固定速度变量
|
||||
const step = speed * dt;
|
||||
let next = current + direction * step;
|
||||
|
||||
if ((direction > 0 && next >= targetY) || (direction < 0 && next <= targetY)) {
|
||||
next = targetY;
|
||||
this.stopRollerDoorAnimation();
|
||||
this.rollerDoorIsOpen = open;
|
||||
console.log('Roller door animation finished');
|
||||
}
|
||||
|
||||
// 移动透明盒子
|
||||
this.rollerDoorGroup!.position.y = next;
|
||||
|
||||
// 打印每个卷帘门的当前位置
|
||||
// console.log('Roller door positions:');
|
||||
// for (const mesh of this.rollerDoorMeshes) {
|
||||
// console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`);
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
/** 当前卷帘门是否开启 */
|
||||
isRollerDoorOpen(): boolean {
|
||||
return this.rollerDoorIsOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置卷帘门的缩放
|
||||
* @param meshName - 卷帘门网格名称
|
||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||
*/
|
||||
setRollerDoorScale(meshName: string, scale: number | Vector3): void {
|
||||
const mesh = this.meshDic.Get(meshName);
|
||||
if (mesh) {
|
||||
if (typeof scale === 'number') {
|
||||
mesh.scaling.set(scale, scale, scale);
|
||||
} else {
|
||||
mesh.scaling.copyFrom(scale);
|
||||
}
|
||||
console.log(`Set scale for ${meshName}:`, mesh.scaling.asArray());
|
||||
} else {
|
||||
console.warn(`Roller door mesh not found: ${meshName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置所有卷帘门的缩放
|
||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||
*/
|
||||
setAllRollerDoorsScale(scale: number | Vector3): void {
|
||||
this.rollerDoorMeshes.forEach(mesh => {
|
||||
if (typeof scale === 'number') {
|
||||
mesh.scaling.set(scale, scale, scale);
|
||||
} else {
|
||||
mesh.scaling.copyFrom(scale);
|
||||
}
|
||||
console.log(`Set scale for ${mesh.name}:`, mesh.scaling.asArray());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基于 Y 轴的剖切平面,keepAbove=true 时保留平面以上部分
|
||||
* onlyMeshNames 指定只作用于哪些网格,其他网格不受影响
|
||||
*/
|
||||
setYAxisClip(
|
||||
height: number,
|
||||
keepAbove = true,
|
||||
onlyMeshNames?: string[],
|
||||
excludeMeshNames?: string[]
|
||||
): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) {
|
||||
console.warn('Scene not found for clipping');
|
||||
return;
|
||||
}
|
||||
const normal = new Vector3(0, keepAbove ? 1 : -1, 0);
|
||||
this.yClipPlane = Plane.FromPositionAndNormal(new Vector3(0, height, 0), normal);
|
||||
|
||||
// 如果指定了特定网格,只对这些网格应用剖切
|
||||
if (onlyMeshNames?.length) {
|
||||
this.applyClipPlaneToMeshes(this.yClipPlane, onlyMeshNames);
|
||||
} else {
|
||||
// 否则使用场景级别的剖切,作用于所有网格
|
||||
scene.clipPlane = this.yClipPlane;
|
||||
}
|
||||
|
||||
console.log('[clipping] Scene clipPlane set:', { height, keepAbove, normal: normal.asArray(), targets: onlyMeshNames || 'all' });
|
||||
}
|
||||
|
||||
/** 关闭 Y 轴剖切 */
|
||||
clearYAxisClip(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (scene) {
|
||||
scene.clipPlane = null;
|
||||
}
|
||||
this.yClipPlane = null;
|
||||
this.yClipTargets = null;
|
||||
|
||||
// 清除所有网格材质上的 clipPlane
|
||||
this.meshDic.Values().forEach((mesh) => {
|
||||
const mat = mesh.material as any;
|
||||
if (mat && 'clipPlane' in mat) {
|
||||
mat.clipPlane = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cacheRollerDoorMeshes(customNames?: string[]): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (!scene) return;
|
||||
|
||||
const names = customNames?.length ? customNames : this.rollerDoorNames;
|
||||
this.rollerDoorMeshes = [];
|
||||
|
||||
// 创建或获取 group 作为父级
|
||||
if (!this.rollerDoorGroup) {
|
||||
// 创建一个 AbstractMesh 作为组
|
||||
// 使用 TransformNode 代替 AbstractMesh,因为 AbstractMesh 是抽象类无法实例化
|
||||
this.rollerDoorGroup = new TransformNode('rollerDoorGroup', scene) as any;
|
||||
// 确保 group 的初始位置为 (0, 0, 0)
|
||||
this.rollerDoorGroup.position.set(0, 0, 0);
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
const mesh = this.meshDic.Get(name);
|
||||
if (mesh) {
|
||||
this.rollerDoorMeshes.push(mesh);
|
||||
|
||||
// 保存网格的当前位置作为初始位置
|
||||
if (!this.rollerDoorInitialY.has(name)) {
|
||||
this.rollerDoorInitialY.set(name, mesh.position.y);
|
||||
}
|
||||
|
||||
// 保存网格的世界位置和缩放
|
||||
const worldPosition = mesh.getAbsolutePosition();
|
||||
const worldScaling = new Vector3(mesh.scaling.x, mesh.scaling.y, mesh.scaling.z);
|
||||
|
||||
// 将网格添加到 group 中
|
||||
mesh.parent = this.rollerDoorGroup;
|
||||
|
||||
// 调整网格的局部位置和缩放,保持世界位置和大小不变
|
||||
mesh.setAbsolutePosition(worldPosition);
|
||||
mesh.scaling.copyFrom(worldScaling);
|
||||
} else {
|
||||
console.warn(`Roller door mesh not found: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private stopRollerDoorAnimation(): void {
|
||||
const scene = this.mainApp.appScene?.object;
|
||||
if (scene && this.rollerDoorObserver) {
|
||||
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||
}
|
||||
this.rollerDoorObserver = null;
|
||||
}
|
||||
|
||||
/** 将 clipPlane 只作用到指定网格的材质 */
|
||||
private applyClipPlaneToMeshes(plane: Plane, targetNames: string[]): void {
|
||||
const targetSet = new Set(targetNames);
|
||||
let appliedCount = 0;
|
||||
|
||||
this.meshDic.Values().forEach((mesh) => {
|
||||
const mat = mesh.material as any;
|
||||
if (!mat) {
|
||||
console.log('[clipping] Mesh has no material:', mesh.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetSet.has(mesh.name)) {
|
||||
// 目标网格:应用剖切
|
||||
mat.clipPlane = plane;
|
||||
appliedCount++;
|
||||
console.log('[clipping] Applied to mesh:', mesh.name, 'material:', mat.name);
|
||||
} else {
|
||||
// 非目标网格:清除剖切
|
||||
mat.clipPlane = null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[clipping] Total meshes processed:', this.meshDic.Keys().length, 'Applied to:', appliedCount);
|
||||
if (appliedCount === 0) {
|
||||
console.warn('[clipping] No meshes found with names:', targetNames);
|
||||
console.log('[clipping] Available mesh names:', this.meshDic.Keys());
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取公共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.stopRollerDoorAnimation();
|
||||
this.clearYAxisClip();
|
||||
this.rollerDoorMeshes = [];
|
||||
this.rollerDoorInitialY.clear();
|
||||
this.rollerDoorIsOpen = false;
|
||||
|
||||
// 清理 rollerDoorGroup
|
||||
if (this.rollerDoorGroup && this.rollerDoorGroup.dispose) {
|
||||
this.rollerDoorGroup.dispose();
|
||||
this.rollerDoorGroup = null;
|
||||
}
|
||||
|
||||
// 清理所有材质资源
|
||||
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 options 材质配置选项
|
||||
@ -761,12 +77,10 @@ export class GameManager extends Monobehiver {
|
||||
}): void {
|
||||
this.updateDictionaries();
|
||||
|
||||
// 查找目标材质(支持精确匹配和前缀匹配)
|
||||
const targetMaterials: PBRMaterial[] = [];
|
||||
|
||||
// 如果提供了 modelId,只查找该模型的材质
|
||||
if (options.modelId) {
|
||||
// 获取该模型的所有 meshes
|
||||
const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId);
|
||||
|
||||
if (!modelMeshes || modelMeshes.length === 0) {
|
||||
@ -774,12 +88,10 @@ export class GameManager extends Monobehiver {
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历该模型的所有 mesh,查找匹配的材质
|
||||
modelMeshes.forEach((mesh: AbstractMesh) => {
|
||||
if (mesh.material && mesh.material instanceof PBRMaterial) {
|
||||
const material = mesh.material as PBRMaterial;
|
||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||
// 避免重复添加
|
||||
if (!targetMaterials.includes(material)) {
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
@ -787,11 +99,9 @@ export class GameManager extends Monobehiver {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
// 没有提供 modelId,全局查找(保持向后兼容)
|
||||
// 没有提供 modelId,全局查找
|
||||
this.materialDic.Values().forEach(material => {
|
||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||
console.log(material.name);
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
});
|
||||
@ -801,8 +111,8 @@ export class GameManager extends Monobehiver {
|
||||
console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`);
|
||||
return;
|
||||
}
|
||||
console.log(options);
|
||||
// 应用材质属性到目标材质
|
||||
|
||||
// 应用材质属性
|
||||
targetMaterials.forEach(material => {
|
||||
// 应用颜色
|
||||
if (options.albedoColor) {
|
||||
@ -810,27 +120,47 @@ export class GameManager extends Monobehiver {
|
||||
material.albedoColor.copyFrom(color);
|
||||
}
|
||||
|
||||
|
||||
//应用反照率纹理(颜色贴图)
|
||||
// 应用反照率纹理
|
||||
if (options.albedoTexture !== undefined) {
|
||||
if (options.albedoTexture) {
|
||||
material.albedoTexture = new Texture(options.albedoTexture);
|
||||
material.albedoTexture = new Texture(options.albedoTexture, this.mainApp.appScene.object);
|
||||
} else {
|
||||
// 传入空字符串或 null 时清空贴图
|
||||
material.albedoTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
material.roughness = 0.8;
|
||||
material.metallic = 0;
|
||||
// 应用法线贴图
|
||||
if (options.normalMap !== undefined) {
|
||||
if (options.normalMap) {
|
||||
material.bumpTexture = new Texture(options.normalMap, this.mainApp.appScene.object);
|
||||
} else {
|
||||
material.bumpTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 强制刷新材质
|
||||
material.markDirty();
|
||||
// 应用金属度贴图
|
||||
if (options.metallicTexture !== undefined) {
|
||||
if (options.metallicTexture) {
|
||||
material.metallicTexture = new Texture(options.metallicTexture, this.mainApp.appScene.object);
|
||||
} else {
|
||||
material.metallicTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用粗糙度
|
||||
if (options.roughness !== undefined) {
|
||||
if (material.roughness !== options.roughness) {
|
||||
material.roughness = options.roughness;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用金属度
|
||||
if (options.metallic !== undefined) {
|
||||
if (material.metallic !== options.metallic) {
|
||||
material.metallic = options.metallic;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user