☰
Vue 3 + Three.js 智慧园区 3D 可视化实战:性能调优与避坑指南
2026/10/18 9:12:48 网站建设 项目流程

简介:这份资源是一套基于 Vue 与 Three.js 的智慧园区前端 3D 场景源码,面向具备一定前端基础、希望入门 WebGL 三维可视化开发的开发者,可用于学习 3D 场景搭建、模型加载与设备监测数据展示等典型需求。压缩包共 67 个文件,约 35.33MB,包含 21 个 js 脚本、5 个 vue 组件、3 个 css 样式、3 个 json 配置,以及 png 贴图、mp4 演示视频、glb 模型、wasm 与 draco 解码文件等,覆盖从页面结构、三维渲染到资源加载的完整链路。项目以电力监测、水力监测等园区设备为展示对象,借助场景、相机、渲染器构建可交互的立体园区模型,目录划分清晰,便于按模块阅读与二次改造。目前已有 2436 人学习下载,适合作为三维可视化练手项目或智慧园区类前端方案的参考起点。

1. 智慧园区大屏为什么总在演示时掉链子

做过智慧园区项目的前端大概都遇到过这种场面:领导来参观,大屏刚切到 3D 园区全景,鼠标一拖,帧率从 60 掉到 12,楼栋模型糊成一片,标签飘在半空找不到对应建筑。演示被迫中断,现场气氛尴尬到极点。问题往往不在 Three.js 本身,而在于把「智慧园区」当成了普通网页来做——模型动辄几十万面、贴图全是 4K、灯光开了实时阴影,浏览器当然扛不住。

Vue + Three.js 这套组合在智慧园区前端 3D 场景里之所以流行,是因为 Vue 负责数据流和 UI 面板,Three.js 负责三维渲染,两者职责清晰。但真正落地时,难点集中在三块:模型怎么轻量化、场景怎么按需加载、交互怎么和业务数据绑定。这篇文章面向已经会 Vue、想上手 Three.js 做园区可视化的前端,从环境搭建讲到性能调优,把每一步的命令、参数和踩坑点都摊开说。看完你应该能独立跑出一个可交互的园区 3D 场景,并且知道哪些地方最容易翻车。

2. 从零搭出第一个可交互园区场景

2.1 技术选型:为什么是 Vue 3 + Three.js 而不是其他组合

先明确一件事:智慧园区前端 3D 场景不是游戏,不需要物理引擎和复杂光照。它的核心诉求是「展示 + 交互 + 数据联动」。基于这个前提,选型逻辑就清晰了。

Vue 3 的 Composition API 适合把 Three.js 的渲染循环、场景对象、相机控制封装成可复用的组合式函数。相比 React,Vue 的响应式系统在绑定业务数据到 3D 对象属性时更直观——比如园区里某个楼栋的能耗数据变化,直接改 ref 就能驱动 3D 场景里的颜色变化。Three.js 则是目前生态最成熟的 WebGL 库,文档全、示例多、社区方案丰富,遇到问题容易找到参考。

不选 Babylon.js 的原因也简单:Babylon 功能更全但包体积更大,智慧园区场景用不到那么多高级特性,Three.js 的按需引入更灵活。不选原生 WebGL 的原因更直接——开发效率差一个数量级,除非你有极致的性能需求。

版本上,Vue 用 3.4 以上,Three.js 用 r150 以上。注意 Three.js 从 r150 开始对颜色管理和渲染管线做了调整,老教程里的outputEncoding已经换成outputColorSpace,照抄旧代码会报错。

2.2 环境搭建与最小可运行场景

先建项目,装依赖。这里用 Vite 而不是 Webpack,因为 Vite 的冷启动和热更新在 3D 开发中体验好太多——改一个材质参数不用等十几秒。

# 创建 Vue 3 项目,选择 TypeScript 模板 npm create vite@latest smart-park-3d -- --template vue-ts cd smart-park-3d # 安装核心依赖 npm install three npm install -D @types/three # 安装轨道控制器(Three.js 官方示例模块,需单独装) npm install three @examples/jsm

依赖装好后,先写一个最小的 Three.js 场景组件。关键点:Three.js 的渲染必须挂载在真实 DOM 上,所以要在onMounted里初始化,在onUnmounted里销毁,否则组件切换时 WebGL 上下文会泄漏。

