This commit is contained in:
yinsx
2026-01-05 10:14:33 +08:00
commit cf0b049abf
33 changed files with 3130 additions and 0 deletions

View File

@ -0,0 +1,52 @@
import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Tools } from '@babylonjs/core/Misc/tools';
import { Monobehiver } from '../base/Monobehiver';
/**
* 相机控制类- 负责创建和控制弧形旋转相机
*/
export class AppCamera extends Monobehiver {
object: ArcRotateCamera | null;
constructor(mainApp: any) {
super(mainApp);
this.object = null;
}
/** 初始化相机 */
Awake(): void {
const scene = this.mainApp.appScene.object;
const canvas = this.mainApp.appDom.renderDom;
if (!scene || !canvas) return;
// 创建弧形旋转相机水平角70度垂直角80度距离5目标点(0,1,0)
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(80), 5, new Vector3(0, 0, 0), scene);
this.object.attachControl(canvas, true);
this.object.minZ = 0.01; // 近裁剪面
this.object.wheelPrecision =999999; // 滚轮缩放精度
this.object.panningSensibility = 0;
this.object.position = new Vector3(-0, 0, 100);
this.setTarget(0, 2, 0);
}
/** 设置相机目标点 */
setTarget(x: number, y: number, z: number): void {
if (this.object) {
this.object.target = new Vector3(x, y, z);
}
}
/** 重置相机到默认位置 */
reset(): void {
if (!this.object) return;
this.object.radius = 2; this.setTarget(0, 0, 0);
this.object.position = new Vector3(0, 1.5, 2);
}
update(): void {
}
}

View File

@ -0,0 +1,13 @@
type OptionalCallback = (() => void) | null | undefined;
type ErrorCallback = ((error?: unknown) => void) | null | undefined;
/**
* 共享运行时配置对象
*/
export const AppConfig = {
container: 'renderDom',
modelUrlList: [] as string[],
success: null as OptionalCallback,
error: null as ErrorCallback
};

21
src/babylonjs/AppDom.ts Normal file
View File

@ -0,0 +1,21 @@
import { AppConfig } from './AppConfig';
/**
* 负责获取渲染容器 DOM
*/
export class AppDom {
private _renderDom: HTMLCanvasElement | null;
constructor() {
this._renderDom = null;
}
get renderDom(): HTMLCanvasElement | null {
return this._renderDom;
}
Awake(): void {
const dom = document.getElementById(AppConfig.container) || document.querySelector('#renderDom');
this._renderDom = (dom as HTMLCanvasElement) ?? null;
}
}

38
src/babylonjs/AppEngin.ts Normal file
View File

@ -0,0 +1,38 @@
import { Engine } from '@babylonjs/core/Engines/engine';
import { Monobehiver } from '../base/Monobehiver';
/**
* 渲染引擎管理类 - 负责创建和管理3D渲染引擎
*/
export class AppEngin extends Monobehiver {
object: Engine | null;
canvas: HTMLCanvasElement | null;
constructor(mainApp: any) {
super(mainApp);
this.object = null;
this.canvas = null;
}
Awake(): void {
this.canvas = this.mainApp.appDom.renderDom;
if (!this.canvas) {
throw new Error('Render canvas not found');
}
this.object = new Engine(this.canvas, true, {
preserveDrawingBuffer: false, // 不保留绘图缓冲区
stencil: true, // 启用模板缓冲
alpha: true // 启用透明背景
});
this.object.setSize(window.innerWidth, window.innerHeight);
this.object.setHardwareScalingLevel(1); // 1:1像素比例
}
/** 处理窗口大小变化 */
handleResize(): void {
if (this.object) {
this.object.setSize(window.innerWidth, window.innerHeight);
this.object.resize();
}
}
}

58
src/babylonjs/AppEnv.ts Normal file
View File

@ -0,0 +1,58 @@
import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture';
import { Monobehiver } from '../base/Monobehiver';
/**
* 环境管理类- 负责创建和管理HDR环境贴图
*/
export class AppEnv extends Monobehiver {
object: CubeTexture | null;
constructor(mainApp: any) {
super(mainApp);
this.object = null;
}
/** 初始化 - 创建默认HDR环境 */
Awake(): void {
this.createHDR();
}
/**
* 创建HDR环境贴图
* @param hdrPath HDR文件路径
*/
createHDR(hdrPath = '/hdr/sanGiuseppeBridge.env'): void {
const scene = this.mainApp.appScene.object;
if (!scene) return;
const reflectionTexture = CubeTexture.CreateFromPrefilteredData(hdrPath, scene);
scene.environmentIntensity = 1.5;
scene.environmentTexture = reflectionTexture;
this.object = reflectionTexture;
}
/**
* 修改HDR环境光强度
* @param intensity 强度值
*/
changeHDRIntensity(intensity: number): void {
if (this.mainApp.appScene.object) {
this.mainApp.appScene.object.environmentIntensity = intensity;
}
}
/**
* 旋转HDR环境贴图
* @param angle 旋转角度(弧度)
*/
rotateHDR(angle: number): void {
if (this.object) this.object.rotationY = angle;
}
/** 清理资源 */
clean(): void {
if (this.object) {
this.object.dispose();
this.object = null;
}
}
}

209
src/babylonjs/AppLight.ts Normal file
View File

@ -0,0 +1,209 @@
import { SpotLight } from '@babylonjs/core/Lights/spotLight';
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent';
import { Vector3, Quaternion } from '@babylonjs/core/Maths/math.vector';
import { Monobehiver } from '../base/Monobehiver';
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { Color3 } from '@babylonjs/core/Maths/math.color';
import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
import { Mesh } from '@babylonjs/core/Meshes/mesh';
type DebugMarkers = {
marker: Mesh;
arrow: Mesh;
gizmoManager: GizmoManager;
onKey: (e: KeyboardEvent) => void;
};
/**
* 灯光管理类- 负责创建和管理场景灯光
*/
export class AppLight extends Monobehiver {
lightList: SpotLight[];
shadowGenerator: ShadowGenerator | null;
debugMarkers?: DebugMarkers;
coneMesh?: Mesh;
updateCone?: () => void;
constructor(mainApp: any) {
super(mainApp);
this.lightList = [];
this.shadowGenerator = null;
}
/** 初始化灯光并开启阴影 */
Awake(): void {
const light = new SpotLight(
"mainLight",
new Vector3(-0.6, 2.12, 2),
new Vector3(0, -0.5, -1),
Math.PI * 0.6, // angle 弧度
);
light.angle = 1.5;
light.innerAngle = 1;
light.exponent = 2;
light.diffuse = new Color3(1, 0.86, 0.80);
light.specular = new Color3(1, 1, 1);
light.intensity = 60;
light.shadowMinZ = 0.01;
light.shadowMaxZ = 100;
light.range = 5000;
const generator = new ShadowGenerator(4096, light);
generator.usePercentageCloserFiltering = true;
generator.filteringQuality = ShadowGenerator.QUALITY_HIGH;
generator.transparencyShadow = true;
this.lightList.push(light);
this.shadowGenerator = generator;
}
/** 将网格添加为阴影投射者 */
addShadowCaster(mesh: AbstractMesh): void {
if (this.shadowGenerator) {
this.shadowGenerator.addShadowCaster(mesh);
}
}
/** 设置主光源强度 */
setIntensity(intensity: number): void {
if (this.lightList[0]) this.lightList[0].intensity = intensity;
}
/** 创建灯光可视化调试器 - W键拖拽位置E键旋转方向 */
enableLightDebug(): void {
const scene = this.mainApp.appScene.object;
const light = this.lightList[0];
if (!light || !scene) return;
const marker = MeshBuilder.CreateSphere("lightMarker", { diameter: 0.3 }, scene);
marker.position = light.position.clone();
const mat = new StandardMaterial("lightMat", scene);
mat.emissiveColor = Color3.Yellow();
marker.material = mat;
const arrow = MeshBuilder.CreateCylinder("lightArrow", { height: 1, diameterTop: 0, diameterBottom: 0.1 }, scene);
arrow.parent = marker;
arrow.position.set(0, 0, 0.6);
arrow.rotation.x = Math.PI / 2;
const arrowMat = new StandardMaterial("arrowMat", scene);
arrowMat.emissiveColor = Color3.Red();
arrow.material = arrowMat;
const dir = light.direction.normalize();
marker.rotation.y = Math.atan2(dir.x, dir.z);
marker.rotation.x = -Math.asin(dir.y);
const gizmoManager = new GizmoManager(scene);
gizmoManager.attachableMeshes = [marker];
gizmoManager.usePointerToAttachGizmos = false;
gizmoManager.attachToMesh(marker);
scene.onBeforeRenderObservable.add(() => {
light.position.copyFrom(marker.position);
const forward = new Vector3(0, 0, 1);
const rotationMatrix = marker.getWorldMatrix().getRotationMatrix();
light.direction = Vector3.TransformNormal(forward, rotationMatrix).normalize();
});
const onKey = (e: KeyboardEvent) => {
if (e.key === 'w' || e.key === 'W') {
gizmoManager.positionGizmoEnabled = true;
gizmoManager.rotationGizmoEnabled = false;
} else if (e.key === 'e' || e.key === 'E') {
gizmoManager.positionGizmoEnabled = false;
gizmoManager.rotationGizmoEnabled = true;
}
};
window.addEventListener('keydown', onKey);
gizmoManager.positionGizmoEnabled = true;
this.debugMarkers = { marker, arrow, gizmoManager, onKey };
}
/** 隐藏灯光调试器 */
disableLightDebug(): void {
if (this.debugMarkers) {
window.removeEventListener('keydown', this.debugMarkers.onKey);
this.debugMarkers.gizmoManager.dispose();
this.debugMarkers.arrow.dispose();
this.debugMarkers.marker.dispose();
this.debugMarkers = undefined;
}
}
/** 创建聚光灯可视化Gizmo - 带光锥范围 */
createLightGizmo(): void {
const scene = this.mainApp.appScene.object;
const light = this.lightList[0];
if (!light || !scene) return;
const coneLength = 3;
const updateCone = () => {
if (this.coneMesh) this.coneMesh.dispose();
const radius = Math.tan(light.angle) * coneLength;
const cone = MeshBuilder.CreateCylinder("lightCone", {
height: coneLength,
diameterTop: radius * 2,
diameterBottom: 0
}, scene);
const mat = new StandardMaterial("coneMat", scene);
mat.emissiveColor = Color3.Yellow();
mat.alpha = 0.2;
mat.wireframe = true;
cone.material = mat;
cone.position = light.position.add(light.direction.scale(coneLength / 2));
const up = new Vector3(0, 1, 0);
const axis = Vector3.Cross(up, light.direction).normalize();
const angle = Math.acos(Vector3.Dot(up, light.direction.normalize()));
if (axis.length() > 0.001) cone.rotationQuaternion = Quaternion.RotationAxis(axis, angle);
this.coneMesh = cone;
};
updateCone();
this.updateCone = updateCone;
}
/** 创建angle和innerAngle调试滑动条 */
createAngleSliders(): void {
const light = this.lightList[0];
if (!light) return;
const container = document.createElement('div');
container.style.cssText = 'position:fixed;top:10px;right:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:5px;color:#fff;font-size:12px;z-index:1000';
const createSlider = (label: string, value: number, min: number, max: number, onChange: (v: number) => void) => {
const wrap = document.createElement('div');
wrap.style.marginBottom = '8px';
const lbl = document.createElement('div');
lbl.textContent = `${label}: ${value}`;
const slider = document.createElement('input');
slider.type = 'range';
slider.min = String(min);
slider.max = String(max);
slider.value = String(value);
slider.style.width = '150px';
slider.oninput = () => {
lbl.textContent = `${label}: ${slider.value}`;
onChange(Number(slider.value));
};
wrap.append(lbl, slider);
return wrap;
};
const toRad = (deg: number) => deg * Math.PI / 180;
const toDeg = (rad: number) => Math.round(rad * 180 / Math.PI);
container.append(
createSlider('angle', toDeg(light.angle), 1, 180, v => { light.angle = toRad(v); this.updateCone?.(); }),
createSlider('innerAngle', toDeg(light.innerAngle), 0, 180, v => { light.innerAngle = toRad(v); })
);
document.body.appendChild(container);
}
}

