简介:ShadowEditor是一款基于WebGL技术的在线3D模型编辑器,内置数据后台,面向3D建模、游戏开发及虚拟现实场景,用户无需额外插件即可在浏览器中完成模型创建、编辑、预览和项目管理。资源为RAR压缩包,共7173个文件,总大小347.62MB,以JavaScript源码为主(5214个js),另有TypeScript、GLSL着色器、HTML页面、CSS样式以及Markdown文档和JSON配置,涵盖编辑器前端交互、三维渲染、数据持久化等完整链路。压缩包内还包含MongoDB数据文件与一系列部署配置,可支撑数据后台独立运行,便于研究模型存储、版本管理和多人协作机制。目前已吸引283人学习下载,适合具备一定前端基础、希望深入WebGL三维编辑或后台管理系统的开发者。源码开放了模型导入导出(OBJ、FBX、GLTF)、骨骼动画编辑等多类功能,可直接部署运行并按需二次开发,是理解浏览器端3D工具链的完整参考。
1. 为什么 WebGL 模型编辑器必须带数据后台
浏览器能跑 Three.js 之后,"网页里的三维编辑器"这类产品门槛一下低了很多:拖一个 Box 进来、转一下相机、改个材质颜色,前端半天就能拼出一个像样的 demo。可一旦场景里堆了几十个模型、上百张贴图,还要同时给美术、实施、运维三拨人用,场景数据存在哪就直接决定项目能不能交付。localStorage 只有几 MB,IndexedDB 换个浏览器就"失忆",导出单个 JSON 文件又没法多人协作,更别提版本回溯和权限控制。ShadowEditor 的解法,是让 WebGL 模型编辑器自带数据后台:场景树、模型文件、贴图、脚本组件全部由服务端统一管理,浏览器只做一个有状态的工作台,关掉页面数据不丢。这篇按"数据模型 → 部署落库 → 存取细节 → 可靠性验证"的顺序把整条链路铺开,适合正在做 3D 可视化、数字孪生平台,或者打算自建可视化编辑器的后端与全栈工程师。
2. ShadowEditor 的数据模型:场景树如何变成可持久化的后台记录
2.1 Three.js 场景图与 JSON 序列化的对应关系
ShadowEditor 的数据后台没有另造一套私有格式,而是直接复用 Three.js 的序列化约定。Three.js 里整个编辑现场就是一棵 Object3D 树:Scene 挂 Camera、Mesh、Group、Light,每个节点除了名字和 transform,还引用各自的几何体、材质和脚本。保存场景时,后台拿到的是这棵树的完整快照,而不是一堆零散的增删改操作。
{ "uuid": "3f9a-1c2d-4e5f", "type": "Mesh", "name": "产线传送带", "parent": "2c81-9a3b-7d6e", "transform": { "position": [12.5, 0.6, -3.2], "rotation": [0, 0.785, 0], "scale": [1, 1, 1] }, "material": { "type": "MeshStandardMaterial", "color": 16777215 }, "geometry": { "type": "BoxGeometry", "params": { "width": 8, "height": 0.8, "depth": 1.4 } } }这段 JSON 是一次保存动作里单个节点的最小单元,也是理解 ShadowEditor 数据后台的钥匙。几个字段的设计意图要讲清楚:uuid 是全局唯一标识,跨会话、跨用户都靠它保持引用稳定;parent 指向父节点的 uuid,后台在加载时靠它把扁平数组重建成树;transform 里的 position/rotation/scale 都是数组形态,绕开了 JSON 对 Vector3 这类类实例的原生序列化问题。material 和 geometry 既可以内联,也可以只存一个资源 ID 去引用素材表里的记录,后一种做法在多个场景复用一个材质时能省下大量冗余数据。反序列化方向也完全对称:后台把 JSON 交给前端,编辑器逐个节点 new 出对应的 Object3D 子类,再按 parent 字段挂接,整个过程不需要任何业务上的二次映射。这也是 ShadowEditor 相对自研编辑器最大的省心点——数据格式跟着 Three.js 走,升级 Three.js 版本时序列化结构的兼容性由上游维护。
2.2 全量快照保存:ShadowEditor 的落库策略
理解 ShadowEditor 的保存,只要记住一句话:保存是整棵场景树的全量覆盖写,不是增量事务。这个策略听上去笨,但在编辑器场景里非常合理——编辑操作千变万化,增量记录需要复杂的 diff 和对账逻辑,而全量快照天然保证"读出来的东西就是最后编辑的样子",不会有半更新状态。
// 保存场景:把编辑器当前状态全量提交给数据后台 async function saveScene(editor, sceneId) { const snapshot = editor.scene.toJSON(); // Three.js 内置序列化,输出整棵对象树 const payload = { id: sceneId, name: editor.scene.name, version: editor.version + 1, // 版本号递增,回滚和冲突判断都靠它 data: snapshot, updatedAt: new Date().toISOString() }; const res = await fetch(`/api/scene/${sceneId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error(`保存失败: HTTP ${res.status}`); return res.json(); }这里两个参数最值得留意。version 是保护数据的第一道闸:多人同时编辑时,后提交的人如果拿到的 version 比服务端旧,后台应该拒绝覆盖或者提示合并,否则就是经典的"最后写入者胜出",静默丢数据。updatedAt 看起来只是时间戳,但在列表页做"最近编辑"排序、以及定时清理过期草稿时都依赖它,插入数据时补上总比事后迁移省事。另外一个隐含代价要提前有数:场景越大,每次保存的 JSON 就越大,几十个模型带贴图引用的场景轻松超过 1MB;如果用户频繁点保存,后台 CPU 和磁盘 IO 的压力会明显上升。常见做法是前端做 3~5 秒的节流,用户连续操作只合并成一次保存请求。
2.3 命令队列、内存态与落库态的分界
ShadowEditor 前端把所有编辑动作——移动物体、删除节点、改材质颜色、添加脚本——都包装成 command 对象,推入一个 undo/redo 栈。命令执行时不仅更新场景树,还会通知渲染器重新绘制。理解这套机制的关键,是分清三个状态:内存态是当前编辑器的现场,命令栈只存在于内存里;快照态是最近一次点保存时写到后台的版本;落库态则是数据库里真实持久化的数据。三者之间有明显的时间差。很多用户报"我明明改了,刷新就没了",排查方向往往不是后台,而是前端压根没触发保存动作。所以自测的时候要养成习惯:改完任何东西,先看右上角是否出现未保存标记,确认保存请求发出后,再刷新页面验证。
2.4 数据后台需要接住的四类请求
| 请求类型 | 触发时机 | 失败时的表现 |
|---|---|---|
| 场景全量写 | 点保存 / 发布 | 刷新后最近改动全部丢失 |
| 素材上传 | 拖入模型、贴图、HDR | 场景引用悬空,加载时报 404 |
| 场景读取 | 打开编辑器工作台 | 页面空白或场景树为空 |
| 权限校验 | 多用户登录与协作 | 误覆盖他人场景或越权访问 |
这四类请求对应到后台就是四个能力域:场景的增删改查、文件的上传与静态服务、按 ID 加载文档、以及用户/会话级别的访问控制。其中素材上传最容易被人忽略——很多编辑器项目第一个线上事故,都是因为贴图传上去了,但上传目录没做持久化挂载,容器一重启资源全丢,场景里只剩一堆悬空的材质引用。从这个角度看,模型编辑器的数据后台本质上就是个"场景文档库 + 资源文件库"的组合,前者管结构化 JSON,后者管非结构化文件,两者通过 ID 互相引用。
3. 本地跑通 ShadowEditor:从安装到第一次把场景写进数据库
3.1 依赖清单与版本建议
ShadowEditor 的服务端是 Node.js 写的,数据存储默认走 MongoDB,较新的版本也支持切换到 SQLite,把部署成本压到单进程级别。这里给一张依赖清单,照着准备即可:
| 依赖 | 建议版本 | 用途 | 没装会怎样 |
|---|---|---|---|
| Node.js | 14 及以上 LTS | 数据服务、静态资源、构建脚本 | 启动命令直接报错 |
| MongoDB | 4.x 及以上 | 场景与素材元数据存储 | 无法持久化场景 |
| 浏览器 | Chrome / Edge 较新版本 | WebGL 渲染与编辑器 UI | 旧版本 WebGL 特性缺失 |
渲染发生在用户浏览器里,所以服务器不需要 GPU,一台 1 核 2G 的云主机跑开发环境绰绰有余。真正吃资源的是上传目录的磁盘空间,模型和贴图文件会随项目推进快速膨胀,部署前先把磁盘配额和备份策略想好。如果只是本地研究,优先用 SQLite 模式,省掉 MongoDB 的安装和守护进程,一套 Node 环境就能跑起来。
3.2 从 clone 到后台就绪的三条命令
git clone --depth=1 <ShadowEditor 仓库地址> cd ShadowEditor npm install npm run dev三条命令各有一个容易踩的细节。clone 时加--depth=1只拉最新提交,ShadowEditor 的历史提交里有大量示例资源,全量拉下来浪费时间也占磁盘;npm install装的是前后端全部依赖,网络不好时多试几次或者换镜像源;npm run dev会同时拉起数据服务和静态资源服务,启动成功后控制台会打印访问地址,常见默认端口是 5017,以你本机输出为准。
服务是否就绪,直接访问编辑器页面验证最靠谱:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5017/editor.html返回 200 说明静态资源服务正常。紧接着在浏览器里打开同一个地址,能看到默认场景和左侧的场景树,就说明前端到后端的链路已经通了。这一步排查的要点是区分故障层:页面都打不开是静态服务问题,页面开了但场景加载不出来才是数据接口或数据库的问题,别混在一起查。
3.3 保存一个场景并用数据库验证
编辑器界面操作流程:从素材面板拖入一个 Box 或任意模型,在属性面板改个名字,点工具栏的保存按钮。然后在命令行里连上数据库,看这条记录是否真的落了库:
mongosh use shadoweditor db.scenes.find().sort({ updatedAt: -1 }).limit(1).pretty()use shadoweditor里的库名是部署配置里指定的默认库,如果你改过配置,记得换成实际库名。看到返回的文档里 data 字段是一整棵场景树,并且 data.objects 的数组长度和你编辑器里拖入的对象数量一致,说明序列化闭环已经通了。最常见的失败表现有两种:记录完全不存在,说明保存请求根本没到数据库,去查前端网络请求是否报错;记录存在但 objects 为空,说明保存时机太早,拖入对象后没有等待渲染完成就点了保存。验证脚本里sort({ updatedAt: -1 })加上limit(1)是很实用的组合,避免在库里有历史场景时还要去翻最新一条。
3.4 部署前要先确认的三处配置
第一处是数据存储方式,mongodb 模式要填对连接串,sqlite 模式要确认数据文件的路径可写,写错位置会导致场景存了但换目录就找不到。第二处是上传根目录,模型、贴图、HDR 文件都落在这个目录下,开发环境用本地磁盘没问题,生产环境建议挂独立数据盘或对象存储,并确保目录权限对服务进程可写。第三处是静态资源与上传文件的前缀路径,一旦前置了 Nginx 反向代理,/api和/uploads两块的路由经常会被规则吞掉,表现为"页面正常但保存 405、图片全裂"。这三处确认完,本地跑通 ShadowEditor 这块才算真正闭环,可以进入数据后台的深入改造了。
4. 数据后台的存取细节:模型、素材与场景版本
4.1 场景记录的主表结构与版本回滚
场景在数据库里就是一条文档记录,字段设计直接决定后续查询和回溯的代价。常用的主表字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string / UUID | 主键,前端场景标识 |
| name | string | 场景名,列表页展示用 |
| version | int | 每次保存自增,冲突判断依据 |
| data | JSON | 完整场景树快照,体积最大的字段 |
| thumbnail | string | 封面图 URL,缩略图 |
| creatorId | string | 创建者,权限校验用 |
| createdAt | datetime | 创建时间 |
| updatedAt | datetime | 最后保存时间,列表排序用 |
| deleted | boolean | 软删除标记 |
这里有一个必须遵守的查询纪律:列表接口只返回元数据字段,千万不要把 data 整段查出来再循环。场景 JSON 动辄几百 KB 到几 MB,列表页一次查 50 条,光网络传输就能把页面拖死。常见做法是用数据库的投影查询,只取 id、name、updatedAt、thumbnail 这几个字段,进入编辑页时再按 ID 查完整 data。版本回滚的实现也很直接:保存时带过来的旧版 data 在覆盖前先拷一份到 history 集合,或者在同一文档里保留历史版本数组,回滚就是取指定 version 覆盖当前 data。deleted软删除标记比物理删稳妥,编辑器里"误删场景"是高发操作,物理删除等于把恢复路径也删了。
4.2 模型与贴图的落盘组织
模型资源上传后,后台不能把所有文件平铺在一个目录里,否则文件一多,目录遍历和备份都会变成灾难。我一般会按日期两级分目录:
upload/ 2025/ 04/ model/ 18/xxxx-8f3a.gltf 18/xxxx-8f3a.bin texture/ 18/xxxx-8f3a.png hdr/ 18/xxxx-8f3a.hdr按年月日分目录的收益有两个:一是单目录文件数可控,文件系统在目录内文件过万后性能明显下降;二是备份时可以按时间段增量打包,出问题时定点恢复某一天的数据。文件名用上传时的哈希或 UUID 前缀,避免中文名和空格带来的 URL 编码问题。导入 glTF 格式时有一个坑必须提醒:glTF 主文件往往伴随 .bin 二进制缓冲和外部贴图,是一整组文件,后台要把这个文件组整体保存下来,并维护主文件与伴随文件的相对路径关系。只存 .gltf 主文件,加载时模型会缺几何或贴图,控制台报一堆 404,排查起来非常隐蔽。至于高程数据这类体积大、结构特殊的资源,不建议塞进场景 JSON,常见做法是外链成 heightmap 或 terrain 资源文件存入 upload 目录,场景里只保留一个 URL 引用,加载时按需拉取。
4.3 数据后台接口的常见划分与参数
ShadowEditor 各版本的路由路径会有调整,但接口的划分逻辑是稳定的,这里给出一份常见划分作参照:
| 方法 | 路径 | 作用 | 关键参数 |
|---|---|---|---|
| GET | /api/scene/list | 场景列表,只返回元信息 | page, pageSize, keyword |
| POST | /api/scene | 新建场景 | name, data |
| PUT | /api/scene/:id | 覆盖保存 | version, data |
| GET | /api/scene/:id | 加载完整场景 | 无 |
| DELETE | /api/scene/:id | 软删除场景 | 无 |
| POST | /api/upload | 上传模型 / 贴图 / HDR | file, type |
| GET | /api/uploads/... | 读取已上传素材 | 相对路径 |
这里最重要的一个设计区分是 list 和 load 的职责分离:list 只给列表页用,返回的是轻量元信息;load 才返回完整 data。前端拿到场景 JSON 后直接反序列化重建场景树,不需要再做一层业务 DTO 转换。upload 接口的 type 参数用来区分模型、贴图、HDR 等资源类型,后台据此决定文件落到哪个子目录,也决定返回的资源 URL 前缀。逆向排查时还有个实用技巧:用 curl 直接打 load 接口,把返回的 JSON 存成文件,和编辑器里导出的场景对比,能快速定位是后台丢数据还是前端渲染问题。
4.4 为什么编辑器数据不能只放浏览器:从 IDBFS 写入失败说起
在 WebGL 生态里,浏览器端持久化一直是个说不清的痛。Unity 发布 WebGL 项目时会用 IDBFS 把文件系统模拟到 IndexedDB 上,"unity 发布 webgl 使用 idbfs 写入失败"是社区里反复出现的高频问题:隐私模式直接禁用 IndexedDB,写入静默失败;Safari 旧版本配额收紧,数据写到一半报错;多标签页同时打开一个页面,各自往同一块区域写,互相覆盖。这些问题在自研 Three.js 编辑器里一样存在,只是换了个表现形式。ShadowEditor 选择把数据后台放在服务端,本质上就是绕开浏览器存储的全部不确定性。浏览器只承担渲染和编辑,场景文档、模型文件、版本历史都跟着账号和服务走,清缓存、换电脑、换浏览器都不影响数据。对小程序、小游戏这类容器化 WebGL 场景,存储限制只会更严格,模板配置稍有不对,初始化就失败——这进一步印证了一件事:凡是"编辑器 + 数据"的产品形态,服务端持久化不是可选优化,而是底线设计。
5. 给 ShadowEditor 数据后台做一次可靠性体检
5.1 用并发压测脚本验证保存链路
数据后台最常见的问题不是功能缺失,而是多人同时保存时顶不住。先写一个十几行的压测脚本,把并发保存的失败率测出来:
// 并发保存压测:20 个场景同时写库,统计失败率 const baseUrl = 'http://127.0.0.1:5017'; async function saveScene(i) { const res = await fetch(`${baseUrl}/api/scene/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: `压测场景${i}`, data: { objects: [] } }) }); return res.ok; } (async () => { const results = await Promise.all( Array.from({ length: 20 }, (_, i) => saveScene(i)) ); console.log(`成功率: ${results.filter(Boolean).length}/20`); })();压测的观察点集中在两处:一是数据库的并发写入,MongoDB 在大量并发 upsert 时可能出现文档冲突或锁等待,报错通常是 duplicate key 或 write conflict;二是 Node 进程的连接池耗尽,表现为请求超时或 ECONNRESET。失败时先去翻后端日志,E11000 这类错误多半是 ID 生成策略在并发下重复,调整为主键自增或 UUID 即可。压测框架搭好之后,每次改完数据层代码都跑一遍,比上线后等用户报 bug 划算得多。
5.2 快照备份与跨环境迁移
注意:mongodump 备份的是数据库元数据,upload 目录里的大文件必须单独打包迁移,二者缺一不可。
场景数据的跨环境迁移,比如从开发机挪到测试服务器,标准动作是两条腿走路。数据库部分用 mongodump 导出场景集合:
mongodump --db shadoweditor --collection scenes --out ./backup资源文件部分直接把 upload 目录整体打包,连同目录结构一起拷贝到目标机器。恢复顺序有讲究:先恢复数据库,再恢复 upload 目录,最后启动服务。迁移完成后的验证不要只打开一个空场景就算通过,要挑一个引用模型数量超过 10 个的场景完整加载,打开浏览器控制台确认没有任何 404 资源请求。这个验证能同时覆盖数据库恢复、资源路径映射、静态服务配置三个环节。
5.3 一个 5 分钟可复现的端到端验证流程
最后分享一个我常用的稳定性自检流程,全部做完 5 分钟以内。第一步,清空浏览器站点数据,模拟用户换设备场景;第二步,打开编辑器加载一个含模型真实引用的场景,控制台无红色报错;第三步,移动一个物体并保存,等待节流窗口过后,直接查数据库确认 data 里该节点 transform 已更新;第四步,杀掉服务进程再启动,重复加载同一场景,确认改动仍在。第四步如果用 SQLite 模式,顺便验证数据文件路径权限——很多部署事故都出在服务进程对数据文件没有写权限,界面看着正常,一重启全回滚。这一套流程每次改完配置、升级版本、迁移环境后都跑一遍,数据后台的可靠性就有底了。
本文还有配套的精品资源,点击获取