// src/components/ParkScene.vue <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' const container = ref<HTMLDivElement>() let renderer: THREE.WebGLRenderer let scene: THREE.Scene let camera: THREE.PerspectiveCamera let controls: OrbitControls let animationId: number function init() { if (!container.value) return // 场景:背景色用深蓝灰,贴近园区大屏的科技感底色 scene = new THREE.Scene() scene.background = new THREE.Color(0x0a1628) // 相机:透视相机,视野 45 度,近裁面 0.1,远裁面 1000 // 远裁面不要设太大,否则深度精度下降会出现模型闪烁 camera = new THREE.PerspectiveCamera( 45, container.value.clientWidth / container.value.clientHeight, 0.1, 1000 ) camera.position.set(80, 60, 80) camera.lookAt(0, 0, 0) // 渲染器:开启抗锯齿,限制像素比上限为 2 // 像素比超过 2 在高分屏上性能损耗极大,肉眼几乎看不出差别 renderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setSize(container.value.clientWidth, container.value.clientHeight) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.outputColorSpace = THREE.SRGBColorSpace container.value.appendChild(renderer.domElement) // 轨道控制器:限制俯仰角,防止相机钻到地面以下 controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true controls.dampingFactor = 0.05 controls.maxPolarAngle = Math.PI / 2.2 controls.minDistance = 20 controls.maxDistance = 200 // 环境光 + 平行光:园区场景不需要实时阴影,用环境光打底即可 const ambientLight = new THREE.AmbientLight(0xffffff, 0.8) scene.add(ambientLight) const dirLight = new THREE.DirectionalLight(0xffffff, 1.2) dirLight.position.set(50, 100, 50) scene.add(dirLight) // 临时放一个立方体验证渲染管线是否正常 const geometry = new THREE.BoxGeometry(10, 20, 10) const material = new THREE.MeshStandardMaterial({ color: 0x2a6fdb }) const cube = new THREE.Mesh(geometry, material) cube.position.y = 10 scene.add(cube) animate() } function animate() { animationId = requestAnimationFrame(animate) controls.update() renderer.render(scene, camera) } // 窗口尺寸变化时同步更新相机和渲染器 function onResize() { if (!container.value) return camera.aspect = container.value.clientWidth / container.value.clientHeight camera.updateProjectionMatrix() renderer.setSize(container.value.clientWidth, container.value.clientHeight) } onMounted(() => { init() window.addEventListener('resize', onResize) }) onUnmounted(() => { cancelAnimationFrame(animationId) window.removeEventListener('resize', onResize) controls.dispose() renderer.dispose() // 手动清理场景中的几何体和材质,避免显存泄漏 scene.traverse((obj) => { if (obj instanceof THREE.Mesh) { obj.geometry.dispose() if (Array.isArray(obj.material)) { obj.material.forEach((m) => m.dispose()) } else { obj.material.dispose() } } }) }) </script> <template> <div ref="container" class="scene-container"></div> </template> <style scoped> .scene-container { width: 100%; height: 100vh; overflow: hidden; } </style>

这段代码里有几个参数值得单独说。setPixelRatio限制为 2 是血泪经验——不限制的话,某些 4K 屏上像素比能到 3 甚至 4,渲染面积翻好几倍,帧率直接腰斩。maxPolarAngle设为Math.PI / 2.2是为了防止用户把相机拖到地面以下,看到模型底部的空洞。onUnmounted里的清理逻辑不能省,否则在 Vue 路由切换时,旧的 WebGL 上下文不会自动释放,开几个页面后浏览器就会报「Too many active WebGL contexts」。

2.3 园区模型加载:GLTF 格式与 Draco 压缩

实际项目里不可能用立方体代替楼栋。园区模型通常从建模软件导出,格式选 GLTF 2.0(.glb 单文件),因为它是 Web 端事实标准,Three.js 的 GLTFLoader 支持最完善。

模型加载的核心矛盾是:建模师给的模型往往几十兆甚至上百兆,直接加载用户要等半分钟。解决办法是 Draco 压缩——它能把网格顶点数据压缩到原来的 10% 到 20%,代价是解压需要额外加载一个解码器。

