可视化编辑器
目的与范围
可视化编辑器是一个基于浏览器的 3D 场景创建和操作工具,为构建 Three.js 场景提供完整的图形界面。它允许用户无需编写代码即可创建、导入、编辑和导出 3D 内容,具有实时渲染、可视化属性编辑和全面的撤销/重做功能。
有关展示 Three.js 功能的示例系统的信息,请参阅 示例浏览器。有关渲染架构详情,请参阅 WebGL 渲染管线。
架构概览
编辑器遵循中心辐射架构,Editor 类充当中央状态管理器,通过基于信号的事件系统协调 UI 组件。UI 组件(Viewport、Sidebar、Menubar、Toolbar)与 Editor 实例交互,但不直接相互通信。
编辑器核心类
Editor 类管理场景状态、对象层次结构,并通过基于信号的发布-订阅系统协调所有编辑器子系统。
信号系统
Editor 类在其构造函数中初始化 40+ 个信号 editor/js/Editor.js19-96。这些信号实现子系统间的解耦通信。例如,当对象被修改时,signals.objectChanged.dispatch(object) 通知所有注册的监听器,无需紧密耦合。
状态管理
| 属性 | 类型 | 用途 |
|---|---|---|
| scene | THREE.Scene | 主场景图 |
| sceneHelpers | THREE.Scene | 辅助对象(网格、光源辅助器) |
| camera | THREE.PerspectiveCamera | 默认编辑器相机 |
| viewportCamera | THREE.Camera | 活动视口相机 |
| selected | Object3D | 当前选中对象 |
| geometries | Object | 几何体 UUID → 几何体映射 |
| materials | Object | 材质 UUID → 材质映射 |
| textures | Object | 纹理 UUID → 纹理映射 |
| scripts | Object | 对象 UUID → 脚本数组映射 |
| helpers | Object | 对象 ID → 辅助器映射 |
| cameras | Object | 相机 UUID → 相机映射 |
| materialsRefCounter | Map | 跟踪材质使用计数 |
Editor 维护所有场景资源的字典,按 UUID 索引,支持高效查找和引用跟踪。materialsRefCounter 跟踪每个材质的对象使用数量,防止过早释放 editor/js/Editor.js123
JSON 序列化
toJSON() 方法 editor/js/Editor.js703-741 序列化整个编辑器状态,包括场景层次结构、脚本、历史和项目配置。fromJSON() 方法 editor/js/Editor.js668-701 使用 THREE.ObjectLoader 重建场景,保留 UUID 和关系。
UI 组件层次结构
编辑器界面由五个主要 UI 组件组成,围绕中央视口排列。
视口组件
Viewport 类管理 3D 渲染表面和对象操作控件。
Viewport 组件管理三个渲染通道:
- 主场景渲染 editor/js/Viewport.js891
- 网格辅助器渲染(如果可见)editor/js/Viewport.js896
- 场景辅助器渲染(光源、相机、骨骼)editor/js/Viewport.js897
当对象被操作时,TransformControls 发出事件 editor/js/Viewport.js78-144,视口创建相应的命令对象以实现撤销/重做功能。
视口着色模式
| 模式 | 实现 | 用途 |
|---|---|---|
| solid | 默认渲染 | 标准材质显示 |
| normals | scene.overrideMaterial = MeshNormalMaterial | 可视化表面法线 |
| wireframe | scene.overrideMaterial = MeshBasicMaterial({wireframe: true}) | 显示网格拓扑 |
| realistic | ViewportPathtracer + three-gpu-pathtracer | 物理精确渲染 |
来源: editor/js/Viewport.js678-704
侧边栏组件
Sidebar 使用带四个主要标签页的标签面板结构。
UIOutliner 组件 editor/js/Sidebar.Scene.js132 将场景层次结构显示为可折叠树,支持拖放重新排序。每个节点显示类型图标和相关资源(几何体、材质、脚本)editor/js/Sidebar.Scene.js100-128
菜单栏组件
MenubarFile 组件使用导出器模块的动态导入提供导出功能 editor/js/Menubar.File.js250-445,在保持初始包体积小的同时支持多种导出格式。
撤销/重做的命令模式
编辑器通过 History 类和命令对象实现命令模式,启用完整的撤销/重做功能。
命令类
| 命令类 | 用途 | 关键方法 |
|---|---|---|
| AddObjectCommand | 向场景添加对象 | execute(): 添加对象, undo(): 移除对象 |
| RemoveObjectCommand | 从场景移除对象 | AddObjectCommand 的逆操作 |
| SetPositionCommand | 更改对象位置 | 存储旧/新位置 |
| SetRotationCommand | 更改对象旋转 | 存储旧/新旋转 |
| SetScaleCommand | 更改对象缩放 | 存储旧/新缩放 |
| SetGeometryCommand | 替换对象几何体 | 交换几何体引用 |
| SetMaterialCommand | 替换对象材质 | 交换材质引用 |
| SetValueCommand | 更改任意属性 | 通用属性设置器 |
| MultiCmdsCommand | 执行多个命令 | 复合命令模式 |
命令可以通过 update() 方法与先前命令合并 editor/js/Command.js27-31,防止连续值变化(例如拖动滑块)导致历史记录泛滥。
历史持久化
当配置中启用 settings/history editor/js/Config.js24 时,历史记录会与项目一起序列化并在会话间持久化 editor/js/History.js80-150。这允许用户在关闭并重新打开编辑器后仍能撤销更改。
文件加载系统
Loader 类通过动态模块加载支持导入 30+ 文件格式。
动态导入策略
加载器使用动态 import() 按需加载格式特定的解析器 editor/js/Loader.js89-959,减少初始包体积:
// 示例:GLTF 加载
case 'glb':
reader.addEventListener('load', async function(event) {
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
const loader = await createGLTFLoader();
loader.parse(contents, '', function(result) {
const scene = result.scene;
scene.animations.push(...result.animations);
editor.execute(new AddObjectCommand(editor, scene));
});
});GLTF 加载器配置
createGLTFLoader() 函数 editor/js/Loader.js954-976 配置 GLTFLoader,包含:
DRACOLoader用于几何体解压缩 editor/js/Loader.js961-962KTX2Loader用于纹理转码 editor/js/Loader.js964-965MeshoptDecoder用于 meshopt 压缩 editor/js/Loader.js972
KTX2 加载器通过 signals.rendererDetectKTX2Support 与渲染器能力同步 editor/js/Loader.js967
ZIP 档案支持
加载器可以提取和处理 ZIP 档案 editor/js/Loader.js747-760,自动检测 Poly 资产(model.obj + materials.mtl)或档案内的单个文件 editor/js/Loader.js842-951
存储与持久化
编辑器为配置和项目数据实现两层存储系统。
自动保存实现
自动保存机制监听 10+ 个信号 editor/index.html163-173,并在 1000ms 无活动后触发防抖保存 editor/index.html145-157。防抖防止快速更改期间的过度写入:
let timeout;
function saveState() {
if (editor.config.getKey('autosave') === false) return;
clearTimeout(timeout);
timeout = setTimeout(function() {
editor.signals.savingStarted.dispatch();
timeout = setTimeout(function() {
editor.storage.set(editor.toJSON());
editor.signals.savingFinished.dispatch();
}, 100);
}, 1000);
}IndexedDB 模式
Storage 类 editor/js/Storage.js1-92 使用带单个对象存储的 IndexedDB:
| 数据库 | 对象存储 | 键 | 值 |
|---|---|---|---|
| threejs-editor | states | 0 (固定) | 完整项目 JSON |
服务工作者与离线支持
编辑器注册服务工作者以实现离线功能和跨域隔离。
服务工作者实现网络优先缓存策略 editor/sw.js284-323,该策略:
- 尝试从网络获取
- 为编辑器页面注入 COEP/COOP 头 editor/sw.js290-297
- 成功获取时更新缓存
- 网络失败时回退到缓存版本
这使 Draco、Rhino3dm 和 Basis Universal WASM 解码器能够使用 SharedArrayBuffer 获得更好性能。
国际化
Strings 类为 UI 文本提供多语言支持。
字符串字典使用分层键(例如 menubar/file/open、sidebar/object/position)editor/js/Strings.js417-571。语言从 navigator.language 自动检测,但可在设置中更改 editor/js/Config.js5-7