尧图精选

Vue3 + Three.js 智慧校园三维可视化实战:从选型到性能优化

🕒 发布时间:2026/9/20 12:28:42 📁 来源:尧图网络
简介基于 Vue3 与 three.js 打造的智慧校园 3D 可视化前端项目源码面向具备前端基础、希望系统学习 Web 三维开发的工程师和学习者可帮助快速搭建可交互的校园场景并理解从模型加载、场景构建到用户交互的完整实现链路。压缩包共 142 个文件大小约 193.42MB核心代码包含 Vue 单文件组件、JavaScript 逻辑文件场景资源涵盖 GLB/GLTF 三维模型、PNG/JPG 贴图素材、MP4 视频以及 glsl/frag 着色器文件并带有工程配置与运行依赖目录中模型、样式、脚本分层清晰便于按需替换和二次开发。项目支持场景旋转缩放、多预设视角切换、模型自动旋转并能在三维场景内播放视频交互逻辑基于 Vue3 组合式 API 与 three.js 的场景、相机、渲染器协同实现可帮助开发者掌握几何体、材质、光源、动画循环等核心概念也适合作为智慧校园、数字孪生类项目的起步模板。目前已有 1497 人学习浏览相比零散教程这套源码能直接安装依赖并运行调试从环境搭建到功能实现都有明确入口可降低技术踩坑成本同时模型、贴图与交互脚本分类齐全适合用于课程设计或毕业设计参考。 这段时间一直在忙一个智慧校园的三维可视化项目技术栈锁定在 Vue3 Three.js。整体看下来Vue3 的响应式机制和组合式 API 跟 Three.js 这种偏命令式的渲染引擎搭配起来比 Vue2 时期顺手得多。但真正把一个 3D 场景从“能转”做到“能用、能管、能联动业务”中间踩的坑是真不少。这篇文章就把这套智慧校园 3D 场景从技术选型、场景搭建到业务联动、性能优化完整梳理一遍分享给正在做或者准备做同类 Web3D 可视化项目的人参考。1. 为什么最终选了 Vue3 Three.js 这套组合1.1 智慧校园项目到底在解决什么问题做技术选型之前得先想清楚甲方要什么。智慧校园 3D 场景不是简单地做个学校模型放网页上转两圈它要承接的是校园可视化管理的诉求把教学楼、宿舍、食堂、操场这些物理实体和课表、设备状态、人员位置这些业务数据在同一个三维空间里叠起来。具体到我们这个项目核心需求有四条第一把整个校区的建筑和地形按真实位置还原出来第二用户可以在场景中自由漫游、切换视角第三点击任意建筑或教室能弹出对应的信息面板第四教室的实时使用状态、设备的在线状态要能跟着后端数据变化。这几条需求直接决定了技术选型的方向——需要一个成熟的 Web 渲染引擎还需要一个能快速落地业务界面的前端框架。1.2 Vue3 比 Vue2 更适合做这件事的三个理由很多教程还在用 Vue2 配 Three.js但做了这个项目之后我的感受是Vue3 有本质性的优势不只是语法层面的变化。第一个理由是组合式 API。Three.js 的代码量大、状态分散场景、相机、渲染器、灯光、模型各是各的。Vue2 时代习惯用 options API 把 setup 里乱七八糟的逻辑硬塞进 data、methods、mounted 里面一个 3D 场景动辄上千行mounted 里堆一堆初始化代码维护起来极其痛苦。Vue3 的 setup 函数天然适合把 Three.js 的初始化逻辑拆成一个个可复用的组合函数比如 useScene、useCamera、useRenderer、useControls每个模块各管一摊代码结构清晰很多。第二个理由是生命周期更合理。Vue2 里组件销毁后要手动清 Three.js 的资源稍不注意就会内存泄漏。Vue3 的 onBeforeUnmount 钩子和组合式函数结合起来可以很自然地封装一个 useThree 函数在 onBeforeUnmount 里统一做 renderer.dispose()、scene.clear()、控制器的 dispose()避免一个个记。第三个理由是响应式系统的差异。Vue2 的响应式是基于 Object.defineProperty 实现的对对象属性的新增和删除侦测不到存在很多边界情况Vue3 的 Proxy 响应式虽然也要注意别把 Three.js 的实例对象直接丢进 reactive 里但整体上对复杂对象的状态管理要稳得多。我们后面会详细讲这个坑。1.3 为什么不选 Cesium、Babylon 或纯 Three.js这个项目也考虑过 Cesium 和其他引擎最终放弃是综合考量的结果。Cesium 虽然三维地球和 GIS 能力很强但做校园这种小范围、高精细度场景有点杀鸡用牛刀而且它默认的坐标体系和渲染风格跟普通 Web 页面差异很大想做出好看的智慧校园效果定制成本比 Three.js 高不少。巴比伦 Babylon.js 在 WebGPU 支持和引擎整合度上其实很强但国内社区资料和人才储备明显不如 Three.js招人、查问题都费劲。纯 Three.js 裸写当然也能做但当项目里需要弹窗、表单、数据面板这些常见业务组件时纯 Three.js 没有 DOM 层面的框架支持做起来要从零开始造轮子。而 Vue3 Three.js 的分工是Three.js 专注于 WebGL 渲染和交互Vue 3 负责业务组件和状态管理各自在自己擅长的领域发力这是这套组合的核心优势。2. 场景数据与工程目录设计先把地基打牢2.1 一个清晰的 3D 场景工程目录长什么样选好技术栈之后最忌讳的就是一上来就写代码。做智慧校园这种大型场景工程结构和数据组织没理清楚的话后面加功能就是灾难。我这里列一个实测下来很好用的目录结构src/ ├── components/ │ ├── CampusScene.vue # 3D场景容器组件 │ ├── InfoPanel.vue # 建筑信息弹窗 │ └── TagOverlay.vue # 标签叠加层 ├── composables/ │ ├── useThree.js # 场景初始化 │ ├── useScene.js # 模型加载与场景管理 │ ├── useControls.js # 轨道控制器封装 │ ├── useRaycaster.js # 点击拾取 │ └── useCampusData.js # 校园业务数据(教室状态等) ├── data/ │ ├── buildings.json # 建筑坐标与基础信息 │ └── classrooms.json # 教室状态数据 ├── models/ │ ├── campus.glb # 校园整体模型 │ └── building_detail.glb # 建筑细节模型 └── utils/ └── resize.js # 窗口自适应这套结构把 3D 渲染逻辑、业务数据、界面组件完全解耦。核心思路是把所有 Three.js 相关的代码封装在 composables 里组件层只负责调用 setup业务数据统一从 data 目录读取或通过接口请求。2.2 用 JSON 配置驱动场景而不是写死坐标智慧校园场景里最麻烦的事情之一是建筑点位、教室编号、设备位置这些信息非常多。如果把坐标点和建筑信息直接写死在代码里后期调整一次得改一堆东西。我的做法是维护一份 buildings.json 配置每个建筑五个关键字段id、name、position、rotation、scale。渲染层在加载模型时根据这份配置决定每栋楼放在哪里、朝向哪里、多大尺寸。这样模型和配置分离的好处是3D 美术或实施人员调模型时前端只需要修改 JSON代码不用动。以数据驱动的方式组织场景核心优势在于业务联动非常方便。比如后端返回了“A栋302教室今日出勤率95%”前端拿到之后通过 id 找到场景中的对应标签更新内容即可。2.3 场景对象的分层管理策略Three.js 的 Scene 是一个树状结构管理不好就容易乱。我给智慧校园场景定位了这么几个层级Scene (根场景) ├── Lighting (环境光、平行光、点光源) ├── Terrain (地形、道路、操场) ├── Buildings (所有建筑的分组容器) │ ├── Building_A (分组内挂建筑模型的各个部件) │ └── Building_B ├── Trees (植被) └── Markers (标签、热点、特效层)这个分层策略的核心思想用 Group 把同一类对象归组方便对某一类对象做统一操作。比如你夜间模式要调整所有建筑的发光强度直接遍历 Buildings 组就行要给所有建筑加呼吸灯效果也只需要在 Buildings 组里统一处理。另外有一个实战经验加载进来的 glTF 模型经常带有多层嵌套嵌套的 Group 层级我一般在加载完成后先调用 scene.updateWorldMatrix(true, true) 再做坐标转换同时尽量把模型内部的嵌套层级简化掉能合并的几何体就合并减少场景内节点的数量。3. 从零搭起智慧校园 3D 场景的核心流程3.1 Vue3 项目中集成 Three.js 基础环境创建项目用的是 Vite目前这是 Vue3 生态的最佳选择。命令很简单npm create vitelatest vue3-campus -- --template vue cd vue3-campus npm install three0.160.0 npm install -D types/three特别提醒一下Three.js 在 r150 之后 API 变化比较大比如 outputEncoding 改成了 outputColorSpace不同版本的写法不通用最好在 package.json 里锁一个版本。我这边的项目用的是 0.160.0整个项目期间没出现 API 变动问题。接下来封装 useThree.js。所有 Three.js 场景的第一步都是一个铁三角Scene场景、Camera相机、Renderer渲染器。import * as THREE from three export function useThree(containerRef) { const scene new THREE.Scene() scene.background new THREE.Color(0x87CEEB) // 天空蓝 const camera new THREE.PerspectiveCamera( 60, // 视角 window.innerWidth / window.innerHeight, // 宽高比 0.1, // 近裁面 1000 // 远裁面 ) camera.position.set(150, 120, 200) camera.lookAt(0, 0, 0) const renderer new THREE.WebGLRenderer({ antialias: true, // 抗锯齿 logarithmicDepthBuffer: false }) renderer.setSize(window.innerWidth, window.innerHeight) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.shadowMap.enabled true renderer.shadowMap.type THREE.PCFSoftShadowMap if (containerRef.value) { containerRef.value.appendChild(renderer.domElement) } return { scene, camera, renderer } }这里面几个细节值得注意PerspectiveCamera 的第一个参数越大视野越宽智慧校园场景我用 60 度主观视角刚好近裁面 0.1、远裁面 1000 是基于校园模型尺度来的如果场景特别大远裁面还要调setPixelRatio 限制在 2 是为了避免高分屏上每个像素都渲染导致性能爆炸。3.2 渲染循环从哪来Three.js 需要持续渲染才能实现动画和交互刷新的效果常见的做法是用 requestAnimationFrame 驱动。但在 Vue3 项目里渲染循环不能在组件里直接写个 while 循环必须结合生命周期来做。我在 useThree.js 里加了一段import { onBeforeUnmount } from vue let animationId null function startLoop(callback) { const loop () { animationId requestAnimationFrame(loop) callback() } loop() } function stopLoop() { if (animationId) { cancelAnimationFrame(animationId) animationId null } } onBeforeUnmount(() { stopLoop() })startLoop 的 callback 里可以传一个函数每次渲染帧时执行场景更新。比如在智慧校园项目里就会更新 OrbitControls、更新标签朝向、更新设备状态对应的颜色变化等。有一点必须提前想到路由切换或组件销毁后一定要 cancelAnimationFrame否则渲染循环不会停White 屏、卡顿、内存泄漏都会接踵而来。3.3 加载 glTF 模型智慧校园的“面子”所在校园场景的模型一般由美术用 Blender、SketchUp 或者 Revit 制作导出的格式我们统一用的 GLBglTF 的二进制版原因很简单Three.js 原生支持、体积小、材质信息保留好、纹理可以直接内嵌。加载模型用的是 GLTFLoaderimport { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js import { DRACOLoader } from three/examples/jsm/loaders/DRACOLoader.js const loader new GLTFLoader() const dracoLoader new DRACOLoader() dracoLoader.setDecoderPath(https://www.gstatic.com/draco/versioned/decoders/1.4.1/) loader.setDRACOLoader(dracoLoader) loader.load(/models/campus.glb, (gltf) { const model gltf.scene // 模型坐标修正 model.position.set(0, 0, 0) model.scale.set(1, 1, 1) scene.add(model) })这里要说一下 DRACOLoader 的用处。智慧校园的模型动辄二三十兆直接加载非常慢。Draco 压缩是 Google 的网格压缩方案可以把 GLB 文件压缩到三分之一甚至更小。但副作用是运行时需要有解压器代码所以需要 setDecoderPath 指定解码器路径。另外常遇到模型加载进来朝向不对、位置偏移、比例不对这种问题在配置阶段解决比在代码里做矩阵变换要省事得多。如果美术用的建模软件和 Three.js 坐标系不一致比如 Blender 是右手坐标系Three.js 也是右手坐标系但 Z 轴朝上还是 Y 轴朝上往往搞混模型长宽高缩放不对会导致整个场景比例失调。3.4 地表、植被和道路的处理技巧校园场景的地表和大面积植被不需要一栋楼一栋楼去建模这里有两个我在项目里实测好用的技巧。地表部分先用一个大的 PlaneGeometry 铺底然后加载一张卫星图或手绘校园平面图作为纹理贴在地面上作为场景的空间定位基础。这个做法的好处是后期模型摆放有明确的视觉参考不用靠猜。植被和小的装饰物如果用单独模型会带来极大的性能压力。我这边是用 InstancedMesh 实现的把一棵树的几何体加载一次然后用 instanceMatrix 复制几十个实例放到不同位置绘制调用从 50 次降到 1 次。种树时配合随机算法让树的旋转、缩放有点差异化整体视觉上自然很多。道路和跑道线这类线性元素用 Three.js 的 Line 或 LineSegments 就能解决不需要创建复杂的曲面。3.5 光照与阴影提升真实感的关键组合默认的三维场景是没有光照的加载出来的模型看起来“平”得不行。智慧校园场景里我用的是三光源组合环境光 平行光 点光源。环境光AmbientLight作为基础照明把所有模型提亮到可见程度强度大概 0.4平行光DirectionalLight模拟阳光是产生阴影的主光源强度 1.2 左右位置从斜上方照射校园点光源PointLight在需要重点展示的体育馆、图书馆周围补充氛围光视觉效果更丰富。阴影这块要注意的是阴影非常消耗性能不需要每个模型都开 castShadow。我的做法是建筑和树木开 castShadow地面和道路不开 receiveShadow 就不明显反而会边缘异常变暗。平行光的方向需要多次试验调太高会显得中午烈日调太低又像黄昏最终位置我调成了 (100, 150, 80)。4. 业务功能落地从“能看”到“能用”的关键跨越4.1 OrbitControls 漫游的参数调优场景建好之后第一个需求几乎都是“能不能转”。Three.js 官方提供了 OrbitControls轨道控制器支持鼠标拖拽旋转、滚轮缩放、右键平移是智慧校园项目最常用的控制器。import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js const controls new OrbitControls(camera, renderer.domElement) controls.enableDamping true // 开启惯性/阻尼效果 controls.dampingFactor 0.08 // 阻尼系数 controls.maxPolarAngle Math.PI / 2.2 // 限制俯仰角避免转到地底下去 controls.minDistance 20 // 最小缩放距离 controls.maxDistance 500 // 最大缩放距离 controls.target.set(0, 0, 0) // 旋转中心这里最关键的是 enableDamping 设为 true没有阻尼的话拖拽停止得特别生硬。还有一个细节OrbitControls 会在控制器的变化事件里更新相机位置所以必须在渲染循环里调用 controls.update()否则阻尼效果不会生效。智慧校园场景里我还给相机加了预设视角功能——全览、教学楼、宿舍区等几个关键位置一键跳转。实现方式是定义一个 CameraRoute 数组切换动画时用 gsap 或者手写插值函数让相机平滑移动到目标位置。4.2 Raycaster 点击拾取建筑与信息弹窗能让用户点击建筑弹信息是智慧校园从“3D 沙盘”走向“3D 应用”的关键功能。Three.js 点击检测的标准方案是 Raycaster光线投射器。原理说起来很简单从相机发射一条穿过鼠标位置的射线检测射线与场景中哪些对象相交取距离最近的交点。但实现起来有几个坑逐一说一下。首先监听鼠标事件拿归一化坐标const raycaster new THREE.Raycaster() const mouse new THREE.Vector2() function onMouseClick(event) { // 将鼠标坐标转换为归一化设备坐标 (-1 到 1) mouse.x (event.clientX / window.innerWidth) * 2 - 1 mouse.y -(event.clientY / window.innerHeight) * 2 1 raycaster.setFromCamera(mouse, camera) const intersection raycaster.intersectObjects(buildings.children) if (intersection.length 0) { const target intersection[0].object // 通过 userData 或场景节点 id 匹配业务数据 const buildingId target.userData.id showBuildingInfo(buildingId) } }第二个关键是射线检测的性能。如果把整个 scene 传进去做检测每次点击都会遍历所有网格场景大时会卡顿。我的做法是专门维护一个 clickableObjects 数组只有需要交互的建筑和热点对象放进去。第三个是容易忽略的细节。检测到的 target 可能是建筑模型内部的某个子节点比如一面墙、一个窗户而不是整栋楼。所以需要给每个网格的 userData 设置 id然后把模型的每个部件都打上父级建筑的 id点击后逐级向上查找父节点找到对应的建筑 id。信息弹窗这块比较简单Vue3 的 Teleport 组件很适合做点击建筑后打开一个绝对定位的浮层组件内部接受建筑信息 props再配合 Vue Transition 做淡入淡出效果。4.3 用 CSS2DRenderer 做建筑标签智慧校园场景里每个建筑都要显示名称比如“综合楼”“图书馆”。网上很多教程用 Sprite 做标签我的经验是效果一般文字不清晰缩放也麻烦。推荐用 CSS2DRenderer——它是 Three.js 官方示例集里面的一个渲染器本质是把 DOM 元素定位映射到三维空间坐标类似一个 3D 空间里的 HTML 图层。import { CSS2DRenderer, CSS2DObject } from three/examples/jsm/renderers/CSS2DRenderer.js const labelRenderer new CSS2DRenderer() labelRenderer.setSize(window.innerWidth, window.innerHeight) labelRenderer.domElement.style.position absolute labelRenderer.domElement.style.pointerEvents none // 防止遮挡点击 container.appendChild(labelRenderer.domElement) // 给建筑添加标签 const labelDiv document.createElement(div) labelDiv.className building-label labelDiv.textContent 综合楼 const label new CSS2DObject(labelDiv) label.position.set(0, 15, 0) // 悬浮在建筑上方 building.add(label)为什么选择 CSS2DRenderer 而不是 Sprite因为标签是 HTML 写的能用 CSS 控制样式字体、颜色、圆角、动画都很好做而且文字清晰度不受像素比影响。但要注意的是CSS2DRenderer 渲染出来的 DOM 元素会覆盖在 WebGL 画布上需要单独维护它的容器层级给它设置 pointer-events 为 none避免标签挡住用户点击场景的触发。CSS2DRelatedObject 和 CSS2DObject 一样最大的一个坑是在组件销毁时清不干净。我给每个标签加了 dispose 方法路由离开时把 labelRenderer.domElement 从 DOM 树上移除同时递归遍历 scene 移除所有 CSS2DObject否则下一套场景会叠着上一套的标签。4.4 用 Vue3 响应式数据驱动场景内的状态变化智慧校园的核心业务能力在于“动态数据可视化”。比如教室的真实使用状态空闲/占用/考试如果能在 3D 场景上用不同颜色区分管理者一眼就能看清全貌。这块的实现思路是Three.js 场景对象的数据源直接对接 Vue3 的响应式数据。const classroomStatus ref({}) // 假设从接口定时拉取教室状态 async function fetchClassroomStatus() { const res await api.getClassroomStatus() classroomStatus.value res.data updateBuildingColors(classroomStatus.value) } function updateBuildingColors(statusMap) { Buildings.children.forEach((building) { const id building.userData.id const status statusMap[id] const material building.material if (Array.isArray(material)) { material.forEach((m) { m.color.set(status busy ? 0xff4d4f : 0x52c41a) }) } else { material.color.set(status busy ? 0xff4d4f : 0x52c41a) } }) }每次数据更新时调用 updateBuildingColors把对应建筑的颜色按状态改变。项目中我用了定时器每 5 秒轮询一次接口这样教室状态的刷新不需要刷新页面实时性也能接受。但这个方案有几个问题要注意。第一不要直接把 Three.js 的 Object3D 对象塞进 Vue3 的 reactive 或 ref 里Proxy 包装会导致性能下降而且内部的 three 属性会被递归代理出现不可预期的 bug。第二更新颜色时如果建筑有多个材质数组要逐个设置。第三如果想做渐变过渡而不是瞬间变色可以在渲染循环里逐步把当前颜色和目标颜色做 lerp 插值效果会精致很多。4.5 智慧校园项目的其他高频业务组件除了上面这些实际项目里一定还会用到以下几个功能简单提一下实现思路。监控点位联动在场景的特定位置放置一个小的球体几何体或者视频图标贴图点击后打开一个模态框里面放入真实的 HLS/RTSP 视频流通常通过 WebRTC 或者相关的转流服务来实现。这本质上是 CSS2D 标签弹窗的组合应用。路径规划用户点击两个建筑需要生成校园内的行走路线这个得结合后端的地理信息数据在 Three.js 场景里画出 PathLine。用 LineGeometry 会出现线条太细的问题可以用 TubeGeometry 或 LineSegmentsLineBasicMaterial 的 linewidth注意 linewidth 在大多数浏览器 WebGL 有上限实际上是 1所以用 TubeGeometry 更靠谱。告警提示设备出现异常时在场景中对应位置弹出一个红色闪烁感叹号。这种效果我一般用 Sprite 或者 CSS2D 元素通过动画让透明度不断变化UI 交互效果好而且不会打断用户的主流程。5. 踩坑实录Vue3 响应式、纹理黑屏和打包体积5.1 Vue 3 的 Proxy 响应式把 Three.js 对象“包坏了”这是我在项目里踩的最深的一个坑。最开始很自然地写了const scene reactive(new THREE.Scene())结果场景加载一切正常但一旦调用 scene.add 或 scene.remove就会报一堆类型错误。原因很简单Vue3 的 reactive 是 Proxy 代理Three.js 内部大量使用 this 引用自己比如 scene.add 方法内部会调用 this.__addObject而 Proxy 的 get 拦截会让 this 指向代理对象而不是原始对象导致内部逻辑判断全部混乱。解决办法也非常简单第一永远不要用 reactive 去包装 Three.js 实例Scene、Mesh、Camera 这些都用普通变量。第二需要响应式管理的只有业务数据比如当前选中建筑 ID、教室状态映射把这类数据用 ref 维护场景对象通过调用方法来响应变化形成一个单向数据流。这就够了。5.2 纹理加载异步导致的“裸奔”和闪烁项目过程中出现过模型加载进去后是暗淡的、没有贴图过几秒后才出现贴图的情况而且偶尔还会白屏闪烁。定位了半天问题出在 Three.js 纹理加载是异步的如果材质还没 ready 就开始渲染画面自然不对。处理方式有两个一是统一用 LoadingManager 监听所有资源的加载进度全部加载完成后才开始渲染循环二是给模型设置 renderer.waitForImages true这个属性加上去之后渲染器会等纹理解码完成再绘制闪烁问题基本消失。加一个 LoadingManager 的进度条也很有必要因为智慧校园模型大体验上需要给用户一个加载中的反馈。否则模型没出来用户直接关页面了。5.3 路由切换后再进场景白屏组件销毁没做干净这个坑在多人协作时最容易出。Vue 组件路由切走了但 Three.js 的场景、渲染器、控制器都还留在内存里再次进入组件时又创建了一份新的场景越堆越多最终要么白屏要么帧率跌到谷底。解决方案就是严格遵守一个原则在 onBeforeUnmount 里把该释放的全部释放掉。关键代码onBeforeUnmount(() { stopLoop() controls.dispose() renderer.dispose() // 遍历场景 Geometry/Material/Texture 并 dispose scene.traverse((obj) { if (obj.geometry) obj.geometry.dispose() if (obj.material) { // 处理材质数组 if (Array.isArray(obj.material)) { obj.material.forEach(m disposeMaterial(m)) } else { disposeMaterial(obj.material) } } }) labelRenderer.domElement.remove() renderer.forceContextLoss() })其中 renderer.forceContextLoss() 是压箱底的绝招强制释放 WebGL 上下文防止多次创建 Renderer 后浏览器因为上下文数量耗尽而白屏。我实测过不调用这段代码路由切个五六次之后必然出现问题。5.4 打包体积从 8MB 缩到 1.8MB 的优化过程首次 build 之后发现产物 8 兆多加载体验很差。分析下来主要大头是 three.js 模块体积、GLB 模型文件体积、以及没有做代码分割。第一步是模型优化。把 GLB 里用不到的动画、摄像机、灯光节点全部删掉用 Draco 压缩后体积减掉一半多。第二步是对 Three.js 做 tree-shaking。用 import * as THREE from three 的方式引入其实大部分打包器无法对命名空间引入做很好的 tree-shaking最好改成按需引入import { WebGLRenderer, Scene, PerspectiveCamera, Color, AmbientLight, DirectionalLight, PointLight, MeshStandardMaterial } from three import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js第三步是 Vite 配置手动分包// vite.config.js build: { rollupOptions: { output: { manualChunks: { three: [three], three-addons: [three/examples/jsm/controls/OrbitControls.js, three/examples/jsm/loaders/GLTFLoader.js] } } } }这样基础包和 Three.js 单独拆开配合 CDN 缓存策略用户二次访问时首屏几乎秒开。6. 性能优化的几个实操点从 30 帧到 60 帧的调整6.1 合并几何体与绘制调用先说说对性能影响最大的一个指标Draw Call绘制调用次数。智慧校园场景如果建了 100 栋楼每栋楼是独立 Mesh 的话渲染一帧的 Draw Call 就有 100加上地形、树木、路灯等轻轻松松几百次。浏览器每帧能承受的 Draw Call 数量是有限的到了瓶颈就会严重掉帧。最有效的优化是合并几何体把静态的、材质相同的模型部分用 BufferGeometryUtils.mergeGeometries 合成一个大几何体。比如路边的路灯把几百个路灯合并成一个 Mesh材质还保持不变渲染开销瞬间降下来。但要注意合并是有代价的合并后就不能单独控制每个物体了也无法做独立的点击拾取。所以我对智慧校园的策略是可交互的建筑不合并保证点击检测方便纯装饰的路灯、绿化、栅栏这些不交互的对象全部合并。6.2 纹理图集与压缩智慧校园项目里用到的纹理贴图非常多教学楼的外墙、玻璃、地面、草坪如果每张纹理都单独加载会占用大量显存切换材质时也会卡顿。我的做法是尽量用纹理图集Texture Atlas把多张小图拼到一张大图上这样材质只需要一个贴图资源。配合 Texture.anisotropy 设置各向异性过滤远处的地面纹理不会模糊到没法看。同时记得压缩图片格式jpg 格式适合摄影图png 适合透明贴图能转 WebP 的场景尽量转 WebP。大尺寸纹理2048x2048 以上要考虑分辨率是否真的被看到校园场景大部分贴图 1024 就够这个细节可以省很多显存。6.3 LOD 细节层次与视野裁剪校园很大用户站在东门的时候看不到南门的细节。给每个建筑加 LODLevel of Detail细节层次就是根据相机距离切换模型的精细程度远的建筑用低模近的建筑用高模。Three.js 提供了 LOD 类实现起来不复杂const lod new THREE.LOD() lod.addLevel(highDetailModel, 0) lod.addLevel(midDetailModel, 100) lod.addLevel(lowDetailModel, 200) scene.add(lod)再配合相机视锥体自动裁剪Frustum CullingScene 会自动忽略视野外的对象。默认的 Box3 包围盒计算在多节点模型上可能不准确我习惯手动设置 mesh.frustumCulled 相关属性或者调用 computeBoundingSphere 重新计算。6.4 限制像素比和后期效果高分屏上 setPixelRatio 设置太高会导致像素填充量爆炸。我统一做了 clamp把上限设为 2一般项目就够了。后期处理是视觉提升的神器But 也是性能杀手。UnrealBloomPass 泛光效果加上去整个场景的科技感会强很多但移动端低端设备会直接卡到没法用。我的策略是检测硬件通过 navigator.hardwareConcurrency 或者 WebGL 的 debugRendererInfo 里的 UNMASKED_RENDERER_WEBGL 查看 GPU如果是低端设备就自动关闭后期特效只保留基础渲染。整个项目从选型到上线前前后后折腾了差不多两个月。现在回想最有价值的经验其实就两条一是场景对象和业务数据严格分离Three.js 的对象不要碰 Vue 的响应式系统业务状态用 ref 单独管二是组件销毁时那套 dispose 代码一定要写彻底白屏和内存泄漏基本都是这个原因。如果你的项目也要做校园、园区、工厂这类 3D 可视化场景按照这篇文章的路径走一遍至少能少踩掉七成的坑。最后再分享一个我个人的小习惯调试 3D 场景时我永远会在项目中加一个 stats 插件开着浏览器后台跑一会儿观察 Draw Call 和三角形的数字变化。这三个数字跟真实用户体验的关联度非常高——当 Draw Call 超过 300、三角形超过 200 万的时候帧率几乎必然下降。性能优化别等最后才做从项目一开始就要记账心里有数后面调起来才不慌。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →