ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue3 + Three.js 打造 3D 场景编辑器:从架构到避坑全指南

2026/9/9 22:14:34 拓冰建站 浏览量
Vue3 + Three.js 打造 3D 场景编辑器:从架构到避坑全指南 简介基于 Vue3、Three.js 与 Vite 构建的 3D 场景编辑器面向具备一定前端基础、希望上手三维可视化开发的工程师与爱好者解决在 Web 端快速搭建、配置 3D 场景并复用导出的需求。编辑器支持场景导入/导出和自定义配置导出的场景文件可由 scene-view 解析器直接渲染当前为纯前端单机版能够完整呈现场景配置、模型加载与编辑器交互流程适合学习 Three.js 场景组织、组件化封装以及编辑器整体架构设计。资源包共 230 个文件以 102 个 JS 逻辑脚本、20 个 CSS 样式文件为核心另有 95 张 JPG 贴图/预览图、5 个 WASM 底层模块、2 个 PNG 图标及说明文档等压缩包整体约 12.1MB由于是打包后产物拿到后自行启动静态服务即可查看界面与交互效果。已有 2972 人学习下载对于想掌握三维场景编辑器开发思路、场景序列化方案及 scene-view 集成方式的开发者是一份直观的参考实现。 做后台管理系统久了你迟早会在需求列表里看到一条让人头皮发麻的“给场景加个 3D 编辑器客户要能自己拖模型、改参数。”这个 vue3 版本的 Three.js 3D 场景编辑器就是我当时接的活。做完之后我把整套方案沉淀了下来场景怎么组织、选中交互怎么做、属性面板怎么联动、序列化怎么保证模型不丢以及 vue3 里集成 Three.js 最容易踩的响应式坑。这篇文章适合正好在看 vue3 Three.js 可视化方案、想搭同类后台的朋友按我的路线可以直接复刻出一个能用的最小版本。1. 项目整体设计想清楚编辑器边界再动手1.1 编辑器定位不是引擎是可视化配置工具我反复提醒自己别上来就写代码。绝大多数团队要的不是一个 Three.js 官方 Editor 的复刻品而是一个能嵌入后台业务的可视化配置工具。目标用户可能是运营、售前也可能是内部实施人员他们要能添加物体、调整位置、改颜色最后把场景保存下来给展示端加载。功能范围我一开始就控制在场景可视化渲染场景、自由视角、辅助网格物体管理新增、删除、复制、层级调整、重命名变换操作移动、旋转、缩放属性编辑几何体参数、材质颜色与透明度、光照保存与加载把场景序列化为 JSON能完整还原导出与预览给独立展示端提供可加载的资源包。这个范围控制非常重要。一上来就想做撤销重做、动画时间轴、材质编辑器项目大概率会烂尾。我的策略是先做出“新增物体-修改属性-保存-加载”的闭环再考虑扩展。设计原则也只有三条数据驱动、渲染与 UI 解耦、一切可持久化。所谓数据驱动是指操作本质上在修改一份场景描述数据Three.js 里的 Scene、Mesh 只是这份数据在渲染层的投影。这样保存、加载、多端同步都会简单很多。1.2 技术选型为什么是 vue3 Three.js后台项目本来就是 vue 技术栈所以 UI 层选 vue3 顺理成章。Three.js 在 WebGL 渲染层面足够成熟相机控件、射线拾取、对象变换这些编辑器核心诉求都有现成实现不需要自己造轮子。为什么不直接改 Three.js 官方 Editor官方 Editor 是原生 JS 项目对象模型、事件机制和业务后台完全是两套体系强行嵌进 vue3 项目后续维护成本会非常高。要支持业务自定义字段、权限控制、数据联动基本等于重写。至于 vue2 和 vue3 的区别在这个项目里体现得非常明显。vue2 里做全局复用主要靠 mixin多个编辑器模块混在一起之后非常难排查vue3 的组合式函数可以把渲染器初始化、场景管理、动画循环拆成独立 hook互不污染。还有响应式vue3 提供了 shallowRef 和 markRaw可以精确控制哪些对象进响应式系统这对 Three.js 这种大量复杂对象的场景来说太关键了。下面这张表是我实际切换时的感受维度vue2 Three.jsvue3 Three.js逻辑复用mixin命名冲突多组合式函数按需组装对象响应式容易整个对象被 ObserveshallowRef/markRaw 按需控制生命周期beforeDestroy 手工清理onBeforeUnmount 内聚清理类型推导一般配合 TS 更舒服1.3 模块拆分与数据流编辑器我拆成了五个模块画布、场景树、属性面板、工具栏、资源库。画布模块负责渲染器、相机、变换控件和拾取场景树模块负责展示 scene 的层级属性面板负责展示和修改选中对象工具栏负责新增、删除、保存、加载等动作资源库管理可拖入场景的模型与预设。模块之间不直接调用对方内部状态统一走一个轻量事件总线我用的是 mitt也可以用 vue3 的 provide/inject 自己封装一个。比如属性面板修改了颜色不直接改 Mesh 的材质而是发出 update-object 事件由场景模块去执行具体更新再广播 scene-changed 让场景树刷新。这样做的好处是将来接入撤销重做时只需要在命令层加一层记录不用改各个面板。2. 核心功能模块拆解2.1 场景与场景树用 uuid 贯穿对象生命周期Three.js 里所有 Object3D 都有自带的 uuid 字段我在编辑器里把它当作节点唯一 ID 使用不额外生成。之所以不用数组索引是因为删除、排序、插入子节点后索引一定会变用 uuid 才能保证保存和加载前后对象身份一致。场景树的数据来源其实是 scene.traverse()。每次增删节点后重新遍历场景生成一棵普通的 JS 树再用 vue3 渲染成嵌套列表。注意这里我刻意没有用 reactive 去嵌套包装每个节点而是用一个 version 计数器的自增来触发树刷新避免深度响应式开销。场景树上的交互主要有点击选节点、右键弹出操作菜单复制、删除、重命名、添加到分组。新增物体时默认挂到当前选中节点下如果没有选中就挂到场景根节点。这一条交互规则很简单但实际用起来非常顺手。2.2 选中与操作射线拾取、高亮、变换手柄画布上的点击选中核心是 Raycasterfunction onCanvasClick(event) { const rect renderer.domElement.getBoundingClientRect(); const mouse new THREE.Vector2(); mouse.x ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y -((event.clientY - rect.top) / rect.height) * 2 1; raycaster.setFromCamera(mouse, camera); const hits raycaster.intersectObjects(scene.children, true); if (hits.length 0) { selectObject(hits[0].object); } }有几个细节一定要处理。一是拖拽相机时也会触发 click需要在 mousedown 时记录坐标mouseup 时判断位移是否超过阈值超过就当拖拽处理不派发选中。二是 TransformControls 的手柄也是 Object3D如果不排除点击手柄也可能触发射线选中体验很怪我会在拾取前把 controls.getHelper() 从目标列表里临时剔除。三是 Mesh、Group、Light 都能选中但辅助网格和坐标轴不能选也要过滤。选中后的高亮我用的方案是给物体记录原始材质然后设置 emissive 或边缘发光。最简单有效的是临时换一个高亮材质但要注意恢复时把原始材质完整存下来否则撤选后材质会丢。变换操作直接用 TransformControls顶部做模式切换位置、旋转、缩放。它和 OrbitControls 必须互斥否则鼠标操作会打架。处理方式是监听 transformControls 的 dragging-changed 事件拖拽时把 OrbitControls 的 enabled 关掉transformControls.addEventListener(dragging-changed, (event) { orbitControls.enabled !event.value; });2.3 属性面板Three 对象与表单的双向同步属性面板是编辑器里最容易写得乱的地方因为 Three.js 的对象属性本身不带响应式能力。我的做法是选中物体时把它当前的位置、旋转、缩放、颜色、可见性等字段拷贝到一个普通对象中喂给表单。表单每次修改直接更新 Three 对象。如果表单只是输入框事件很好处理但 TransformControls 拖拽时物体属性是持续变化的面板也要实时反映。这时监听 transformControls 的 objectChange 事件再手动把最新值写回表单数据。这里不要用 watch 监听 three 对象本身它永远触发不了你想要的回调。组件通信方面也有一个小经验属性面板这类 UI 组件需要拿到当前选中的对象与其每个组件都自己去 scene 里找不如由场景模块统一维护一个 currentSelected 对象用 shallowRef 对外暴露属性面板通过 inject 拿引用。这个编辑器里的元素交互很密集建议所有跨组件状态都收敛到一个 editor context别散落各处。如果某些形态需要依赖现有状态派生用 computed 维护派生值就行不要直接改 Three 对象。3. 编辑器核心流程的实操实现3.1 工程初始化vite vue3 three创建项目没什么新鲜事命令行一下就好。需要注意的反而是一些小配置。比如给 配置别名把组件目录、组合式函数目录都规划好。初始化渲染器时有几项是必须写的antialias 打开抗锯齿setPixelRatio 限制在 window.devicePixelRatio防止高分屏模糊但也不要让像素比无限大导致性能崩掉。组件里我一般用 shallowRef 持有 renderer、scene、camera因为这三个对象只会在编辑器启动时创建一次后续只是读取和操作属性不需要深度响应式。用 markRaw 标记后即使不小心丢进 reactive 容器Vue 也会跳过对它的嵌套代理是个很理想的保险。3.2 渲染循环与组件生命周期渲染循环我用 requestAnimationFrame 包一层并在循环里调用 OrbitControls.update()以及后续动画组件需要的更新。这个循环在编辑器组件挂载时启动卸载时一定要取消并释放 WebGL 资源onBeforeUnmount(() { cancelAnimationFrame(rafId); disposeScene(scene); renderer.dispose(); });disposeScene 要遍历场景里所有对象调用 geometry.dispose()、material.dispose()、texture.dispose()。很多人只调 renderer.dispose()结果切页面再回来就报错或者 GPU 显存一直涨。这个在后台系统里尤其致命因为路由切换是非常常见的操作。3.3 场景序列化保存与加载的完整链路Three.js 自带了 scene.toJSON()会把场景结构、几何体、材质、光源参数都导成 JSON。加载时用 ObjectLoader 或者 scene.fromJSON() 恢复。但要注意几点。第一自定义业务字段要放进 object.userData。Three.js 的 toJSON 默认会保留 userData这是我们把设备 ID、告警状态、业务类型挂上去的最佳位置。第二函数不能序列化。不要在 userData 里放事件回调正确做法是存一个 type 字符串加载后根据 type 统一注册行为。第三纹理、HDR、模型这类资源不应该全部塞进 JSON。我实际项目里维护了一个资源库 manifestJSON 里只存资源 id 或 url加载时再从资源库映射。保存按钮做的事很简单读取当前场景 toJSON加上项目版本号调用后台接口存库。加载则是反方向拉 JSONObjectLoader.parse替换当前场景的 children再刷场景树。3.4 资源库与预设模板让编辑器真正好用纯手动添加立方体、球体、模型对于一个给业务人员用的工具还是不够直观。我把常用模型和预设场景做成了资源库拖拽或者点击就能插入。这个资源库不光放模型。我当时为了快速演示效果做了一个简单的火箭发射动画预设用锥体当箭体粒子系统模拟尾焰点击发射后 position.y 随时间快速抬升粒子的透明度、大小也跟着变化。整个预设作为一个 Group 插入场景并挂到资源库。这样演示的时候非技术人员也能快速理解编辑器的能力边界还能在这个预设场景上继续改材质、调位置接入真实业务数据后替换模型内部的业务变量就行。类似的预设还可以做机器人工作台演示、厂区设备摆放、产品展示台等套路都一样预设场景封装成一个函数返回 Group 实例并约定好 userData 里每个节点的业务含义。资源库的价值不只是省时间它变相给用户提供了一套可参考的场景模板能显著降低编辑器上手的门槛。3.5 导出与预览闭环交给业务展示端编辑器里的场景最终要给别人看导出环节就必须做。我提供两种方式导出 JSON 资源包展示端用相同的 loader 逻辑解析导出 PNG 截图用于列表页封面或给客户快速确认效果。导出 PNG 需要 renderer 的 preserveDrawingBuffer 设置为 true或者导出前渲染一帧再取 toDataURL。这里有个小坑preserveDrawingBuffer 打开后会在渲染性能上有一些损失尤其在连续操作时很明显。所以我只给导出相关页面短暂开启正常编辑状态保持默认关闭。4. 常见问题排查与避坑记录4.1 高频问题速查表实际开发中遇到频率比较高的问题我整理成了表格现象原因解决方案保存后再加载材质全部变黑自定义材质参数没有写入 JSON统一走 toJSON加载后按 type 重设材质参数拖拽相机时误选中物体没区分 click 和 dragmousedown/mouseup 坐标差超过阈值就忽略 click点击 TransformControls 手柄会选中后面的物体手柄对象参与了射线拾取拾取前排除 controls.getHelper()OrbitControls 和 TransformControls 抢鼠标两个控件同时响应监听 dragging-changed互相禁用vue3 版性能明显比 vue2 还卡Three 对象被响应式 Proxy 包裹改用 shallowRef markRaw非必要不进响应式切换页面后 WebGL 报错或内存上涨渲染器、几何体、材质没有释放onBeforeUnmount 里 dispose 全部资源4.2 三个最容易让项目翻车的细节第一个是响应式。Three.js 的对象层级深、数量大包进 reactive 不仅会有 Proxy 开销还会遇到“这个属性改了但场景不刷新”的困惑。我最终把所有 Three.js 对象都 markRaw 了需要通知视图刷新时人为发出事件而不是依赖 Vue 的依赖追踪。刚开始可能会觉得麻烦但习惯了之后编辑器状态会非常可控。第二个是资源释放。WebGLRenderer.dispose() 只释放渲染器自身几何体、材质、纹理的内存要手动处理。我封装了 disposeScene 工具遍历场景时先递归 children 再释放如果场景里有外部加载的 GLTF 模型还要注意默认材质和贴图的释放顺序不然下次加载同样的模型GPU 显存会肉眼可见地往上涨。第三个是版本兼容。Three.js 的 API 在 r125 之后变动很大网上很多老教程写的代码在新版本里根本跑不起来。比较典型的是 Geometry 已经被移除全部要用 BufferGeometry还有 ObjectLoader 的 parse 方式也有变化。建议直接锁定一个稳定版本不要贸然跟着最新发版升级等核心功能稳定后再评估。4.3 性能优化与后续扩展编辑器场景里的物体数量一般不会特别夸张但修改属性时仍要注意不要每帧同步 UI。TransformControls 拖拽过程中 objectChange 事件频率很高表单响应式的频繁更新会带来额外开销。我的做法是渲染循环里正常重绘UI 同步打一个节流大概 60ms 更新一次体感上不会有延迟CPU 占用却明显下降。后续扩展方向上撤销重做非常推荐前提是设计初期就走统一事件改对象的路径否则后面根本无从下手。多选、编组、模型拖拽导入、材质编辑器、动画时间轴也都是编辑器常用的增强项。现在 webgl/three.js 相关的可视化岗位需求明显比前几年多后台管理系统里的 3D 场景配置、数字孪生、大屏展示基本都是类似这套技术栈做成一次后面复用的机会很多。最后再分享一点个人经验。做完这个编辑器我最深的体会是Three.js API 反而是最简单的部分真正决定项目能不能持续迭代的是数据结构和生命周期管理。别把 Three.js 对象包进 vue3 的响应式系统里别让 UI 组件直接操作 scene先把 JSON 存取链路跑通再谈炫酷功能。如果你也准备动手我建议第一个版本只做“新增立方体、改颜色、保存、加载”这四步一旦这四步顺了后面的模型导入、动画、业务联动都只是往上叠积木。这个项目我还在继续迭代下一步计划把撤销重做和 GLTF 拖拽导入补上等稳定之后再单独写一篇分享。本文还有配套的精品资源点击获取