Skip to content

Playground

本页把 @tripo3d/engine最佳实践方式拆成 System / Script / Component,覆盖: 本地 GLB/GLTF 加载、BVH 加速 raycaster、OrbitControls、Transform Gizmo、Viewport Gizmo、Stats 性能监控、指针事件日志。

架构总览

源码目录:

_playground-demos/
├── Playground.vue                   # UI 编排(不含 Three.js 逻辑)
├── engine-setup.ts                  # 引擎工厂:光照 + 网格 + 注册 5 个 system
├── systems/
│   ├── camera-system.ts             # OrbitControls + ViewportGizmo
│   ├── transform-system.ts          # TransformControls + pointer lock
│   ├── model-loader-system.ts       # GLB 加载 + BVH + 拾取脚本装配
│   ├── stats-system.ts              # Stats.js 面板
│   └── pointer-log-system.ts        # 全局事件日志 + miss handler
├── scripts/
│   └── pickable-mesh.ts             # 挂在 mesh entity 上的 click/hover script
├── components/
│   └── bvh-index.ts                 # 已建 BVH 的 marker component
└── utils/
    ├── bvh.ts                       # BVH 原型补丁 + build/dispose 工具
    └── glb-loader.ts                # GLTFLoader 封装 + geometry/material 释放

三层抽象的职责切分

  • System (engine-wide singleton)状态机,有 setup 与生命周期钩子(onReady/onUpdate/onDispose 等)。适合「一个 engine 里只需要一份」的能力
    • CameraSystemOrbitControls + ViewportGizmo,suspendControls() 提供引用计数暂停
    • TransformSystemTransformControls;拖拽时 engine.pointer.suspend() + 相机 suspend,rAF 延迟释放避免 click 穿透
    • ModelLoaderSystem编排 glb-loader → BVH → world.registerTree → 附加 PickableMesh 脚本的完整流程
    • StatsSystemStats.js 面板,onBeforeRender(-100)/onAfterRender(1000) 精确夹住 render
    • PointerLogSystem响应式日志数组 + 订阅 engine.pointer.onMissed;Script 和 UI 共享同一份
  • Script (per-entity behavior)挂在 Entity 上,有自己的生命周期,entity 销毁时自动清理
    • PickableMeshonInit 注册 click/pointerenter/pointerleave,onDestroy 注销;通过 engine.getSystem 调用其他 system
  • Component (per-entity data)纯数据/工厂,用于标记实体状态,给 Query 做批量操作
    • BvhIndex标记 mesh entity 已建 BVH;world.query(BvhIndex) 可批量索引

为什么要这样拆?

1. UI 组件只剩编排

Playground.vue 通过 createPlaygroundEngine() 拿到 { engine, cameraSystem, transformSystem, modelSystem, ... } 几个句柄,模板里只调它们的公共方法——完全不 import three.js、不管 OrbitControls 怎么初始化、也不知道 BVH 怎么构建。

ts
async function onFileSelected(event: Event) {
  const file = (event.target as HTMLInputElement).files?.[0];
  if (!file) return;
  await modelSystem.loadFile(file);
  cameraSystem.focus(modelSystem.root);
}

2. System 之间通过 engine.getSystem 松耦合

TransformSystem 需要暂停相机:

ts
releaseCamera = engine.getSystem(CameraSystem)?.suspendControls() ?? null;

不需要 import 相机实例、不需要事件总线手动协调,也不需要 DI 容器——engine 本身就是 service locator。

3. Script 自动生命周期

PickableMesh 的 onInit 注册指针订阅,onDestroy 注销。当 ModelLoaderSystem.unload()world.removeEntity(rootEntity) 时,整棵子树的 Script 都会触发 onDestroy——不会有悬空的 pointer handler

ts
export const PickableMesh = createScript('PickableMesh', ({ engine, entity }) => {
  const handlers: (() => void)[] = [];
  return {
    onInit() {
      handlers.push(entity.onPointer('click', (e) => { /* ... */ }));
    },
    onDestroy() {
      handlers.forEach(off => off());
    },
  };
});

4. Component 是查询的锚点

给每个 mesh entity 附 BvhIndex 后,未来要批量操作(统计、重建、可视化 helper)时,一行查询搞定:

ts
const entities = world.query(BvhIndex).list();
entities.forEach(e => { /* ... */ });

两个关键陷阱的处理

拖拽 Gizmo 时点击穿透到模型

engine 的指针运行时用 pointerRuntime.flushFrame()每帧开头批处理 DOM 事件队列——如果在 mouseUp 里同步释放 pointer.suspend(),当前帧 flush 时 suspend 已经是 false,click 还是会被派发。

TransformSystem 里的解法:

ts
const releaseLocksDeferred = () => {
  const pendingPointer = releasePointer;
  releasePointer = null;
  requestAnimationFrame(() => pendingPointer?.()); // 下一帧再释放
};
controls.addEventListener('mouseUp', releaseLocksDeferred);

ViewportGizmo 需要独立 DOM 容器

直接 new ViewportGizmo(camera, renderer) 不传 container 的话,gizmo 没有地方挂自己的 canvas。CameraSystem 在 onReady 时动态创建一个 120×120 的 absolute div 加到 view.rootEl:

ts
gizmoContainer = document.createElement('div');
Object.assign(gizmoContainer.style, { position: 'absolute', right: '0', top: '0', width: '120px', height: '120px', pointerEvents: 'auto' });
view.rootEl!.appendChild(gizmoContainer);
gizmo = new ViewportGizmo(view.camera, view.renderer!, { container: gizmoContainer, /* ... */ });

源码参考

完整实现见仓库 packages/doc/src/engine/playground/_playground-demos/

基于 MIT 协议发布(内部使用)