// src/utils/loadParkModel.ts import * as THREE from 'three' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader' export async function loadParkModel( url: string, onProgress?: (percent: number) => void ): Promise<THREE.Group> { // Draco 解码器:从本地 public 目录加载,不要用 CDN // 内网部署时 CDN 不可达会导致模型永远加载不出来 const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath('/draco/') dracoLoader.setDecoderConfig({ type: 'js' }) const loader = new GLTFLoader() loader.setDRACOLoader(dracoLoader) return new Promise((resolve, reject) => { loader.load( url, (gltf) => { const model = gltf.scene // 遍历模型,统一处理材质和阴影 model.traverse((child) => { if (child instanceof THREE.Mesh) { // 园区建筑不需要投射阴影,关掉能省大量性能 child.castShadow = false child.receiveShadow = false // 如果材质是标准材质,调低粗糙度让建筑有轻微反光 if (child.material instanceof THREE.MeshStandardMaterial) { child.material.roughness = 0.7 child.material.metalness = 0.1 } } }) // 自动缩放模型到合适尺寸 const box = new THREE.Box3().setFromObject(model) const size = box.getSize(new THREE.Vector3()) const maxDim = Math.max(size.x, size.z) const scale = 100 / maxDim model.scale.setScalar(scale) // 重新计算包围盒,把模型底部对齐到 y=0 const newBox = new THREE.Box3().setFromObject(model) model.position.y -= newBox.min.y dracoLoader.dispose() resolve(model) }, (xhr) => { if (onProgress && xhr.total > 0) { onProgress(Math.round((xhr.loaded / xhr.total) * 100)) } }, (err) => { dracoLoader.dispose() reject(err) } ) }) }

参数说明:setDecoderConfig({ type: 'js' })强制用 JS 解码器而不是 WASM,因为部分内网环境对 WASM 的 MIME 类型支持有问题,会导致解码器加载失败。roughness设 0.7、metalness设 0.1 是园区建筑的通用值,太高会像金属雕塑,太低会像塑料。自动缩放逻辑是为了兼容不同建模软件导出的单位差异——有的用米,有的用厘米,不统一缩放的话模型可能小到看不见或大到穿模。

3. 让场景真正「智慧」起来:数据绑定与交互

3.1 用 Vue 响应式驱动 3D 对象状态

智慧园区的「智慧」体现在数据联动上。比如某栋楼能耗超标,模型要变红并弹出告警标签;点击楼栋要显示详情面板。这些都需要 Vue 的响应式数据和 Three.js 对象之间建立映射。

常见做法是维护一个buildingMap,把业务 ID 和 Three.js 的 Mesh 对象关联起来。当 Vue 里的数据变化时,通过watch去更新对应 Mesh 的材质或标签。

// src/composables/useBuildingState.ts import { ref, watch } from 'vue' import * as THREE from 'three' interface BuildingData { id: string name: string energy: number // 能耗值,用于判断告警 status: 'normal' | 'warning' | 'offline' } export function useBuildingState() { const buildings = ref<BuildingData[]>([]) // 业务 ID 到 Three.js Mesh 的映射 const meshMap = new Map<string, THREE.Mesh>() // 注册 Mesh,在模型加载完成后调用 function registerMesh(id: string, mesh: THREE.Mesh) { meshMap.set(id, mesh) // 给 Mesh 挂上业务 ID,方便射线检测时反查 mesh.userData.buildingId = id } // 根据状态更新材质颜色 function applyStatusColor(mesh: THREE.Mesh, status: BuildingData['status']) { const colorMap = { normal: 0x2a6fdb, warning: 0xff4d4f, offline: 0x8c8c8c, } const material = mesh.material as THREE.MeshStandardMaterial material.color.setHex(colorMap[status]) // 告警状态加自发光,让建筑在暗色场景里更醒目 material.emissive.setHex(status === 'warning' ? 0x661111 : 0x000000) } // 监听数据变化,同步到 3D 场景 watch( buildings, (list) => { list.forEach((b) => { const mesh = meshMap.get(b.id) if (mesh) applyStatusColor(mesh, b.status) }) }, { deep: true } ) return { buildings, registerMesh } }

