第09章:源码架构、事件系统与开发环境搭建
从本章起进入开发篇。我们将搭建 SuperSplat 的本地开发环境,并系统剖析它的整体架构、事件驱动机制与构建流程,为下一章的二次开发打基础。
1. 搭建本地开发环境
按官方 README 的步骤即可。前置要求:安装 Node.js 20.19.0 或更高版本(package.json 中 engines.node 要求 >=20.19.0)。
# 1. 克隆仓库
git clone https://github.com/playcanvas/supersplat.git
cd supersplat
# 2. 安装依赖
npm install
# 3. 构建并启动本地开发服务器
npm run develop
npm run develop 实际执行(见 package.json):
"develop": "cross-env BUILD_TYPE=debug concurrently --kill-others \"npm run watch\" \"npm run serve\""
即同时启动 Rollup 监听构建(watch) 与 本地静态服务器(serve dist)。源代码变更会被自动重新构建,刷新浏览器即可看到效果。
启动后访问:http://localhost:3000
重要:调试时务必关闭缓存。SuperSplat 注册了 Service Worker(
src/sw.ts),开发时需在浏览器开发者工具中启用 “Update on reload” 与 “Bypass for network”(Chrome 的 Application → Service Workers),或清空缓存,否则可能看到旧版本。
其他脚本:
npm run build:生产构建(rollup -c);npm run watch:仅监听构建;npm run serve:仅启动静态服务器;npm run lint:ESLint 检查src。
2. 应用启动流程(main.ts)
src/main.ts 是应用入口,负责按顺序装配整个编辑器。其核心步骤大致是:
- 创建图形设备:用 PlayCanvas 的
createGraphicsDevice初始化 WebGL2/WebGPU; - 创建事件总线:
const events = new Events(); - 创建场景:
new Scene(events, ...); - 注册各模块事件:调用一系列
registerXxxEvents(events, ...),包括:registerEditorEvents(编辑器核心,editor.ts);registerSelectionEvents、registerRenderEvents、registerTimelineEvents、registerPublishEvents、registerDocEvents、registerCameraPosesEvents、registerTrackManagerEvents、registerTransformHandlerEvents等;
- 创建工具管理器并注册工具:
new ToolManager(events),注册BoxSelection、BrushSelection、RectSelection、LassoSelection、PolygonSelection、SphereSelection、FloodSelection、EyedropperSelection、MoveTool、RotateTool、ScaleTool、MeasureTool等; - 创建编辑历史:
new EditHistory(events); - 初始化快捷键:
new ShortcutManager(events); - 初始化国际化:
localizeInit(); - 构建 UI:
new EditorUI(events, ...); - 初始化文件处理、iframe API 等。
可见整个应用通过一个共享的 events 实例把彼此解耦的模块粘合起来——这是理解全部源码的钥匙。
3. 事件驱动架构(events.ts)
src/events.ts 定义的 Events 类继承自 PlayCanvas 的 EventHandler,并在其基础上扩展了函数注册/调用机制:
class Events extends EventHandler {
functions = new Map<string, FunctionCallback>();
// 注册一个可被查询的函数
function(name: string, fn: FunctionCallback) {
if (this.functions.has(name)) {
throw new Error(`error: function ${name} already exists`);
}
this.functions.set(name, fn);
}
// 调用已注册的函数并返回结果
invoke(name: string, ...args: any[]) {
const fn = this.functions.get(name);
if (!fn) {
console.log(`error: function not found '${name}'`);
return;
}
return fn(...args);
}
}
因此 SuperSplat 中存在两套通信方式,理解二者区别至关重要:
| 机制 | API | 语义 | 用途 |
|---|---|---|---|
| 事件(继承自 EventHandler) | events.on(name, cb) / events.fire(name, ...) |
广播,无返回值,多订阅者 | 通知“发生了某事”(如 scene.clear、timeline.frame) |
| 函数(扩展) | events.function(name, fn) / events.invoke(name, ...) |
查询,有返回值,单一提供者 | 获取状态/服务(如 scene.dirty、selection、tool.active) |
例如:
// 注册“当前选中模型”查询
const selected = events.invoke('selection') as Splat;
// 监听场景清空事件
events.on('scene.clear', () => { /* ... */ });
// 触发隐藏选区
events.fire('select.hide');
这种“事件 + 函数”的混合总线让模块之间零直接依赖:UI 只管 fire/invoke,具体逻辑由各 register*Events 提供,便于扩展与测试。
4. 场景与元素(scene.ts / element.ts)
src/scene.ts 的 Scene 基于 PlayCanvas 应用(PCApp,src/pc-app.ts),管理一组 Element(src/element.ts)。元素类型(ElementType)包括高斯模型(Splat)、相机(Camera)、网格(grid)、叠加层等。
Scene 的关键职责:
- 维护元素列表,提供添加/移除(移除时
fire('scene.elementRemoved', element)); - 驱动每帧更新与渲染;
- 高斯泼溅的深度排序:源码
specialSort按每个 meshInstance 包围盒 8 个角中沿相机视线方向最远的距离排序,保证半透明高斯的正确叠加顺序(自定义排序模式SORTMODE_CUSTOM)。
高斯模型封装在 src/splat.ts 的 Splat 类中,内部持有 PlayCanvas 的 GSplat 资源与每点状态(splat-state.ts)。
5. 编辑操作与历史(edit-ops.ts / edit-history.ts)
所有可撤销的修改都实现统一的 EditOp 接口:
interface EditOp {
name: string;
do(): void | Promise<void>;
undo(): void | Promise<void>;
destroy?(): void;
}
edit-ops.ts 中定义了大量操作:SelectAllOp、SelectNoneOp、SelectInvertOp、SelectOp、HideSelectionOp、UnhideAllOp、DeleteSelectionOp、ResetOp、AddSplatOp、MultiOp(组合多个操作)等。底层的 StateOp 通过位运算(SET/CLEAR/TOGGLE)批量修改高斯点状态位:
class StateOp {
private apply(op: BitOp) {
const { state } = this.splat;
switch (op) {
case BitOp.SET: state.setBits(this.ranges, this.mask); break;
case BitOp.CLEAR: state.clearBits(this.ranges, this.mask); break;
case BitOp.TOGGLE: state.toggleBits(this.ranges, this.mask); break;
}
}
}
EditHistory(edit-history.ts)维护 do/undo 栈,Ctrl+Z / Ctrl+Shift+Z 即调用栈中操作的 undo() / do()。新增任何编辑功能时,只需实现 EditOp 并交给 EditHistory,即可自动获得撤销/重做能力。
6. UI 层(ui/ 目录,基于 PCUI)
界面用 @playcanvas/pcui 组件库构建。src/ui/editor.ts 的 EditorUI 是 UI 骨架,组装菜单、工具栏、各类面板。每个面板(如 data-panel.ts、color-panel.ts、timeline-panel.ts):
- 用 PCUI 的
Container、Label、SliderInput、SelectInput、BooleanInput、ColorPicker等构建控件; - 通过
events.fire/events.invoke与业务逻辑通信,自身不直接持有业务对象。
例如颜色面板的滑块改变时,fire 一个事件,由 color-grade 逻辑响应——UI 与逻辑彻底分离。
7. 国际化(localization.ts)
src/ui/localization.ts 用 i18next 实现多语言,语言文件位于 static/locales/*.json:
i18next.use(Backend).use(LanguageDetector).init({
detection: { order: ['querystring', 'navigator', 'htmlTag'] },
backend: { loadPath: './static/locales/.json' },
supportedLngs: ['de', 'en', 'es', 'fr', 'ja', 'ko', 'pt-BR', 'ru', 'zh-CN'],
fallbackLng: 'en'
});
UI 中所有文案通过 localize('some.key') 获取。注意已内置简体中文 zh-CN。可用 URL 参数 ?lng=zh-CN 强制指定语言进行测试。
8. 构建流程(Rollup + SCSS + TS)
SuperSplat 用 Rollup(rollup.config.mjs)打包:
@rollup/plugin-typescript编译 TypeScript;@rollup/plugin-node-resolve解析依赖;rollup-plugin-scss编译 SCSS 样式(src/ui/scss/);@rollup/plugin-image、@rollup/plugin-json处理图片与 JSON;- 生产构建用
@rollup/plugin-terser压缩,@rollup/plugin-strip去除调试代码; BUILD_TYPE=debug环境变量切换调试/生产模式。
输出到 dist/,由 serve 提供静态服务。
9. 本章小结
本章搭建了开发环境并剖析了架构:
- 用 Node ≥20.19 +
npm install+npm run develop启动本地开发,注意关闭 Service Worker 缓存; main.ts用一个共享events总线装配所有模块;Events提供“事件(on/fire,广播)”与“函数(function/invoke,查询)”两套机制,实现模块解耦;Scene管理 Element,并对高斯做自定义深度排序;- 编辑操作统一实现
EditOp接口并由EditHistory管理撤销/重做; - UI 基于 PCUI,与逻辑通过事件解耦;国际化用 i18next;构建用 Rollup。
下一章我们动手二次开发:新增工具、自定义编辑操作、对接 iframe API、增加语言并集成到自有产品。