Compare commits

...

21 Commits

Author SHA1 Message Date
2f48948e43 1 2026-03-16 11:15:06 +08:00
12ae95340f 1 2026-03-16 10:15:14 +08:00
248226e553 加入剖切 2026-03-12 21:50:07 +08:00
7fdbf19951 1 2026-03-11 11:56:46 +08:00
ae59fbe68b 增加环境背景参数
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-07 15:23:21 +08:00
b238139773 1
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 18:37:42 +08:00
eba9a3384b 1
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 18:36:05 +08:00
5a3332badf 1
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 18:32:32 +08:00
c409215867 1
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-05 18:20:33 +08:00
47f0961e22 1
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 18:04:30 +08:00
ed5669fe93 1
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 16:50:05 +08:00
99da97fcb4 1
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 16:40:08 +08:00
260c7e706c 优化
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 16:30:14 +08:00
661aa63f9f 移除部分代码
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 16:26:24 +08:00
fe7d9de6f6 修复
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 16:20:04 +08:00
b9cbb58a9d 修复
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 16:09:36 +08:00
ebbd21916e 修复
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 14:17:30 +08:00
58cd883720 修复
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 14:11:45 +08:00
8e65eeb501 修改
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-05 14:05:55 +08:00
b2dbc415c1 修改 2026-01-05 14:05:10 +08:00
6a3509d623 修改 2026-01-05 14:04:46 +08:00
33 changed files with 1658 additions and 502 deletions

View File

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(cd /d/VscodeProject/zhengte.babylonjs-sdk && npm run build 2>&1 | head -50)"
]
}
}

View File