这段逻辑的关键在于userData的使用。Three.js 的Object3D.userData是一个自由挂载数据的对象,射线检测命中 Mesh 后,可以通过mesh.userData.buildingId反查业务数据,比遍历数组匹配效率高得多。watch的deep: true是必须的,因为buildings是对象数组,浅监听捕获不到内部属性变化。

3.2 射线检测实现楼栋点击与悬浮高亮

交互是园区场景的灵魂。用户点击楼栋要能弹出信息,鼠标悬浮要能高亮。这靠的是 Three.js 的Raycaster。

// src/composables/useRaycaster.ts import * as THREE from 'three' import { ref } from 'vue' export function useRaycaster( camera: THREE.Camera, scene: THREE.Scene, onSelect: (buildingId: string) => void ) { const raycaster = new THREE.Raycaster() const mouse = new THREE.Vector2() const hoveredId = ref<string | null>(null) // 把屏幕坐标转成归一化设备坐标(NDC),范围 -1 到 1 function updateMouse(event: MouseEvent, dom: HTMLElement) { const rect = dom.getBoundingClientRect() mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 } function handleClick(event: MouseEvent, dom: HTMLElement) { updateMouse(event, dom) raycaster.setFromCamera(mouse, camera) // 只检测建筑层,不检测地面和天空盒 const intersects = raycaster.intersectObjects(scene.children, true) for (const hit of intersects) { const id = hit.object.userData.buildingId if (id) { onSelect(id) break } } } function handleHover(event: MouseEvent, dom: HTMLElement) { updateMouse(event, dom) raycaster.setFromCamera(mouse, camera) const intersects = raycaster.intersectObjects(scene.children, true) const firstBuilding = intersects.find((h) => h.object.userData.buildingId) if (firstBuilding) { const id = firstBuilding.object.userData.buildingId if (hoveredId.value !== id) { hoveredId.value = id dom.style.cursor = 'pointer' } } else { hoveredId.value = null dom.style.cursor = 'default' } } return { handleClick, handleHover, hoveredId } }

参数上要注意raycaster.intersectObjects的第二个参数recursive设为true,因为建筑模型通常是 Group 嵌套结构,不递归就检测不到子 Mesh。另外射线检测在mousemove里每帧都跑会有性能开销,实际项目里建议加节流,比如 50ms 一次。

3.3 标签系统:CSS2DRenderer 与性能取舍

园区场景离不开标签——楼栋名称、能耗数值、告警提示。Three.js 提供了CSS2DRenderer,把 HTML 元素投影到 3D 空间,好处是文字清晰、样式好写、支持中文。

// src/utils/labelManager.ts import * as THREE from 'three' import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' export function createLabelRenderer(container: HTMLElement) { const labelRenderer = new CSS2DRenderer() labelRenderer.setSize(container.clientWidth, container.clientHeight) // 标签层要绝对定位覆盖在 canvas 上,且不能拦截鼠标事件 labelRenderer.domElement.style.position = 'absolute' labelRenderer.domElement.style.top = '0' labelRenderer.domElement.style.pointerEvents = 'none' container.appendChild(labelRenderer.domElement) return labelRenderer } export function createBuildingLabel( name: string, position: THREE.Vector3 ): CSS2DObject { const div = document.createElement('div') div.className = 'building-label' div.textContent = name const label = new CSS2DObject(div) label.position.copy(position) // 标签中心对齐,避免偏移 label.center.set(0.5, 1) return label }

CSS2D 标签的坑在于数量。超过 50 个标签后,DOM 操作的开销会明显拖累帧率。我的做法是:只给视野内、距离相机最近的 20 个建筑显示标签,其余隐藏。判断逻辑放在渲染循环里,用相机距离排序,超出阈值的label.visible = false。这样既保证信息可读,又不至于让 DOM 成为瓶颈。

4. 性能调优:从 60 帧掉到 12 帧的排查路径

4.1 用 renderer.info 定位渲染瓶颈

帧率掉了别瞎猜,先看数据。Three.js 的renderer.info会实时统计绘制调用数、三角形数、几何体和纹理数量。