119
src/babylonjs/AppModel.ts Normal file
View File

@ -0,0 +1,119 @@
import { ImportMeshAsync } from '@babylonjs/core/Loading/sceneLoader';
import '@babylonjs/loaders/glTF';
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
import { Scene } from '@babylonjs/core/scene';
import { Monobehiver } from '../base/Monobehiver';
import { Dictionary } from '../utils/Dictionary';
import { AppConfig } from './AppConfig';
type LoadResult = {
success: boolean;
meshes?: AbstractMesh[];
skeletons?: unknown[];
error?: string;
};
/**
* 模型管理类- 负责加载、缓存和管理3D模型
*/
export class AppModel extends Monobehiver {
private modelDic: Dictionary<AbstractMesh[]>;
private loadedMeshes: AbstractMesh[];
private skeletonManager: any;
private outfitManager: any;
private isLoading: boolean;
private skeletonMerged: boolean;
constructor(mainApp: any) {
super(mainApp);
this.modelDic = new Dictionary<AbstractMesh[]>();
this.loadedMeshes = [];
this.skeletonManager = null;
this.outfitManager = null;
this.isLoading = false;
this.skeletonMerged = false;
}
/** 初始化子管理器(占位:实际实现已移除) */
initManagers(): void {
// 这里原本会初始化 SkeletonManager 和 OutfitManager已留空以避免恢复已删除的实现
}
/** 加载配置中的所有模型 */
async loadModel(): Promise<void> {
if (!AppConfig.modelUrlList?.length || this.isLoading) return;
this.isLoading = true;
try {
for (const url of AppConfig.modelUrlList) {
await this.loadSingleModel(url);
}
} finally {
this.isLoading = false;
}
}
/**
* 加载单个模型
* @param modelUrl 模型URL
*/
async loadSingleModel(modelUrl: string): Promise<LoadResult> {
try {
const cached = this.getCachedMeshes(modelUrl);
if (cached) return { success: true, meshes: cached };
const scene: Scene | null = this.mainApp.appScene.object;
if (!scene) return { success: false, error: '场景未初始化' };
// ImportMeshAsync的签名与当前调用不完全一致使用any规避编译报错
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result: any = await (ImportMeshAsync as any)(modelUrl, scene);
if (!result?.meshes?.length) return { success: false, error: '未找到网格' };
this.modelDic.Set(modelUrl, result.meshes);
this.loadedMeshes.push(...result.meshes);
this.setupShadows(result.meshes as AbstractMesh[]);
return { success: true, meshes: result.meshes, skeletons: result.skeletons };
} catch (e: any) {
console.error(`模型加载失败: ${modelUrl}`, e);
return { success: false, error: e?.message };
}
}
/** 为网格设置阴影(投射和接收) */
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(url: string): AbstractMesh[] | undefined {
return this.modelDic.Get(url);
}
/** 清理所有资源 */
clean(): void {
this.modelDic.Clear();
this.loadedMeshes.forEach(m => m?.dispose());
this.loadedMeshes = [];
this.skeletonManager?.clean();
this.outfitManager?.clean();
this.isLoading = false;
this.skeletonMerged = false;
}
}

