820 lines
23 KiB
TypeScript
820 lines
23 KiB
TypeScript
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;
|
||
albedoColor?: string;
|
||
albedoTexture?: string;
|
||
normalMap?: string;
|
||
metallicTexture?: string;
|
||
roughness?: number;
|
||
metallic?: number;
|
||
}): void {
|
||
this.updateDictionaries();
|
||
|
||
|
||
|
||
// 查找目标材质(支持精确匹配和前缀匹配)
|
||
const targetMaterials: PBRMaterial[] = [];
|
||
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}`);
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
// 应用材质属性到目标材质
|
||
targetMaterials.forEach(material => {
|
||
// 应用颜色
|
||
if (options.albedoColor) {
|
||
const color = Color3.FromHexString(options.albedoColor);
|
||
material.albedoColor.copyFrom(color);
|
||
}
|
||
|
||
// 应用反照率纹理(颜色贴图)
|
||
if (options.albedoTexture) {
|
||
material.albedoTexture = new Texture(options.albedoTexture);
|
||
}
|
||
|
||
// 应用法线贴图
|
||
if (options.normalMap) {
|
||
material.bumpTexture = new Texture(options.normalMap);
|
||
}
|
||
|
||
// 应用金属度贴图
|
||
if (options.metallicTexture) {
|
||
material.metallicTexture = new Texture(options.metallicTexture);
|
||
}
|
||
|
||
// 应用粗糙度值
|
||
if (options.roughness !== undefined) {
|
||
material.roughness = options.roughness;
|
||
}
|
||
|
||
// 应用金属度值
|
||
if (options.metallic !== undefined) {
|
||
material.metallic = options.metallic;
|
||
}
|
||
|
||
// 强制刷新材质
|
||
material.markDirty();
|
||
});
|
||
|
||
|
||
}
|
||
}
|