// 在渲染循环里打印关键指标 function logRenderInfo() { const info = renderer.info console.log({ drawCalls: info.render.calls, // 绘制调用次数,超过 500 就要警惕 triangles: info.render.triangles, // 三角形数量,超过 100 万会明显卡顿 geometries: info.memory.geometries, textures: info.memory.textures, }) }

经验阈值:drawCalls控制在 300 以内,triangles控制在 50 万以内,中端笔记本能稳定 60 帧。如果drawCalls过高,说明模型没有合并,每个小物件都在单独绘制;如果triangles过高,说明模型面数没减。这两个指标是排查的第一入口。

4.2 模型合并与实例化:减少 draw call

园区里如果有大量重复元素——路灯、树木、栏杆——用InstancedMesh能把几十次绘制合并成一次。

// 用 InstancedMesh 批量渲染路灯 function createLampInstances( geometry: THREE.BufferGeometry, material: THREE.Material, positions: THREE.Vector3[] ) { const mesh = new THREE.InstancedMesh(geometry, material, positions.length) const matrix = new THREE.Matrix4() positions.forEach((pos, i) => { matrix.setPosition(pos) mesh.setMatrixAt(i, matrix) }) // 实例矩阵更新后必须手动标记,否则不生效 mesh.instanceMatrix.needsUpdate = true return mesh }

InstancedMesh的适用条件是几何体和材质完全相同,只有位置/旋转/缩放不同。路灯、树木、井盖这类标准化设施最适合。注意instanceMatrix.needsUpdate = true这行不能漏,漏了的话实例位置不会更新,所有实例会叠在原点。

4.3 按需渲染与帧率控制

园区场景大部分时间是静止的,没必要一直以 60 帧跑渲染循环。改成「有交互时渲染,静止时降频」能省大量 GPU。

let needsRender = true let lastRenderTime = 0 const RENDER_INTERVAL = 1000 / 30 // 静止时限制到 30 帧 function animate(time: number) { requestAnimationFrame(animate) const isMoving = controls.enableDamping && controls.getAzimuthalAngle() !== lastAzimuth if (needsRender || isMoving || time - lastRenderTime > RENDER_INTERVAL) { controls.update() renderer.render(scene, camera) labelRenderer.render(scene, camera) lastRenderTime = time needsRender = false } }

这个策略的核心是区分「交互态」和「静止态」。用户拖动相机时全帧率渲染保证流畅,松手后降到 30 帧省电。needsRender标志位在数据变化时手动置true,触发一次重绘。实测在中端设备上,这个改动能让 GPU 占用从 70% 降到 25% 左右。

5. 避坑指南:园区 3D 项目最容易翻车的五个点

5.1 模型加载后一片漆黑

现象:模型加载成功,控制台无报错,但场景里什么都看不见。

原因:多数是相机位置或模型缩放问题。建模软件导出的模型可能尺寸是几千单位,而相机远裁面只有 1000,模型在视野外被裁掉了。也可能是模型原点在很远的地方,相机没对准。

解决:加载后先打印包围盒new THREE.Box3().setFromObject(model),看min和max的实际数值。如果尺寸异常,按 2.3 节的自动缩放逻辑处理。同时把相机远裁面临时调到 10000 验证,确认是裁面问题后再调回合理值。

5.2 内存持续增长,切换页面后崩溃

现象:在园区页面和其他页面之间来回切换几次,浏览器标签页内存飙升,最终卡死。

原因:Three.js 的几何体、材质、纹理、渲染器都占用显存,Vue 组件销毁时不会自动释放。renderer.dispose()只释放渲染器本身,场景里的资源要手动遍历清理。

解决:在onUnmounted里完整遍历场景,对每个 Mesh 调用geometry.dispose()和material.dispose(),纹理要单独texture.dispose()。另外renderer.forceContextLoss()可以强制释放 WebGL 上下文,在组件销毁时调用能彻底回收。

5.3 标签位置漂移,拖动相机后对不上建筑

现象:初始加载时标签位置正确,但拖动或缩放相机后,标签和建筑错位。

原因:CSS2DRenderer的渲染没有和主渲染器同步。如果只在renderer.render后偶尔调用labelRenderer.render,标签位置就会滞后。