@ -36,7 +36,6 @@ steps:
- main - main
- master - master
- dev - dev
# 第三步:上传构建文件 # 第三步:上传构建文件
- name: 上传构建文件 - name: 上传构建文件
image: appleboy/drone-scp image: appleboy/drone-scp
@ -52,8 +51,8 @@ steps:
# from_secret: server_ssh_key # from_secret: server_ssh_key
port: 22 port: 22
source: source:
- dist/* - dist/**
- dist/bblcdn/* - vite.config.js
target: /www/wwwroot/sdk.zguiy.com/zt/ target: /www/wwwroot/sdk.zguiy.com/zt/
strip_components: 1 strip_components: 1
when: when:

View File

@ -1 +1 @@
VITE_PUBLIC = https://cdn.files.zguiy.com/studio/ VITE_PUBLIC = ./

69
examples/global-demo.html Normal file
View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDK ȫ<>ֹ<EFBFBD><D6B9>ؼ<EFBFBD><D8BC><EFBFBD>ʾ<EFBFBD><CABE></title>
</head>
<body>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
overflow: hidden;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
#renderDom {
width: 100%;
height: 100%;
display: block;
}
</style>
<canvas id="renderDom"></canvas>
<script src="https://sdk.zguiy.com/zt/assets/index.global.js"></script>
<script>
const config = {
container: 'renderDom',
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
};
const sdkKernel = window.faceSDK && window.faceSDK.kernel;
if (!sdkKernel) {
console.error('faceSDK kernel is not available. Confirm index.global.js loaded correctly.');
} else {
sdkKernel.init(config);
sdkKernel.on('model:load:progress', (data) => {
console.log('加载模型中', data);
});
sdkKernel.on('model:loaded', (data) => {
console.log('模型加载完成', data);
});
sdkKernel.on('model:click', (data) => {
console.log('模型点击事件', data);
});
}
</script>
</body>
</html>

69
examples/index.html Normal file
View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDK ȫ<>ֹ<EFBFBD><D6B9>ؼ<EFBFBD><D8BC><EFBFBD>ʾ<EFBFBD><CABE></title>
</head>
<body>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
overflow: hidden;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
#renderDom {
width: 100%;
height: 100%;
display: block;
}
</style>
<canvas id="renderDom"></canvas>
<script src="https://sdk.zguiy.com/zt/assets/index.global.js"></script>
<script>
const config = {
container: 'renderDom',
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
};
const sdkKernel = window.faceSDK && window.faceSDK.kernel;
if (!sdkKernel) {
console.error('faceSDK kernel is not available. Confirm index.global.js loaded correctly.');
} else {
sdkKernel.init(config);
sdkKernel.on('model:load:progress', (data) => {
console.log('加载模型中', data);
});
sdkKernel.on('model:loaded', (data) => {
console.log('模型加载完成', data);
});
sdkKernel.on('model:click', (data) => {
console.log('模型点击事件', data);
});
}
</script>
</body>
</html>

71
examples/module-demo.html Normal file
View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDK 模块化加载示例</title>
</head>
<body>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
overflow: hidden;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
#renderDom {
width: 100%;
height: 100%;
display: block;
}
</style>
<canvas id="renderDom"></canvas>
<!-- 模块化Dev 使用 /src/main.ts构建后改为 /assets/index.js -->
<script type="module">
import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
const config = {
container: 'renderDom',
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3 },
};
kernel.init(config);
kernel.on('model:load:progress', (data) => {
console.log('模型加载事件', data);
});
kernel.on('model:loaded', (data) => {
console.log('模型加载完成', data);
});
kernel.on('model:click', (data) => {
console.log('模型点击事件', data);
});
</script>
</body>
</html>

View File

@ -0,0 +1,5 @@
[
{}
]

BIN
examples/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
examples/public/hdr/hdr.env Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -1,38 +1,108 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D模型展示SDK - TS</title> <title>3D Model Showcase SDK - TS</title><style>
<style> * {
* { margin: 0; padding: 0; box-sizing: border-box; } margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
} }
#app { width: 100vw; height: 100vh; position: relative; }
#renderDom { width: 100%; height: 100%; display: block; } #app {
width: 100vw;
height: 100vh;
position: relative;
}
#renderDom {
width: 100%;
height: 100%;
display: block;
}
</style> </style>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<canvas id="renderDom"></canvas> <canvas id="renderDom"></canvas>
</div> </div>
<script type="module"> <script type="module">
import { kernel } from '/src/main.ts'; import { kernel } from './src/main.ts';
// import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
const config = { const config = {
container: 'renderDom', container: document.querySelector('#renderDom'),
modelUrlList: ["/model/model.glb"], modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
animationUrlList: [], env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
idleAnimationUrlList: [],
onSuccess: () => console.log('SDK initialized'),
onError: (err) => console.error('SDK init error', err),
}; };
kernel.init(config); kernel.init(config);
kernel.on('model:load:progress', (data) => {
console.log('模型加载事件', data);
});
kernel.on('model:loaded', (data) => {
console.log('模型加载完成', data);
});
kernel.on('all:ready', (data) => {
console.log('所有模块加载完,', data);
kernel.material.apply({
target: 'Material__2',
attribute: 'alpha',
value: 0.5,
});
});
kernel.on('model:click', (data) => {
console.log('模型点击事件', data);
// 切换卷帘门开关
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
// Y轴剖切只作用于卷帘门网格保留下方剖掉上方
const clipHeight = 28; // 调整这个值找到合适的剖切高度
console.log('设置剖切:', clipHeight);
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
// 验证剖切是否生效
setTimeout(() => {
const scene = kernel.mainApp?.appScene?.object;
console.log('Scene:', scene);
console.log('Scene clipPlane:', scene?.clipPlane);
console.log('Scene meshes count:', scene?.meshes?.length);
}, 100);
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -2,6 +2,7 @@ import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
import { Vector3 } from '@babylonjs/core/Maths/math.vector'; import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Tools } from '@babylonjs/core/Misc/tools'; import { Tools } from '@babylonjs/core/Misc/tools';
import { Monobehiver } from '../base/Monobehiver'; import { Monobehiver } from '../base/Monobehiver';
import { AppConfig } from './AppConfig';
/** /**
* 相机控制类- 负责创建和控制弧形旋转相机 * 相机控制类- 负责创建和控制弧形旋转相机
@ -17,16 +18,21 @@ export class AppCamera extends Monobehiver {
/** 初始化相机 */ /** 初始化相机 */
Awake(): void { Awake(): void {
const scene = this.mainApp.appScene.object; const scene = this.mainApp.appScene.object;
const canvas = this.mainApp.appDom.renderDom; const canvas = AppConfig.container;
if (!scene || !canvas) return; if (!scene || !canvas) return;
// 创建弧形旋转相机水平角70度垂直角80度距离5目标点(0,1,0) // 创建弧形旋转相机水平角70度垂直角85度接近上帝视角距离5目标点(0,2,0)
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(80), 5, new Vector3(0, 0, 0), scene); this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene);
this.object.attachControl(canvas, true); this.object.attachControl(canvas, true);
this.object.minZ = 0.01; // 近裁剪面 this.object.minZ = 0.01; // 近裁剪面
this.object.wheelPrecision =999999; // 滚轮缩放精度 this.object.wheelPrecision =999999; // 滚轮缩放精度
this.object.panningSensibility = 0; this.object.panningSensibility = 0;
this.object.position = new Vector3(-0, 0, 100);
// 限制垂直角范围,实现上帝视角
// this.object.upperBetaLimit = Tools.ToRadians(60); // 最大垂直角接近90度避免万向锁
// this.object.lowerBetaLimit = Tools.ToRadians(60); // 最小垂直角
this.object.position = new Vector3(-0, 100, 0);
this.setTarget(0, 2, 0); this.setTarget(0, 2, 0);
} }
@ -42,8 +48,11 @@ export class AppCamera extends Monobehiver {
/** 重置相机到默认位置 */ /** 重置相机到默认位置 */
reset(): void { reset(): void {
if (!this.object) return; if (!this.object) return;
this.object.radius = 2; this.setTarget(0, 0, 0); this.object.radius = 5;
this.object.position = new Vector3(0, 1.5, 2); this.object.alpha = Tools.ToRadians(60); // 水平角
this.object.beta = Tools.ToRadians(60); // 垂直角(上帝视角)
this.setTarget(0, 2, 0);
this.object.position = new Vector3(-0, 100, 0);
} }
update(): void { update(): void {

View File

@ -6,8 +6,12 @@ type ErrorCallback = ((error?: unknown) => void) | null | undefined;
* 共享运行时配置对象 * 共享运行时配置对象
*/ */
export const AppConfig = { export const AppConfig = {
container: 'renderDom', container: document.querySelector('#renderDom') as HTMLCanvasElement,
modelUrlList: [] as string[], modelUrlList: [] as string[],
success: null as OptionalCallback, env: {
error: null as ErrorCallback envPath: '/hdr/sanGiuseppeBridge.env',
intensity: 1.5,
rotationY: 0,
background: true,
}
}; };

View File

@ -1,21 +0,0 @@
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;
}
}

View File

@ -1,5 +1,6 @@
import { Engine } from '@babylonjs/core/Engines/engine'; import { Engine } from '@babylonjs/core/Engines/engine';
import { Monobehiver } from '../base/Monobehiver'; import { Monobehiver } from '../base/Monobehiver';
import { AppConfig } from './AppConfig';
/** /**
* 渲染引擎管理类 - 负责创建和管理3D渲染引擎 * 渲染引擎管理类 - 负责创建和管理3D渲染引擎
@ -15,7 +16,7 @@ export class AppEngin extends Monobehiver {
} }
Awake(): void { Awake(): void {
this.canvas = this.mainApp.appDom.renderDom; this.canvas = AppConfig.container;
if (!this.canvas) { if (!this.canvas) {
throw new Error('Render canvas not found'); throw new Error('Render canvas not found');
} }

View File

@ -1,5 +1,6 @@
import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture'; import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture';
import { Monobehiver } from '../base/Monobehiver'; import { Monobehiver } from '../base/Monobehiver';
import { AppConfig } from './AppConfig';
/** /**
* 环境管理类- 负责创建和管理HDR环境贴图 * 环境管理类- 负责创建和管理HDR环境贴图
@ -21,13 +22,39 @@ export class AppEnv extends Monobehiver {
* 创建HDR环境贴图 * 创建HDR环境贴图
* @param hdrPath HDR文件路径 * @param hdrPath HDR文件路径
*/ */
createHDR(hdrPath = '/hdr/sanGiuseppeBridge.env'): void { createHDR(): void {
const envPath = AppConfig.env.envPath;
const intensity = AppConfig.env.intensity ?? 1.5;
const rotationY = AppConfig.env.rotationY ?? 0;
const scene = this.mainApp.appScene.object; const scene = this.mainApp.appScene.object;
if (!scene) return; if (!scene) return;
const reflectionTexture = CubeTexture.CreateFromPrefilteredData(hdrPath, scene); if (this.object) {
scene.environmentIntensity = 1.5; this.object.dispose();
this.object = null;
}
const reflectionTexture = CubeTexture.CreateFromPrefilteredData(envPath, scene);
reflectionTexture.rotationY = rotationY;
scene.environmentIntensity = intensity;
scene.environmentTexture = reflectionTexture; scene.environmentTexture = reflectionTexture;
this.object = reflectionTexture; this.object = reflectionTexture;
scene.backgroundTexture = reflectionTexture;
const box = scene.createDefaultSkybox(
reflectionTexture,
true,
512,
0,
true
);
console.log('box', AppConfig.env.background);
if (AppConfig.env.background) {
if (box) box.visibility = 1;
} else {
if (box) box.visibility = 0;
}
// 保存环境纹理的引用
this.object = reflectionTexture;
} }
/** /**

View File

@ -1,4 +1,4 @@
import { SpotLight } from '@babylonjs/core/Lights/spotLight'; import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight';
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'; import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'; import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent';
import { Vector3, Quaternion } from '@babylonjs/core/Maths/math.vector'; import { Vector3, Quaternion } from '@babylonjs/core/Maths/math.vector';
@ -7,7 +7,6 @@ import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { Color3 } from '@babylonjs/core/Maths/math.color'; import { Color3 } from '@babylonjs/core/Maths/math.color';
import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager'; import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
import { Mesh } from '@babylonjs/core/Meshes/mesh'; import { Mesh } from '@babylonjs/core/Meshes/mesh';
type DebugMarkers = { type DebugMarkers = {
@ -21,7 +20,7 @@ type DebugMarkers = {
* 灯光管理类- 负责创建和管理场景灯光 * 灯光管理类- 负责创建和管理场景灯光
*/ */
export class AppLight extends Monobehiver { export class AppLight extends Monobehiver {
lightList: SpotLight[]; lightList: DirectionalLight[];
shadowGenerator: ShadowGenerator | null; shadowGenerator: ShadowGenerator | null;
debugMarkers?: DebugMarkers; debugMarkers?: DebugMarkers;
coneMesh?: Mesh; coneMesh?: Mesh;
@ -35,175 +34,9 @@ export class AppLight extends Monobehiver {
/** 初始化灯光并开启阴影 */ /** 初始化灯光并开启阴影 */
Awake(): void { Awake(): void {
const light = new SpotLight( const light = new DirectionalLight(
"mainLight", "mainLight",
new Vector3(-0.6, 2.12, 2),
new Vector3(0, -0.5, -1), 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);
} }
} }

View File

@ -1,10 +1,12 @@
import { ImportMeshAsync } from '@babylonjs/core/Loading/sceneLoader'; import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader';
import '@babylonjs/loaders/glTF'; import '@babylonjs/loaders/glTF';
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
import { Scene } from '@babylonjs/core/scene'; import { Scene } from '@babylonjs/core/scene';
import { ActionManager, ExecuteCodeAction } from '@babylonjs/core/Actions';
import { Monobehiver } from '../base/Monobehiver'; import { Monobehiver } from '../base/Monobehiver';
import { Dictionary } from '../utils/Dictionary'; import { Dictionary } from '../utils/Dictionary';
import { AppConfig } from './AppConfig'; import { AppConfig } from './AppConfig';
import { EventBridge } from '../event/bridge';
type LoadResult = { type LoadResult = {
success: boolean; success: boolean;
@ -44,10 +46,45 @@ export class AppModel extends Monobehiver {
if (!AppConfig.modelUrlList?.length || this.isLoading) return; if (!AppConfig.modelUrlList?.length || this.isLoading) return;
this.isLoading = true; this.isLoading = true;
try { try {
for (const url of AppConfig.modelUrlList) { const total = AppConfig.modelUrlList.length;
await this.loadSingleModel(url); EventBridge.modelLoadProgress({ loaded: 0, total, urls: AppConfig.modelUrlList, progress: 0, percentage: 0 });
} for (let i = 0; i < AppConfig.modelUrlList.length; i++) {
const url = AppConfig.modelUrlList[i];
const handleProgress = (event: ISceneLoaderProgressEvent): void => {
const currentProgress = event.lengthComputable && event.total > 0
? Math.min(1, event.loaded / event.total)
: 0;
const overallProgress = Math.min(1, (i + currentProgress) / total);
EventBridge.modelLoadProgress({
loaded: i + currentProgress,
total,
url,
progress: overallProgress,
percentage: Number((overallProgress * 100).toFixed(2)),
detail: {
url,
lengthComputable: event.lengthComputable,
loadedBytes: event.loaded,
totalBytes: event.total
}
});
};
const result = await this.loadSingleModel(url, handleProgress);
const overallProgress = Math.min(1, (i + 1) / total);
EventBridge.modelLoadProgress({
loaded: i + 1,
total,
url,
success: result.success,
progress: overallProgress,
percentage: Number((overallProgress * 100).toFixed(2))
});
if (!result.success) {
EventBridge.modelLoadError({ url, error: result.error });
}
}
EventBridge.modelLoaded({ urls: AppConfig.modelUrlList });
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
@ -57,7 +94,7 @@ export class AppModel extends Monobehiver {
* 加载单个模型 * 加载单个模型
* @param modelUrl 模型URL * @param modelUrl 模型URL
*/ */
async loadSingleModel(modelUrl: string): Promise<LoadResult> { async loadSingleModel(modelUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void): Promise<LoadResult> {
try { try {
const cached = this.getCachedMeshes(modelUrl); const cached = this.getCachedMeshes(modelUrl);
if (cached) return { success: true, meshes: cached }; if (cached) return { success: true, meshes: cached };
@ -65,16 +102,11 @@ export class AppModel extends Monobehiver {
const scene: Scene | null = this.mainApp.appScene.object; const scene: Scene | null = this.mainApp.appScene.object;
if (!scene) return { success: false, error: '场景未初始化' }; if (!scene) return { success: false, error: '场景未初始化' };
// ImportMeshAsync的签名与当前调用不完全一致使用any规避编译报错 const result = await ImportMeshAsync(modelUrl, scene, { onProgress });
// 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: '未找到网格' }; if (!result?.meshes?.length) return { success: false, error: '未找到网格' };
this.modelDic.Set(modelUrl, result.meshes); this.modelDic.Set(modelUrl, result.meshes);
this.loadedMeshes.push(...result.meshes); this.loadedMeshes.push(...result.meshes);
this.setupShadows(result.meshes as AbstractMesh[]);
return { success: true, meshes: result.meshes, skeletons: result.skeletons }; return { success: true, meshes: result.meshes, skeletons: result.skeletons };
} catch (e: any) { } catch (e: any) {
console.error(`模型加载失败: ${modelUrl}`, e); console.error(`模型加载失败: ${modelUrl}`, e);
@ -116,4 +148,30 @@ export class AppModel extends Monobehiver {
this.isLoading = false; this.isLoading = false;
this.skeletonMerged = false; this.skeletonMerged = false;
} }
/**
* 销毁指定模型
* @param modelName 模型名称
*/
destroyModel(modelName: string): void {
// 遍历模型字典,查找匹配的模型
const keys = this.modelDic.Keys();
for (const key of keys) {
if (key.includes(modelName)) {
const meshes = this.modelDic.Get(key);
if (meshes) {
// 销毁所有网格
meshes.forEach(mesh => mesh?.dispose());
// 从字典中移除
this.modelDic.Remove(key);
// 从加载的网格列表中移除
this.loadedMeshes = this.loadedMeshes.filter(mesh => !meshes.includes(mesh));
console.log(`Model destroyed: ${modelName}`);
return;
}
}
}
console.warn(`Model not found: ${modelName}`);
}
} }

143
src/babylonjs/AppRay.ts Normal file
View File

@ -0,0 +1,143 @@
import {
type IPointerEvent,
PickingInfo,
PointerEventTypes,
Vector3,
AbstractMesh,
Color3,
PBRMaterial,
StandardMaterial,
HighlightLayer,
PointerInfo,
} from '@babylonjs/core'
import { MainApp } from './MainApp'
import { Monobehiver } from '../base/Monobehiver';
import { EventBridge } from '../event/bridge';
class AppRay extends Monobehiver {
oldPoint: Vector3 = Vector3.Zero()
newPoint: Vector3 = Vector3.Zero()
private highlightLayer: HighlightLayer | null = null
private originalMaterial: any = null
private highlightedMesh: AbstractMesh | null = null
constructor(mainApp: MainApp) {
super(mainApp)
}
Awake() {
this.setupHighlightLayer()
this.setupUnifiedEventHandling()
}
// 设置高亮层
setupHighlightLayer() {
// 高亮层创建已禁用
return
}
// 设置统一的事件处理
setupUnifiedEventHandling() {
// 使用观察者模式而不是直接覆盖事件处理器
this.mainApp.appScene.object.onPointerObservable.add((pointerInfo: PointerInfo) => {
const { type, event, pickInfo } = pointerInfo;
// 检查事件类型并转换
const pointerEvent = event as IPointerEvent;
// 只处理鼠标和触摸事件
if (pointerEvent.pointerType !== "mouse" && pointerEvent.pointerType !== "touch") {
return;
}
// 处理非主要触摸点
if (pointerEvent.pointerType === "touch" && (pointerEvent as any).isPrimary === false) {
return;
}
if (type === PointerEventTypes.POINTERDOWN) {
this.oldPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
} else if (type === PointerEventTypes.POINTERUP) {
this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
const distance = Vector3.Distance(this.oldPoint, this.newPoint);
// 只有在没有移动的情况下才处理单击
if (distance < 5) { // 增加一些容差
this.handleSingleClick(pointerEvent, pickInfo);
}
}
});
}
// 处理单击
handleSingleClick(evt: IPointerEvent, pickInfo: PickingInfo | null) {
if (pickInfo && pickInfo.pickedMesh) {
EventBridge.modelClick({
meshName: pickInfo.pickedMesh.name,
});
}
}
// 高亮显示网格 - 已禁用
highlightMesh(mesh: AbstractMesh) {
// 高亮功能已禁用
return
}
// 使用材质方式高亮 - 已禁用
highlightWithMaterial(mesh: AbstractMesh) {
// 材质高亮功能已禁用
return
}
// 清除高亮
clearHighlight() {
try {
// 清除高亮层
if (this.highlightLayer && this.highlightedMesh) {
try {
this.highlightLayer.removeMesh(this.highlightedMesh as any)
} catch (error) {
console.warn('高亮层移除失败:', error)
}
}
// 恢复原始材质
if (this.highlightedMesh && this.originalMaterial) {
const material = this.highlightedMesh.material as PBRMaterial
if (material && this.originalMaterial.albedoColor) {
material.albedoColor = this.originalMaterial.albedoColor
material.emissiveColor = this.originalMaterial.emissiveColor
}
}
this.highlightedMesh = null
this.originalMaterial = null
} catch (error) {
console.error('清除高亮失败:', error)
}
}
/**
* 渲染热点
* @param hotspots 热点数据
*/
renderHotspots(hotspots: any[]): void {
console.log('Rendering hotspots:', hotspots);
// 这里需要根据实际的热点渲染逻辑实现
// 示例实现:
// 1. 清除现有的热点
// 2. 根据热点数据创建新的热点标记
// 3. 为热点添加交互事件
hotspots.forEach((hotspot, index) => {
console.log(`Rendering hotspot ${index}:`, hotspot);
// 这里需要根据实际的热点数据结构实现
});
}
}
export { AppRay }

View File

@ -19,10 +19,5 @@ export class AppScene extends Monobehiver {
this.object = new Scene(this.mainApp.appEngin.object); this.object = new Scene(this.mainApp.appEngin.object);
this.object.clearColor = new Color4(0, 0, 0, 0); // 透明背景 this.object.clearColor = new Color4(0, 0, 0, 0); // 透明背景
this.object.skipFrustumClipping = true; // 跳过视锥剔除优化性能 this.object.skipFrustumClipping = true; // 跳过视锥剔除优化性能
// 1. 开启色调映射(Tone mapping)
// this.object.imageProcessingConfiguration.toneMappingEnabled = true;
// 2. 设置色调映射类型为ACES
// this.object.imageProcessingConfiguration.toneMappingType = ImageProcessingConfiguration.TONEMAPPING_ACES;
} }
} }

View File

@ -0,0 +1,770 @@
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';
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;
}
// 初始化材质字典
for (const mat of scene.materials) {
if (!this.materialDic.Has(mat.name)) {
// 初始化材质属性
// mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND;
this.materialDic.Set(mat.name, mat as PBRMaterial);
}
}
// 初始化网格字典
for (const mesh of scene.meshes) {
if (mesh instanceof Mesh) {
this.meshDic.Set(mesh.name, mesh);
}
}
this.cacheRollerDoorMeshes();
console.log('材质字典:', this.materialDic);
this.setRollerDoorScale("Box006.001", new Vector3(0.12,0.02,0.118));
// 单独设置 Box005.001 的缩放为 (1, 2, 1)
this.setRollerDoorScale("Box005.001", new Vector3(0.13,0.02,0.12));
}
/** 初始化设置材质 */
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 作为组
this.rollerDoorGroup = new AbstractMesh('rollerDoorGroup', scene);
// 确保 group 的缩放为 1
// 确保 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 target 目标对象
* @param material 材质路径
*/
applyMaterial(target: string, attribute: string, value: number | string): void {
// 这里需要根据实际的材质管理逻辑实现
console.log(`Applying attribute ${attribute} to ${value}`);
// 示例实现:根据目标和材质路径应用材质
// 1. 查找目标网格
const targetMaterials: PBRMaterial[] = [];
this.materialDic.Values().forEach(material => {
if (material.name.includes(target)) {
console.log(`${this.materialDic.Get(material.name)}`, material);
targetMaterials.push(material);
}
});
if (targetMaterials.length === 0) {
console.warn(`Target not found: ${target}`);
return;
}
// 2. 处理材质路径
// 这里可以根据材质路径加载对应的材质配置
// 例如paint/blue 可以映射到特定的材质配置
// 3. 应用材质到目标网格
targetMaterials.forEach(material => {
if (material[attribute]) {
material[attribute] = value;
}
console.log(`Applying attribute ${attribute} to ${value} to mesh: ${material.name}`);
// 这里需要根据实际的材质系统实现
});
}
}

View File

@ -3,7 +3,6 @@
* @description 主应用类,负责初始化和协调所有子模块 * @description 主应用类,负责初始化和协调所有子模块
*/ */
import { AppDom } from './AppDom';
import { AppEngin } from './AppEngin'; import { AppEngin } from './AppEngin';
import { AppScene } from './AppScene'; import { AppScene } from './AppScene';
import { AppCamera } from './AppCamera'; import { AppCamera } from './AppCamera';
@ -11,29 +10,34 @@ import { AppLight } from './AppLight';
import { AppEnv } from './AppEnv'; import { AppEnv } from './AppEnv';
import { AppModel } from './AppModel'; import { AppModel } from './AppModel';
import { AppConfig } from './AppConfig'; import { AppConfig } from './AppConfig';
import { AppRay } from './AppRay';
import { GameManager } from './GameManager';
import { EventBridge } from '../event/bridge';
/** /**
* 主应用类 - 3D场景的核心控制器 * 主应用类 - 3D场景的核心控制器
* 负责管理DOM、引擎、场景、相机、灯光、环境、模型和动画等子模块 * 负责管理DOM、引擎、场景、相机、灯光、环境、模型和动画等子模块
*/ */
export class MainApp { export class MainApp {
appDom: AppDom;
appEngin: AppEngin; appEngin: AppEngin;
appScene: AppScene; appScene: AppScene;
appCamera: AppCamera; appCamera: AppCamera;
appModel: AppModel; appModel: AppModel;
appLight: AppLight; appLight: AppLight;
appEnv: AppEnv; appEnv: AppEnv;
appRay: AppRay;
gameManager: GameManager;
constructor() { constructor() {
this.appDom = new AppDom();
this.appEngin = new AppEngin(this); this.appEngin = new AppEngin(this);
this.appScene = new AppScene(this); this.appScene = new AppScene(this);
this.appCamera = new AppCamera(this); this.appCamera = new AppCamera(this);
this.appModel = new AppModel(this); this.appModel = new AppModel(this);
this.appLight = new AppLight(this); this.appLight = new AppLight(this);
this.appEnv = new AppEnv(this); this.appEnv = new AppEnv(this);
this.appRay = new AppRay(this);
this.gameManager = new GameManager(this);
window.addEventListener("resize", () => this.appEngin.handleResize()); window.addEventListener("resize", () => this.appEngin.handleResize());
} }
@ -43,27 +47,28 @@ export class MainApp {
* @param config 配置对象 * @param config 配置对象
*/ */
loadAConfig(config: any): void { loadAConfig(config: any): void {
AppConfig.container = config.container || 'renderDom'; AppConfig.container = config.container;
AppConfig.modelUrlList = config.modelUrlList || []; AppConfig.modelUrlList = config.modelUrlList || [];
AppConfig.success = config.success; AppConfig.env = config.env;
AppConfig.error = config.error;
} }
loadModel(): void { async loadModel(): Promise<void> {
this.appModel.loadModel(); await this.appModel.loadModel();
await this.gameManager.Awake();
EventBridge.allReady({ scene: this.appScene.object });
} }
/** 唤醒/初始化所有子模块 */ /** 唤醒/初始化所有子模块 */
async Awake(): Promise<void> { async Awake(): Promise<void> {
this.appDom.Awake();
this.appEngin.Awake(); this.appEngin.Awake();
this.appScene.Awake(); this.appScene.Awake();
this.appCamera.Awake(); this.appCamera.Awake();
this.appLight.Awake(); this.appLight.Awake();
this.appEnv.Awake(); this.appEnv.Awake();
this.appRay.Awake()
this.appModel.initManagers(); this.appModel.initManagers();
this.update(); this.update();
EventBridge.sceneReady({ scene: this.appScene.object });
} }
/** 启动渲染循环 */ /** 启动渲染循环 */

68
src/event/bridge.ts Normal file
View File

@ -0,0 +1,68 @@
import { emit, on, once, off, Emitter } from './bus';
import {
ModelClickPayload,
ModelLoadedPayload,
ModelLoadErrorPayload,
ModelLoadProgressPayload,
SceneReadyPayload
} from './types';
/**
* Centralized event helpers to avoid spreading raw event strings.
*/
export class EventBridge {
// Emits
static modelLoadProgress(payload: ModelLoadProgressPayload): Emitter {
return emit("model:load:progress", payload);
}
static modelLoadError(payload: ModelLoadErrorPayload): Emitter {
return emit("model:load:error", payload);
}
static modelLoaded(payload: ModelLoadedPayload): Emitter {
return emit("model:loaded", payload);
}
static modelClick(payload: ModelClickPayload): Emitter {
return emit("model:click", payload);
}
static sceneReady(payload: SceneReadyPayload): Emitter {
return emit("scene:ready", payload);
}
static allReady(payload: SceneReadyPayload): Emitter {
return emit("all:ready", payload);
}
// Listeners
static onModelLoadProgress(callback: (payload: ModelLoadProgressPayload) => void, context?: unknown): Emitter {
return on("model:load:progress", callback, context);
}
static onModelLoadError(callback: (payload: ModelLoadErrorPayload) => void, context?: unknown): Emitter {
return on("model:load:error", callback, context);
}
static onModelLoaded(callback: (payload: ModelLoadedPayload) => void, context?: unknown): Emitter {
return on("model:loaded", callback, context);
}
static onModelClick(callback: (payload: ModelClickPayload) => void, context?: unknown): Emitter {
return on("model:click", callback, context);
}
static onSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
return on("scene:ready", callback, context);
}
static onAllReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
return on("all:ready", callback, context);
}
static onceSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
return once("scene:ready", callback, context);
}
static off(eventName?: string, callback?: (...args: unknown[]) => void): Emitter {
return off(eventName, callback);
}
}

82
src/event/bus.ts Normal file
View File

@ -0,0 +1,82 @@
type Listener = {
callback: (...args: unknown[]) => void;
context?: unknown;
};
export class Emitter {
private _events: Record<string, Listener[]> = {};
on(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
if (!this._events[name]) {
this._events[name] = [];
}
this._events[name].push({ callback, context });
return this;
}
once(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
const onceWrapper = (...args: unknown[]) => {
this.off(name, onceWrapper);
callback.apply(context, args);
};
return this.on(name, onceWrapper, context);
}
off(name?: string, callback?: (...args: unknown[]) => void): this {
if (!name) {
this._events = {};
return this;
}
if (!this._events[name]) return this;
if (!callback) {
delete this._events[name];
return this;
}
this._events[name] = this._events[name].filter(
listener => listener.callback !== callback
);
return this;
}
removeAllListeners(): this {
this._events = {};
return this;
}
emit(name: string, ...args: unknown[]): this {
if (!this._events[name]) return this;
this._events[name].forEach(listener => {
listener.callback.apply(listener.context, args);
});
return this;
}
listenerCount(name: string): number {
return this._events[name]?.length ?? 0;
}
}
export class EventBus extends Emitter { }
export const eventBus = new EventBus();
export const on = (eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter => {
return eventBus.on(eventName, callback, context);
};
export const off = (eventName?: string, callback?: (...args: unknown[]) => void): Emitter => {
return eventBus.off(eventName, callback);
};
export const once = (eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter => {
return eventBus.once(eventName, callback, context);
};
export const emit = (eventName: string, ...args: unknown[]): Emitter => {
return eventBus.emit(eventName, ...args);
};
export const removeAllListeners = (eventName?: string): Emitter => {
if (eventName) return eventBus.off(eventName);
return eventBus.removeAllListeners();
};

37
src/event/types.ts Normal file
View File

@ -0,0 +1,37 @@
import { Scene } from '@babylonjs/core/scene';
export type ModelLoadProgressDetail = {
url?: string;
lengthComputable?: boolean;
loadedBytes?: number;
totalBytes?: number;
};
export type ModelLoadProgressPayload = {
loaded: number;
total: number;
url?: string;
urls?: string[];
success?: boolean;
progress?: number;
percentage?: number;
detail?: ModelLoadProgressDetail;
};
export type ModelLoadErrorPayload = {
url: string;
error?: unknown;
};
export type ModelLoadedPayload = {
urls: string[];
};
export type ModelClickPayload = {
meshName?: string;
};
export type SceneReadyPayload = {
scene: Scene | null;
};

79
src/kernel/Adapter.ts Normal file
View File

@ -0,0 +1,79 @@
import { MainApp } from '../babylonjs/MainApp';
/**
* Kernel 转接器类 - 封装 mainApp 的功能提供统一<E7BB9F>?API 接口
*/
export class KernelAdapter {
private mainApp: MainApp;
constructor(mainApp: MainApp) {
this.mainApp = mainApp;
}
/** 模型管理 */
model = {
/**
* 销毁指定模<E5AE9A>? * @param modelName 模型名称
*/
destroy: (modelName: string): void => {
this.mainApp.appModel.destroyModel(modelName);
}
};
/** 材质管理 */
material = {
/**
* 应用材质
* @param options 材质应用选项
*/
apply: (options: { target: string; attribute: string,value:number|string }): void => {
this.mainApp.gameManager.applyMaterial(options.target, options.attribute,options.value);
}
};
/** 卷帘门控<E997A8>?*/
door = {
/** 再次调用会自动反向动<E59091>?*/
toggle: (options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
this.mainApp.gameManager.toggleRollerDoor(options);
},
/** 显式设置开/<2F>?*/
setState: (open: boolean, options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
this.mainApp.gameManager.setRollerDoorState(open, options);
},
/** 当前是否已开<E5B7B2>?*/
isOpen: (): boolean => {
return this.mainApp.gameManager.isRollerDoorOpen();
}
};
/** Y 轴剖<E8BDB4>?*/
clipping = {
/** <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>и߶ȣ<DFB6>keepAbove=true ʱ<><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϲ<EFBFBD><CFB2>֣<EFBFBD>onlyMeshNames Ϊ<><CEAA>Ĭ<EFBFBD>Ͻ<EFBFBD><CFBD><EFBFBD><EFBFBD>þ<EFBFBD><C3BE><EFBFBD><EFBFBD>ţ<EFBFBD>excludeMeshNames <20><><EFBFBD><EFBFBD><EFBFBD>ų<EFBFBD><C5B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD><D0B5><EFBFBD><EFBFBD><EFBFBD> */
setY: (height: number, keepAbove = true, onlyMeshNames?: string[], excludeMeshNames?: string[]): void => {
this.mainApp.gameManager.setYAxisClip(height, keepAbove, onlyMeshNames, excludeMeshNames);
},
/** 关闭剖切 */
clear: (): void => {
this.mainApp.gameManager.clearYAxisClip();
}
};
/** 热点管理 */
hotspot = {
/**
* 渲染热点
* @param hotspots 热点数据
*/
render: (hotspots: any[]): void => {
this.mainApp.appRay.renderHotspots(hotspots);
}
};
/** 调试工具 */
debug = {
/** 列出当前场景网格名称 */
listMeshNames: (): string[] => {
return this.mainApp.gameManager.listMeshNames();
}
};
}

View File

@ -1,7 +1,10 @@
import { MainApp } from './babylonjs/MainApp'; import { MainApp } from './babylonjs/MainApp';
import { AppConfig } from './babylonjs/AppConfig';
import configurator, { ConfiguratorParams } from './components/conf'; import configurator, { ConfiguratorParams } from './components/conf';
import auth from './components/auth'; import auth from './components/auth';
import { on } from './utils/event'; import { on, off, once, emit } from './event/bus';
import { EventBridge } from './event/bridge';
import { KernelAdapter } from './kernel/Adapter';
declare global { declare global {
interface Window { interface Window {
@ -11,39 +14,46 @@ declare global {
} }
type InitParams = { type InitParams = {
container?: string; container?: string | HTMLCanvasElement;
modelUrlList?: string[]; modelUrlList?: string[];
animationUrlList?: string[];
idleAnimationUrlList?: string[];
onSuccess?: () => void;
onError?: (error?: unknown) => void;
apiConfig?: ConfiguratorParams; apiConfig?: ConfiguratorParams;
env?: {
hdrPath?: string;
intensity?: number;
rotationY?: number;
background?: boolean;
};
}; };
let mainApp: MainApp | null = null; let mainApp: MainApp | null = null;
let kernelAdapter: KernelAdapter | null = null;
const kernel = { const kernel = {
// 事件工具,提供给外部订阅/退订
on,
off,
once,
emit,
/** 初始化应用 */ /** 初始化应用 */
init: async function (params: InitParams): Promise<void> { init: async function (params: InitParams): Promise<void> {
if (!params) { console.error('params is required'); return; } if (!params) { console.error('params is required'); return; }
if (params.apiConfig) {
await configurator.init(params.apiConfig);
if (params.apiConfig.name) {
const userInfo = await auth.login(params.apiConfig.name);
if (!userInfo) {
console.error('failed to fetch user');
return;
}
}
}
mainApp = new MainApp(); mainApp = new MainApp();
kernelAdapter = new KernelAdapter(mainApp);
// 展开转接器的属性和方法到kernel对象
Object.assign(kernel, kernelAdapter);
const container = (typeof params.container === 'string'
? (document.querySelector(params.container) || document.getElementById(params.container))
: params.container || document.querySelector('#renderDom')) as HTMLCanvasElement | null;
if (!container) { throw new Error('Render canvas not found'); }
mainApp.loadAConfig({ mainApp.loadAConfig({
container: params.container || 'renderDom', container,
modelUrlList: params.modelUrlList || [], modelUrlList: params.modelUrlList || [],
success: params.onSuccess ?? null, env: params.env
error: params.onError ?? null
}); });
await mainApp.Awake(); await mainApp.Awake();
@ -56,17 +66,4 @@ if (!window.faceSDK) {
} }
window.faceSDK.kernel = kernel; window.faceSDK.kernel = kernel;
if (!window.yiyu) {
window.yiyu = {};
}
window.yiyu.kernel = kernel;
window.yiyu.onAppLoaded = () => { };
window.yiyu.onSingleSignFinished = (_text: string) => { };
window.yiyu.onSentenceFinished = (_text: string) => { };
window.yiyu.onSentenceChanged = (_data: unknown) => { };
window.yiyu.onGloss = (_gloss: unknown) => { };
window.onload = () => { };
export { kernel }; export { kernel };

View File

@ -1,25 +0,0 @@
/**
* @file compressor.js
* @description 解压缩工具使用pako完成与GoBetterStudio相同的deflate/raw解码
*/
import pako from 'pako';
const decoder = new TextDecoder();
export const compressor = {
/** 压缩字符串或字节数据 */
compress(data: Uint8Array | string): Uint8Array {
const source = typeof data === 'string' ? new TextEncoder().encode(data) : data;
return pako.deflateRaw(source);
},
/** 解压缩字节数据并返回字符串 */
decompress(data: ArrayBuffer | Uint8Array): string {
const input = data instanceof Uint8Array ? data : new Uint8Array(data);
const uncompressed = pako.inflateRaw(input);
return decoder.decode(uncompressed);
}
};
export default compressor;

View File

@ -1,73 +0,0 @@
import md5 from 'js-md5';
const KEY_SIZE = 256 / 32;
const ITERATIONS = 1000;
type EncryptedAsset = {
value: Uint8Array;
Timestamp: string;
};
type Credential = {
token: string;
uid: string;
};
type CryptoMaterial = {
obj: Uint8Array;
iv: Uint8Array;
salt: Uint8Array;
key: string;
};
function prepareCryptoMaterial(data: EncryptedAsset, info: Credential): CryptoMaterial {
const userIdPrefix = info.uid.slice(0, 16);
const iv = new TextEncoder().encode(userIdPrefix);
const obj = data.value.slice(7);
const saltHex = md5(`${info.token}${data.Timestamp}`);
const saltBytes = new Uint8Array((saltHex.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16)));
return { obj, iv, salt: saltBytes, key: info.token };
}
/** 从服务器返回的数据中异步解密出原始GLTF内容 */
export async function decryptAsync(data: EncryptedAsset, info: Credential): Promise<ArrayBuffer | null> {
const { obj, iv, salt, key } = prepareCryptoMaterial(data, info);
const derivedKey = await generateAesKeyAsync(key, salt, KEY_SIZE, ITERATIONS);
return aesDecryptAsync(obj, derivedKey, iv);
}
async function generateAesKeyAsync(secret: string, salt: Uint8Array, keySize: number, iterations: number): Promise<CryptoKey> {
const passwordBuffer = new TextEncoder().encode(secret);
const baseKey = await crypto.subtle.importKey(
'raw',
passwordBuffer,
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations,
hash: 'SHA-1'
},
baseKey,
{ name: 'AES-CBC', length: keySize * 32 },
false,
['decrypt']
);
}
async function aesDecryptAsync(data: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise<ArrayBuffer | null> {
try {
return await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
key,
data
);
} catch (error) {
console.error('解密失败:', error);
return null;
}
}

View File

@ -1,123 +0,0 @@
type Listener = {
callback: (...args: unknown[]) => void;
context?: unknown;
};
type EventMeta = {
type: string;
description: string;
listeners: Listener[];
};
/**
* 基础事件发射器
*/
export class Emitter {
private _events: Record<string, Listener[]> = {};
on(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
if (!this._events[name]) {
this._events[name] = [];
}
this._events[name].push({ callback, context });
return this;
}
once(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
const onceWrapper = (...args: unknown[]) => {
this.off(name, onceWrapper);
callback.apply(context, args);
};
return this.on(name, onceWrapper, context);
}
off(name?: string, callback?: (...args: unknown[]) => void): this {
if (!name) {
this._events = {};
return this;
}
if (!this._events[name]) return this;
if (!callback) {
delete this._events[name];
return this;
}
this._events[name] = this._events[name].filter(
listener => listener.callback !== callback
);
return this;
}
removeAllListeners(): this {
this._events = {};
return this;
}
emit(name: string, ...args: unknown[]): this {
if (!this._events[name]) return this;
this._events[name].forEach(listener => {
listener.callback.apply(listener.context, args);
});
return this;
}
listenerCount(name: string): number {
return this._events[name]?.length ?? 0;
}
}
/**
* 全局事件管理器
*/
export class EventManager extends Emitter {
private eventMap: Map<string, EventMeta>;
constructor() {
super();
this.eventMap = new Map();
}
registerEvent(type: string, description: string): void {
this.eventMap.set(type, { type, description, listeners: [] });
}
getRegisteredEvents(): EventMeta[] {
return Array.from(this.eventMap.values());
}
}
// 创建全局事件管理器实例
export const eventManager = new EventManager();
// 注册标准事件类型(描述使用英文,避免编码问题)
eventManager.registerEvent('load', 'resource load complete');
eventManager.registerEvent('load-progress', 'resource load progress');
eventManager.registerEvent('load-error', 'resource load error');
eventManager.registerEvent('animation-start', 'animation start');
eventManager.registerEvent('animation-end', 'animation end');
eventManager.registerEvent('animation-loop', 'animation loop');
eventManager.registerEvent('model-change', 'model change');
eventManager.registerEvent('camera-change', 'camera change');
eventManager.registerEvent('scene-ready', 'scene ready');
eventManager.registerEvent('dispose', 'component disposed');
// 导出便捷函数
export function on(eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter {
return eventManager.on(eventName, callback, context);
}
export function off(eventName?: string, callback?: (...args: unknown[]) => void): Emitter {
return eventManager.off(eventName, callback);
}
export function once(eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter {
return eventManager.once(eventName, callback, context);
}
export function emit(eventName: string, ...args: unknown[]): Emitter {
return eventManager.emit(eventName, ...args);
}
export function removeAllListeners(eventName?: string): Emitter {
if (eventName) return eventManager.off(eventName);
return eventManager.removeAllListeners();
}

View File

@ -10,8 +10,8 @@ export default defineConfig({
lib: { lib: {
entry: 'src/main.ts', entry: 'src/main.ts',
name: 'kernel', name: 'kernel',
formats: ['esm'], formats: ['es', 'iife'],
fileName: () => 'assets/index.js', fileName: (format) => format === 'es' ? 'assets/index.js' : 'assets/index.global.js',
}, },
target: 'esnext', target: 'esnext',
outDir: 'dist', outDir: 'dist',