28
src/babylonjs/AppScene.ts Normal file
View File

@ -0,0 +1,28 @@
import { Scene } from '@babylonjs/core/scene';
import { ImageProcessingConfiguration } from '@babylonjs/core/Materials';
import { Color4 } from '@babylonjs/core/Maths/math.color';
import { Monobehiver } from '../base/Monobehiver';
/**
* 场景管理类- 负责创建和管理3D场景
*/
export class AppScene extends Monobehiver {
object: Scene | null;
constructor(mainApp: any) {
super(mainApp);
this.object = null;
}
/** 初始化场景 */
Awake(): void {
this.object = new Scene(this.mainApp.appEngin.object);
this.object.clearColor = new Color4(0, 0, 0, 0); // 透明背景
this.object.skipFrustumClipping = true; // 跳过视锥剔除优化性能
// 1. 开启色调映射(Tone mapping)
// this.object.imageProcessingConfiguration.toneMappingEnabled = true;
// 2. 设置色调映射类型为ACES
// this.object.imageProcessingConfiguration.toneMappingType = ImageProcessingConfiguration.TONEMAPPING_ACES;
}
}

83
src/babylonjs/MainApp.ts Normal file
View File

@ -0,0 +1,83 @@
/**
* @file MainApp.ts
* @description 主应用类,负责初始化和协调所有子模块
*/
import { AppDom } from './AppDom';
import { AppEngin } from './AppEngin';
import { AppScene } from './AppScene';
import { AppCamera } from './AppCamera';
import { AppLight } from './AppLight';
import { AppEnv } from './AppEnv';
import { AppModel } from './AppModel';
import { AppConfig } from './AppConfig';
/**
* 主应用类 - 3D场景的核心控制器
* 负责管理DOM、引擎、场景、相机、灯光、环境、模型和动画等子模块
*/
export class MainApp {
appDom: AppDom;
appEngin: AppEngin;
appScene: AppScene;
appCamera: AppCamera;
appModel: AppModel;
appLight: AppLight;
appEnv: AppEnv;
constructor() {
this.appDom = new AppDom();
this.appEngin = new AppEngin(this);
this.appScene = new AppScene(this);
this.appCamera = new AppCamera(this);
this.appModel = new AppModel(this);
this.appLight = new AppLight(this);
this.appEnv = new AppEnv(this);
window.addEventListener("resize", () => this.appEngin.handleResize());
}
/**
* 加载应用配置
* @param config 配置对象
*/
loadAConfig(config: any): void {
AppConfig.container = config.container || 'renderDom';
AppConfig.modelUrlList = config.modelUrlList || [];
AppConfig.success = config.success;
AppConfig.error = config.error;
}
loadModel(): void {
this.appModel.loadModel();
}
/** 唤醒/初始化所有子模块 */
async Awake(): Promise<void> {
this.appDom.Awake();
this.appEngin.Awake();
this.appScene.Awake();
this.appCamera.Awake();
this.appLight.Awake();
this.appEnv.Awake();
this.appModel.initManagers();
this.update();
}
/** 启动渲染循环 */
update(): void {
if (!this.appEngin.object) return;
this.appEngin.object.runRenderLoop(() => {
this.appScene.object?.render();
this.appCamera.update();
});
}
/** 销毁应用,释放所有资源 */
async dispose(): Promise<void> {
this.appModel?.clean();
this.appEnv?.clean();
}
}