从零到一:SuperSplat选择撤销系统的架构设计与实现原理
【免费下载链接】supersplat 3D Gaussian Splat Editor 项目地址: https://gitcode.com/gh_mirrors/su/supersplat
引言:为什么3D编辑器的撤销功能如此重要?
在3D Gaussian Splat编辑过程中,艺术家和开发者经常需要进行大量精细的选择和修改操作。一个健壮的选择撤销系统不仅能显著提升工作效率,还能避免因误操作导致的创作损失。SuperSplat作为领先的3D Gaussian Splat编辑器,其选择撤销功能采用了模块化设计和事件驱动架构,确保了操作的可追溯性和状态的一致性。本文将深入剖析这一系统的实现原理,包括核心数据结构、操作流程和关键技术点。
核心架构概览
SuperSplat的选择撤销系统基于命令模式(Command Pattern)和观察者模式(Observer Pattern)构建,主要由以下四个模块组成:
模块职责划分
- EditHistory: 负责维护编辑操作的历史记录,管理撤销/重做的状态流转
- EditOp: 定义编辑操作接口,具体实现如删除、变换等操作的正向/反向逻辑
- SelectionManager: 管理当前选择状态,提供选择查询和更新接口
- Events: 事件总线,实现跨模块通信,解耦操作触发与处理逻辑
核心数据结构解析
EditOp接口设计
interface EditOp {
name: string; // 操作名称,用于调试和状态显示
do(): void; // 执行操作
undo(): void; // 撤销操作
destroy(): void; // 清理资源
}
EditOp接口是整个撤销系统的灵魂,它抽象了所有可撤销操作的共性。每个具体操作(如删除选择、变换实体)都需实现此接口,确保撤销/重做的一致性。
历史记录管理
EditHistory类采用游标(cursor)机制管理操作序列:
class EditHistory {
history: EditOp[] = []; // 操作历史数组
cursor = 0; // 当前操作游标位置
add(editOp: EditOp) {
// 清除游标后的所有操作(支持非线性撤销)
while (this.cursor < this.history.length) {
this.history.pop().destroy();
}
this.history.push(editOp);
this.redo(); // 执行新操作并移动游标
}
undo() {
const editOp = this.history[--this.cursor];
editOp.undo(); // 调用具体操作的撤销逻辑
this.events.fire('edit.apply', editOp);
}
redo() {
const editOp = this.history[this.cursor++];
editOp.do(); // 调用具体操作的执行逻辑
this.events.fire('edit.apply', editOp);
}
}
这种设计支持"撤销后继续新操作"的场景,会自动清除游标后的操作记录,保持历史记录的线性特性。
选择系统的实现机制
选择状态管理
selection.ts中实现了选择状态的核心逻辑:
let selection: Element = null;
const initSelection = (events: Events, scene: Scene) => {
// 监听元素添加事件,自动选择新添加的splat
events.on('scene.elementAdded', (element: Element) => {
if (element.type === ElementType.splat) {
setTimeout(() => {
events.fire('selection', element);
});
}
});
// 监听元素移除事件,处理选中元素被删除的情况
events.on('scene.elementRemoved', (element: Element) => {
if (element === selection) {
const splats = scene.getElementsByType(ElementType.splat);
events.fire('selection', splats.length === 1 ? null : splats.find(v => v !== element));
}
});
// 核心选择变更事件处理
events.on('selection', (element: Element) => {
if (element !== selection && (!element || (element as Splat).visible)) {
selection = element;
events.fire('selection.changed', selection); // 通知选择变更
scene.forceRender = true;
}
});
};
选择系统通过事件总线解耦了选择状态与UI展示、编辑操作等模块。当选择变更时,通过selection.changed事件通知其他组件更新状态。
选择操作的类型
在editor.ts中定义了多种选择操作模式:
- 点选:通过鼠标点击选择单个splat
- 框选:通过矩形区域选择多个splat
- 球形选择:基于空间球体选择splat
- 平面选择:基于平面方程选择splat
- 掩码选择:通过图像掩码进行选择
这些选择模式通过统一的事件接口select.pred处理:
events.on('select.pred', (op, pred: (i: number) => boolean) => {
selectedSplats().forEach((splat) => {
const splatData = splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
processSelection(state, op, pred); // 根据谓词函数筛选splat
splat.updateState();
});
});
撤销功能的实现流程
以删除选择操作为例,解析完整的撤销流程:
1. 删除操作的触发
editor.ts中注册了删除操作的事件处理:
events.on('select.delete', () => {
selectedSplats().forEach((splat) => {
editHistory.add(new DeleteSelectionEditOp(splat)); // 创建操作并添加到历史
});
});
当用户执行删除操作时,会创建DeleteSelectionEditOp实例并添加到编辑历史。
2. DeleteSelectionEditOp的实现
edit-ops.ts中定义了删除操作的具体逻辑:
class DeleteSelectionEditOp {
name = 'deleteSelection';
splat: Splat;
indices: Uint32Array; // 保存选中splat的索引
constructor(splat: Splat) {
const splatData = splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
// 构建选中元素的索引数组
this.indices = buildIndex(splatData, (i) => !!(state[i] & State.selected));
this.splat = splat;
}
do() {
const state = this.splat.splatData.getProp('state') as Uint8Array;
// 标记选中元素为删除状态
for (let i = 0; i < this.indices.length; ++i) {
state[this.indices[i]] |= State.deleted;
}
this.splat.updateState(true);
}
undo() {
const state = this.splat.splatData.getProp('state') as Uint8Array;
// 恢复删除的元素
for (let i = 0; i < this.indices.length; ++i) {
state[this.indices[i]] &= ~State.deleted;
}
this.splat.updateState(true);
}
destroy() {
this.splat = null;
this.indices = null; // 释放资源
}
}
关键实现点:
- 构造函数中保存操作前的选中状态(indices数组)
- do()方法执行删除逻辑,通过位运算标记状态
- undo()方法恢复原始状态,同样使用位运算
- destroy()方法清理引用,避免内存泄漏
3. 撤销命令的触发与执行
EditHistory通过事件监听撤销命令:
events.on('edit.undo', () => {
if (this.canUndo()) {
this.undo(); // 调用撤销方法
}
});
当用户触发撤销快捷键或按钮时,事件系统会调用EditHistory的undo()方法,进而执行对应EditOp的undo()逻辑。
事件驱动架构的优势
SuperSplat的撤销系统充分利用了事件驱动架构的优势:
- 模块解耦:操作触发者(UI)与处理者(EditHistory)通过事件通信,互不依赖
- 可扩展性:新增编辑操作只需实现EditOp接口并触发对应事件
- 状态同步:通过事件通知UI更新撤销/重做按钮状态、历史记录列表等
- 测试友好:可独立测试每个事件处理逻辑,无需复杂的依赖注入
事件系统的核心实现(events.ts):
class Events extends EventHandler {
functions = new Map<string, Function>();
// 注册函数供其他模块调用
function(name: string, fn: Function) {
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);
}
}
性能优化策略
1. 状态压缩存储
选择状态使用位运算存储在Uint8Array中:
enum State {
selected = 1, // 0b0001
hidden = 2, // 0b0010
deleted = 4 // 0b0100
}
一个字节可存储3种状态,大幅减少内存占用,便于批量操作和序列化。
2. 索引数组复用
buildIndex函数创建选中元素的索引数组,避免重复遍历:
const buildIndex = (splatData: GSplatData, pred: (i: number) => boolean) => {
let numSplats = 0;
// 先计数符合条件的元素
for (let i = 0; i < splatData.numSplats; ++i) {
if (pred(i)) numSplats++;
}
const result = new Uint32Array(numSplats);
let idx = 0;
// 再填充索引
for (let i = 0; i < splatData.numSplats; ++i) {
if (pred(i)) {
result[idx++] = i;
}
}
return result;
};
这种两次遍历的方式比单次遍历+动态数组更高效,避免了数组扩容的性能开销。
3. 延迟渲染更新
splat.updateState(true)方法支持强制更新标志,避免频繁渲染:
splat.updateState(true); // 立即更新
// vs
splat.updateState(); // 延迟到下一帧更新
在批量操作时使用延迟更新,可显著提升性能。
实际应用场景分析
场景一:复杂选择的精确撤销
当用户进行多次选择操作后发现错误:
- 框选多个splat
- 球形选择添加部分splat
- 平面选择移除不需要的splat
- 执行删除操作
- 发现误删,执行撤销
系统会精确恢复到删除前的选择状态,包括所有添加/移除的操作痕迹。
场景二:非线性撤销流程
支持"撤销-新操作-再撤销"的非线性工作流:
操作序列: A → B → C → 撤销B → 撤销A → 新操作D
历史记录: [A, D] (B和C被自动清除)
这种灵活性极大提升了创作过程的自由度。
总结与未来展望
SuperSplat的选择撤销系统通过命令模式和事件驱动架构,实现了高效、可靠的编辑历史管理。核心优势包括:
- 模块化设计:EditOp接口抽象使新增操作变得简单
- 高效状态管理:位运算和索引数组优化性能
- 灵活的事件系统:解耦组件,便于扩展
- 完整的撤销能力:支持各种选择和编辑操作的撤销
未来可能的改进方向:
- 多级撤销粒度:支持按操作类型或时间范围撤销
- 选择性撤销:允许用户选择特定操作进行撤销
- 撤销预览:在执行撤销前预览效果
- 云同步历史:跨设备同步编辑历史
通过深入理解这一系统的实现原理,开发者不仅可以更好地使用SuperSplat进行3D创作,还能将这些设计模式应用到其他复杂编辑器的开发中。
附录:核心API速查表
| 类/接口 | 关键方法 | 作用 |
|---|---|---|
| EditHistory | add(editOp) | 添加操作到历史记录 |
| EditHistory | undo() | 撤销上一步操作 |
| EditHistory | redo() | 重做下一步操作 |
| EditOp | do() | 执行操作 |
| EditOp | undo() | 撤销操作 |
| Events | on(event, callback) | 注册事件监听 |
| Events | fire(event, data) | 触发事件 |
| Events | function(name, fn) | 注册可调用函数 |
| 事件名称 | 触发时机 | 数据参数 |
|---|---|---|
| selection | 选择状态变更 | 选中的元素 |
| edit.undo | 用户触发撤销 | 无 |
| edit.redo | 用户触发重做 | 无 |
| edit.apply | 操作被应用 | 操作实例 |
| select.delete | 用户删除选择 | 无 |
【免费下载链接】supersplat 3D Gaussian Splat Editor 项目地址: https://gitcode.com/gh_mirrors/su/supersplat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



