简介:一份面向Web前端开发者的Vue3与Three.js 3D智慧园区源码资源,适合需要学习三维可视化、智慧园区数字孪生或相关毕业设计/项目实战的人群。资源完整呈现了园区场景搭建、模型加载、快递车与司机视角切换、自动巡视等功能模块,可作为二次开发与功能扩展的参考底座。包体共142个文件、约193.42MB,其中38个js与5个vue为工程逻辑与组件源码,15个glb与7个gltf为园区3D模型,34个png和22个jpg为贴图纹理,另有css、glsl、wasm等渲染与样式资源,目录结构覆盖场景、模型、样式与辅助脚本。目前已有5412人学习下载。通过阅读源码和模型组织结构,可以掌握Three.js场景管理、Vue3组合式API集成、多视角控制与巡游路径设计等关键实现方法,对智慧园区、智慧楼宇类项目开发有直接帮助。 做智慧园区这块,最怕的就是项目做出来像个“动画片”——场景很炫,但业务用不起来。我这个基于 Vue3 和 Three.js 的 3D 智慧园区项目,核心目标就三个:把园区全貌直观展示出来,把设备、车辆、安防这些实时状态映射到三维空间里,让运维人员能点、能查、能看到数据变化。这篇文章把整个项目从选型、架构到落地的关键过程和踩坑经验完整捋一遍,适合正打算做园区、工厂或大屏可视化项目的同学参考。
1. 为什么是 Vue3 + Three.js:智慧园区项目最先要过的选型关
1.1 项目需求倒推出来的技术栈
接到需求的时候,我没有直接开写,而是先拆了一遍“智慧园区”到底包含什么。说白了无非三件事:园区整体布局要能一眼看懂;楼栋、摄像头、门禁、车位的状态要可视化;数据发生变化时,3D 场景里要有实时反馈。
按这个需求推理下来,技术选型其实很收敛。渲染层用 Three.js 还是原生 WebGL 或者 Babylon.js?业务层用 Vue2 还是 Vue3?我最终的组合是 Vue3 加 Three.js。原因很直接:Three.js 是 WebGL 生态里资料最多、案例最丰富的库,遇到问题搜一下基本都有答案。Vue3 则是因为项目里有大量的侧边栏、数据表格、图表面板,这些 UI 交互必须靠一个成熟的声明式框架来承载。两者搭配,能避免“用 Three.js 手写一堆 UI 组件”这种非常反模式的开发方式。
1.2 Vue3 相比 Vue2 在 3D 项目里的实际优势
很多文章讲 Vue3 必提 Composition API,但说实话,如果只是做传统后台管理系统,Composition API 的优势不一定那么明显。可放到 3D 智慧园区这种“渲染逻辑、业务逻辑、状态逻辑”混在一起的项目里,差别就大了。Three.js 的场景初始化、模型加载、帧循环,这些都属于副作用逻辑,用 Composition API 写进 setup 里,配合 ref 和 reactive 管理状态,代码组织的清晰程度比 Vue2 的 data/methods 高很多。
另一个容易被忽略的点是生命周期。Three.js 非常依赖对场景销毁时机的控制,Vue3 的onBeforeUnmount和onUnmounted语义很明确,配合组合式函数(比如封装一个useThreeScene),能把 Three.js 资源的创建和销毁收敛在同一个模块里。后面做内存优化时,这条帮了大忙。
第三个优势是配套生态。Vite 对 Vue3 的原生支持让开发体验好了不止一个档次,热更新非常快,Three.js 模块又可以按需引入,配合 Vite 的依赖预构建和 tree-shaking,首屏体积能压下来不少。
1.3 为什么不选原生 WebGL 或 Babylon.js
原生 WebGL 就不展开说了,智慧园区这种项目,光是园区里的建筑、地面、道路、设备点位、粒子特效,用原生 WebGL 手写 shader 和场景管理,开发周期至少翻三倍,完全不划算。
Babylon.js 其实在场景编辑器和物理引擎方面很强,但在国内能搜到的智慧园区案例、文档和社区讨论,比 Three.js 少一大截。团队招人、交流、排查问题的成本都会更高。两者放在一起比,Three.js 在智慧园区这个垂直场景里的性价比最高。
| 对比项 | Three.js | Babylon.js | 原生 WebGL |
|---|---|---|---|
| 学习成本 | 较低 | 中等 | 很高 |
| 智慧园区案例数量 | 多 | 少 | 极少 |
| 与 Vue3 结合的资料 | 充足 | 一般 | 需要自己封装 |
| 开发效率 | 高 | 较高 | 低 |
| 社区活跃度 | 高 | 中等 | 一般 |
最终敲定 Vue3 + Three.js,等于把项目定位成“快速交付、持续迭代”,后面的实现过程也验证了这个选择是对的。
2. 项目架构:把 Three.js 场景装进 Vue3 组件的正确姿势
2.1 目录结构先定好,后面才不会乱
Three.js 场景本质上是独立于 UI 的一套渲染世界,如果代码散落在各个 Vue 组件里,项目后期必然乱成一锅粥。我用的目录结构大概是这样:
src/ ├── views/ │ └── campus/ │ ├── CampusScene.vue // 3D 场景容器组件 │ └── CampusPanel.vue // 业务侧边栏/信息面板 ├── three/ │ ├── core/ │ │ ├── sceneManager.js // 场景、相机、渲染器生命周期 │ │ └── raycaster.js // 射线拾取封装 │ ├── objects/ │ │ ├── building.js // 楼栋加载与高亮逻辑 │ │ ├── device.js // 设备点位管理 │ │ └── particleSystem.js // 粒子/动态特效 │ ├── animation/ │ │ └── orbitController.js // 相机控制与巡航动画 │ └── data/ │ └── dataBinding.js // 业务数据到 3D 对象的映射CampusScene.vue只负责提供一个 canvas 容器和事件入口,CampusPanel.vue只负责展示业务数据,两边通过封装好的组合式函数桥接。这样做的好处非常明显:排查渲染问题的时候不用翻 Vue 组件,排查业务问题的时候也不用进 three 模块。
2.2 渲染层和 Vue 组件的边界划在哪里
有一个关键决策:Three.js 的 scene、renderer、camera 这些对象,到底放不放 Vue 的 reactive 数据里?我的答案是——不放。
Three.js 内部有大量循环引用和庞大的对象结构,放进 Vue 的响应式系统会被代理包装,产生不必要的性能开销,销毁时还可能引发内存释放问题。正确的做法是:响应式数据只存 UI 业务状态,比如当前选中楼栋的 id、设备告警数量、园区运行模式。3D 对象本身全部留在 three 模块内部维护。
数据流变成一条清晰的单向链路:Vue 业务数据变化,触发 three 模块的某个方法,然后由 three 模块去更新对应 object 的材质、位置、显隐。排查问题的时候,只要顺着数据流走一遍就能定位,效率很高。
2.3 数据层:园区设备状态怎么驱动 3D 场景
智慧园区项目最核心的不是模型精细度,而是数据能不能“活”起来。我在项目里把数据分了三层:
- 静态数据:楼栋轮廓、楼层高度、道路位置,这类数据基本不变,来自建筑设计图和模型文件。
- 动态数据:摄像头在线状态、门禁开关、车位数、环境传感器数值,通过后端接口轮询或 WebSocket 推送实时更新。
- 交互数据:用户点击、悬浮、拖拽产生的临时状态,是前两者在 UI 层的反映。
动态数据到达之后,我先做一层统一格式化,转成 three 对象可以直接消费的结构。比如设备点位列表包含position、deviceType、status字段,然后根据 status 去更新材质颜色、热点图标显隐。这里有个重要原则:永远不要在 Three.js 的 render 循环里直接拉接口或做数据处理,否则每一帧都在发请求,场景直接卡死。正确做法是用 Vue 的 watch 监听数据仓库变化,变化时才去更新场景对象,render 循环只负责绘制。
3. 园区场景从零搭建:模型、相机、灯光的关键细节
3.1 初始化场景的第一步不能马虎
很多人一上来就是new THREE.Scene(),然后就开始加模型,实际上前几步的小细节能省掉后面一大堆麻烦。
第一是背景和环境的处理。智慧园区项目通常需要深色渐变背景或天空盒,我习惯用scene.background = new THREE.Color(0x0d1b2a)做深色底,再叠加一层雾效果,让园区边界外的部分自然隐去,整体画面干净很多,也更有“智慧大屏”的质感。
第二是相机参数。园区项目最常用透视相机,初始位置和朝向必须仔细调。我建议不要把相机摆在正上方俯视,而是放一个略带倾角的视角,比如position(80, 60, 120),看向园区中心(0, 0, 0)。既能看清全局布局,又有立体空间感。near 和 far 要根据园区实际尺寸设置,我那个园区是 300 米见方,near 设 0.5,far 设 800。far 设太大或太小都不行,太大影响深度缓冲精度,远处物体可能闪烁。
第三是灯光。纯环境光会把建筑物压成一片平板,没有任何立体感。实际项目里我用了三盏灯:AmbientLight 负责基础亮度,DirectionalLight 模拟太阳并开启阴影,再加一个半球光补充环境色。阴影开启后性能下降比较明显,shadow map 分辨率我控制在 1024 而不是默认值,楼栋数量多的时候只对关键模型开阴影,其他楼栋全靠光照烘焙。
3.2 加载园区模型:glTF 格式是最推荐的选择
园区模型是建模师导出的,我统一要求转成 glTF 格式,也就是 .glb 或 .gltf 文件。这里要说一下原因:OBJ、FBX 不是不能用,但 glTF 是 Three.js 官方主推的格式,支持 PBR 材质、节点层级、动画,还有 Draco 压缩扩展,加载效率和跨工具兼容性都最好。
加载代码不算复杂,但有几个细节值得注意:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath('/draco/') const loader = new GLTFLoader() loader.setDRACOLoader(dracoLoader) loader.load('/models/campus.glb', (gltf) => { scene.add(gltf.scene) // 模型加载后一定要做尺寸归一化 const box = new THREE.Box3().setFromObject(gltf.scene) const size = box.getSize(new THREE.Vector3()) const scale = 100 / Math.max(size.x, size.y, size.z) gltf.scene.scale.setScalar(scale) })模型加载完务必做一次尺寸归一化。建模软件里的单位跟 three 场景单位经常对不上,我踩过一个大坑:建筑模型以毫米为单位导出,直接放进场景里就小了 1000 倍,整个园区只有蚂蚁大小,相机根本找不到。归一化之后再调相机远近,顺畅得很。
3.3 视觉增强:粒子和动态光效让园区“活”起来
静止的园区模型看起来就是一张截图,谈不上“智慧”。我在场景里加了粒子系统,用来表现数据传输、人员流动、风向这些抽象信息。Three.js 的 Points 配合自定义纹理,可以模拟出数据流的动态效果:
const geometry = new THREE.BufferGeometry() const positions = new Float32Array(particleCount * 3) // 填充 positions 数值 for (let i = 0; i < particleCount; i++) { positions[i * 3] = randomRange(-50, 50) positions[i * 3 + 1] = randomRange(0, 20) positions[i * 3 + 2] = randomRange(-50, 50) } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) const material = new THREE.PointsMaterial({ color: 0x00d4ff, size: 0.8, transparent: true, opacity: 0.8 }) const particles = new THREE.Points(geometry, material) scene.add(particles) // render loop 中让粒子缓慢旋转 particles.rotation.y += 0.003粒子数量不要贪多,几千个足够营造氛围,上万就容易拖帧率。想要更精致的视觉效果,还可以把粒子纹理换成圆点光晕贴图,视觉上柔和很多。
4. 让园区“智慧”起来:动态数据驱动与业务交互实现
4.1 点击楼栋的实现思路:射线拾取全过程
智慧园区最核心的交互是点击。鼠标点中一栋楼或一个设备,弹出对应的信息面板。底层逻辑是射线拾取,原理不复杂:从相机位置发出一条射线,穿过鼠标点击位置,检测与三维场景里的哪些物体相交。
代码实现分三步:
const raycaster = new THREE.Raycaster() const mouse = new THREE.Vector2() canvas.addEventListener('click', (event) => { const rect = canvas.getBoundingClientRect() mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 raycaster.setFromCamera(mouse, camera) // 注意第三个参数 true 代表递归检测子节点 const targets = raycaster.intersectObjects(campusGroup.children, true) if (targets.length > 0) { const obj = targets[0].object const buildingId = resolveBuildingId(obj) emitSelected(buildingId) } })第二步是给每个楼栋模型挂自定义属性,比如userData.buildingId。拾取到 mesh 后,通过 userData 反查是哪个楼栋。这里要注意的是,楼栋模型往往是一整棵树,楼体、窗户、屋顶都是子节点。如果不递归检测子节点,你点中楼栋墙体的时候是拿不到结果的,所以intersectObjects的第二个参数必须传 true。
4.2 悬停提示的实现:3D 场景里怎么做 “hover 态”
普通页面上做 hover 很简单,但 3D 场景没有 CSS hover。常用方案是鼠标移动时同样发射射线,检测命中后改变模型材质或添加外发光。我在项目里用了一个更轻量的方案:维护一个 3D 文字标签对象,默认隐藏,检测到 hover 时移动到目标物体上方,显示楼栋名称或设备状态。
Sprite 实现方式:
const canvas = document.createElement('canvas') // 在 canvas 上绘制文字 const context = canvas.getContext('2d') context.fillStyle = '#ffffff' context.font = '24px sans-serif' context.fillText('A栋办公楼', 20, 40) const texture = new THREE.CanvasTexture(canvas) const material = new THREE.SpriteMaterial({ map: texture, transparent: true }) const sprite = new THREE.Sprite(material) sprite.scale.set(8, 4, 1) sprite.visible = false scene.add(sprite)在 mousemove 里持续更新 sprite 位置。Sprite 有一个天然优势:永远面向相机,不用手动旋转,比起用 CSS 坐标计算投影位置,方便太多了。
4.3 与 Vue3 通信:让 three 模块和组件彻底解耦
three 模块本质上是纯 JS 类,不应该直接 import Vue 组件。我的做法是定义一个事件回调或者用一个响应式 store 作为中介。
实际项目里的流程是这样的:
- 鼠标点击,three 模块内部 Raycaster 命中物体,调用回调函数,把 buildingId 传出来。
- 回调函数里更新 store 中的
selectedBuildingId。 CampusPanel.vue通过 computed 或 watch 监控selectedBuildingId变化,发起接口请求,更新面板数据。
这样 three 模块完全不知道 Vue 组件的存在,解耦程度最高。后续如果想换掉 Three.js,或者把渲染模块往 Canvas 2D 上迁移,业务代码都不用动,只替换 three 模块内部实现就够了。
5. 上线前的性能优化:帧率、内存与加载时间
5.1 模型优化:大场景能不能跑得动就看这一步
园区模型加载进场景后,第一件事是合并静态网格。楼栋、地面、道路、路灯、绿化这些不动的物体,尽可能合并到少数几个几何体里,减少 draw call。Three.js 的BufferGeometryUtils.mergeGeometries可以做网格合并,但要注意材质必须能合并。不同颜色的楼栋如果共用材质,需要先理清材质分配,否则合并后颜色会乱。
不能合并的部分,要严格控制数量。我的经验是:园区场景里的 mesh 数量控制在 500 个以内,draw call 在 100 左右,普通办公电脑就能稳定跑到 60 帧。如果楼栋数量特别多,考虑用 LOD,远处楼栋渲染低模,近处才切高模,视觉上几乎无感,性能差距很大。
5.2 帧率监控与调试:别等卡了再找问题
项目里我挂了一个实时性能监控面板,用的是 Three.js 官方的 Stats 库:
const stats = new Stats() stats.showPanel(0) document.body.appendChild(stats.dom) function animate() { stats.begin() renderer.render(scene, camera) stats.end() requestAnimationFrame(animate) }Stats 能实时显示帧率、渲染耗时和内存占用。我把目标定在 60fps,一旦掉到 30fps 以下,按顺序排查:相机视野内物体数量是否过多、阴影贴图范围是不是太大、粒子数量有没有超标、渲染器是否开启了抗锯齿导致 GPU 压力过大。
还有一个容易忽略的小配置,创建 WebGLRenderer 时加powerPreference: 'high-performance',提示浏览器优先调用独立显卡而不是核显。在某些笔记本上,光这一个参数就能让帧率明显提升。
5.3 内存管理:Three.js 项目的隐形杀手
Three.js 项目内存泄漏非常常见,尤其在反复切换路由的场景。我遇到过一个经典问题:从智慧园区页面切到其他页面再回来,页面越来越卡。排查半天发现是旧场景没正确销毁,renderer 和 geometry 资源都堆积在内存里。
正确销毁流程长这样:
function disposeScene(scene, renderer, controls) { scene.traverse((obj) => { if (obj.geometry) { obj.geometry.dispose() } if (obj.material) { if (Array.isArray(obj.material)) { obj.material.forEach((m) => m.dispose()) } else { obj.material.dispose() } // 材质上的贴图也需要释放 const maps = obj.material.map || obj.material.emissiveMap if (maps) maps.dispose() } }) renderer.dispose() if (controls) controls.dispose() }在 Vue3 的onBeforeUnmount里调用销毁逻辑,同时记得取消 requestAnimationFrame 循环。循环不取消的话,即使场景销毁了,渲染函数还在后台不断执行,这是最容易漏掉的内存泄漏源。
6. 我在这个项目里踩过的坑,以及给你的建议
6.1 模型坐标系不对,整个园区“躺平”了
第一次导入园区模型时,整个场景是横躺的,楼栋像倒下了一样。查了一圈发现是建模软件的三轴方向和 Three.js 不一致。这个问题的常规解法是检查模型 y 轴是否向上,如果不是,就用rotateX(Math.PI / 2)修正。更保险的做法是在建模软件里统一坐标轴向,导出前把模型摆正,省得在代码里反复调。
6.2 设备点位和模型位置对不上
设备数据里给的是经纬度坐标,要转成 three 场景里的相对坐标。这个转换不能简单用比例尺,因为园区地图可能有旋转角度。我最后是用几个已知的标定点做仿射变换,把经纬度映射到场景坐标,误差控制在 1 米以内。点位少的时候问题不显眼,一旦设备数量多了,错位就会严重影响体验。
6.3 Vite 热更新把 3D 对象重置了
开发阶段遇到过很烦人的情况:Vite 热更新后,场景里某些对象的材质变化被重置了,因为热更新会重新执行组件初始化逻辑。后来我在开发环境做了一层保护:scene 单例只在首次创建时初始化,后续热更新复用已有的 renderer 和场景,避免“重复创建新场景、旧场景被丢弃”的恶性循环。
6.4 给准备入坑的同学几句经验之谈
如果你想做这类项目,我的建议是从小切口进入,不要一上来就想做整个园区。先做一栋楼,加一个动态数据源,跑通射线拾取和信息面板联动,再往外扩。这样踩坑的成本低,成就感来得也快。
园区可视化这个方向,技术栈本身不算复杂,真正难的是业务理解和数据整理。花时间把园区的楼栋、设备、摄像头、门禁的图纸和点位表理清楚,比单纯研究 Three.js 的 API 重要得多。
这个项目做下来,我最大的收获不是学会了多少个 Three.js 的类,而是真正理解了“渲染逻辑、业务逻辑、数据逻辑”三者该如何在同一个高复杂度项目里分层。这套分层思路放到任何大屏可视化项目里都能复用,我觉得这才是项目真正的价值所在。
本文还有配套的精品资源,点击获取