解决:确保labelRenderer.render(scene, camera)和renderer.render(scene, camera)在同一个渲染循环里成对调用,且相机参数完全一致。另外labelRenderer.setSize要在窗口 resize 时同步更新,否则投影矩阵不匹配也会导致偏移。

5.4 射线检测点不中,点击没反应

现象:鼠标点击建筑没反应,或者要点偏一点才能选中。

原因:一是intersectObjects的recursive参数没设true,只检测了顶层 Group;二是模型有透明材质或visible = false的辅助对象挡住了射线;三是鼠标坐标转换时用了window而非 canvas 的getBoundingClientRect。

解决:先确认recursive为true。然后在射线检测时过滤掉不需要的对象,比如地面、天空盒、辅助线,给它们设raycast = () => {}直接禁用射线检测。坐标转换必须用 canvas 元素的getBoundingClientRect,不能用window.innerWidth,否则页面有侧边栏时坐标会偏。

5.5 移动端打开直接白屏

现象:PC 上正常,手机或平板上打开白屏或闪退。

原因:移动端 GPU 性能有限,纹理尺寸超过 4096 会直接失败;另外移动端浏览器对 WebGL 的显存限制更严格,模型稍大就上下文丢失。

解决:纹理统一压到 2048 以下,用KTX2压缩纹理格式替代 PNG/JPG,显存占用能降 75%。模型面数控制在 20 万以内。同时监听webglcontextlost事件,上下文丢失时给出友好提示而不是白屏。

renderer.domElement.addEventListener('webglcontextlost', (e) => { e.preventDefault() console.warn('WebGL 上下文丢失,可能是显存不足') // 这里可以触发降级逻辑,比如切换到 2D 平面图 })

6. 进阶技巧:用后期处理让园区大屏更有质感

基础场景跑通后,想让大屏效果上一个档次,可以加后期处理。Three.js 的EffectComposer支持叠加多种效果,园区场景最常用的是泛光(Bloom)和颜色校正。

泛光能让建筑的窗户灯光、告警高亮产生光晕,科技感立刻出来。但参数调不好会糊成一片,这是最容易翻车的地方。

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer' import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass' import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass' import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass' function setupPostProcessing( renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, size: { width: number; height: number } ) { const composer = new EffectComposer(renderer) // 基础渲染通道 const renderPass = new RenderPass(scene, camera) composer.addPass(renderPass) // 泛光通道:强度 0.6,半径 0.4,阈值 0.85 // 阈值是关键,低于 0.8 会把普通建筑也泛光,整个画面糊掉 const bloomPass = new UnrealBloomPass( new THREE.Vector2(size.width, size.height), 0.6, // strength 强度 0.4, // radius 扩散半径 0.85 // threshold 亮度阈值 ) composer.addPass(bloomPass) // 输出通道:处理颜色空间转换,r150 之后必须加 const outputPass = new OutputPass() composer.addPass(outputPass) return composer }

三个参数的关系要理解清楚:threshold决定「多亮的东西才会泛光」,园区场景建议 0.85 以上,只让自发光材质和告警高亮触发;strength决定光晕强度,0.5 到 0.8 之间比较克制;radius决定扩散范围,0.3 到 0.5 足够,太大整个画面会像蒙了层雾。

用了EffectComposer之后,渲染调用要从renderer.render改成composer.render(),并且composer.setSize要在窗口 resize 时同步。另外后期处理会额外消耗性能,移动端建议直接关掉,只在 PC 大屏上开启。

还有一个实用技巧:给告警建筑单独加一层泛光。做法是把告警建筑渲染到独立的图层,用第二个UnrealBloomPass只处理这个图层,这样普通建筑不受影响,告警效果又足够醒目。代价是多一次渲染通道,需要根据设备性能决定是否启用。

我自己做园区项目时,后期处理这块翻过最大的车是忘了加OutputPass。r150 之前EffectComposer内部会自动处理颜色空间,r150 之后必须显式加OutputPass,否则整个画面会偏暗偏灰,调半天材质颜色都救不回来。这个坑当时排查了大半天,最后翻官方迁移文档才发现。所以升级 Three.js 版本时,一定要把迁移指南过一遍,别凭老经验写代码。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询