【GIS开发小课堂】高德地图带你秀出万级3D模型!

介绍

随着三维技术革新的浪潮,地图应用已经从传统的二维展示跃迁至一个立体多维的视觉时代。然而,随着3D模型数量的指数级增长,如何在地图上高效加载并维持流畅的用户体验,成为了我们必须面对的挑战。本文的内容主要就是对“ 如何在高德地图上加载万级的gltf模型,仍然保持流畅的体验。”的实现方案做一个分享。

图片

实现思路

常规做法

网上对加载gltf模型的常规的做法是直接Loader加载完模型后,使用Mesh.clone()批量复制模型。这种做法只能支持1000个以内的数量级,如果是面数复杂的模型估计上了100个都会有卡顿现象。

实例化网格做法

使用InstancedMesh 来渲染大量具有相同几何体与材质、但具有不同世界变换的物体,特别适合制作森林、砂石、大量城市路灯、大量城市垃圾桶等场景。使用 InstancedMesh 可以有效地减少 draw call 的数量,从而提升应用的整体渲染性能。

这种方法有个限制,instanceMesh(geometry, material, instanceCount)的第一个参数gemetry就要求模型是的geometry是1个单独的Mesh,没有子节点或兄弟节点。换言之如果你的gltf模型如果是3个独立的几何体组成,那么就需要实例化3个instanceMesh,且需要办法保证几何体的相对位置是固定的。

关于模型优化

无论采用哪种方案去实现最终效果的高性能,都需要渲染技术和原始模型的配合。再高效的渲染技术,如果遇到过于三角面数过于庞大的模型还是会有心无力,因此我们需要在保证模型外观过得去的情况下尽量优化模型。

对于模型的优化我们可以从以下几个方面入手,具体步骤再另外的文章分享:

1. 合并模型网格

2. 精简面数

3. 模型展UV贴图

4. 删除模型中看不见的内部面和无用边

经过这几步优化,我把原模型的三角形面数从9044个减少到1492个,文件体积从1060KB缩减到159KB。且保证了外观上没有太大的变化。

图片

实现代码

three.js的版权声明及许可证:

The MIT License
Copyright © 2010-2024 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copyof this software and associated documentation files (the "Software"), to dealin the Software without restriction, including without limitation the rightsto use, copy, modify, merge, publish, distribute, sublicense, and/or sellcopies of the Software, and to permit persons to whom the Software isfurnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included inall copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS INTHE SOFTWARE.

下面我以“在地图上加载10000个灯杆”为例,讲解编码步骤。

图片

1.预先加载模型,创建实例化网格。​​​​​​​

function loadModel(){    this.loader = new GLTFLoader()    this.loader.load(sourceUrl, function (gltf) {        // 获取模型        const mesh = gltf.scene.children[0]               // 缓存模型        _models.model = mesh    _models.animations = gltf.animations        resolve()      },      function (xhr) {        console.log((xhr.loaded / xhr.total * 100) + '% loaded')      },      function (error) {        console.log('An error happened' + error)      }    )} async function initMesh(){    await loadModel()     const { model, animations } = _models    const geometry = model.geometry    const material = model.material   // 加载20000个模型  const instanceCount = this._data.length    // 3个参数分别是几何体、材质、数据量    const instanceMesh = new THREE.InstancedMesh(geometry, material, instanceCount)    this.scene.add(instanceMesh)}

2.遍历数据,并调整模型的实例化网络。​​​​​​​

const dummy = new THREE.Object3D()const size = 4.0dummy.scale.set(size, size, size) const color = new THREE.Color() for (let i = 0; i < instanceCount; i++) {  const { id, modelId, altitude, angle, coords, lngLat } = this._data[i]    // 调整空间位置  dummy.position.set(coords[0], coords[1], (altitude === undefined ? 0 : altitude) + 0)  dummy.updateMatrix()  // 调整朝向  if (angle !== undefined) {    const fn = upAxis === 'x' ? 'rotateX' : (upAxis === 'z' ? 'rotateZ' : 'rotateY')    dummy[fn](-angle / 180 * Math.PI)  }  // 将对dummy的调整复制到具体的模型中  instanceMesh.setMatrixAt(i, dummy.matrix)  // 给实例添加一个空的颜色,用于实现后面的拾取后颜色控制    instanceMesh.setColorAt(i, color)}

3. 处理网格的鼠标拾取事件,鼠标拾取还是用Raycaster射线去实现。​​​​​​​

// 整个地图容器添加鼠标事件const t = thisthis._raycaster = new THREE.Raycaster()this.container.addEventListener('mousemove', _.debounce(function (event) {  t.onRay(event)}, 100, true)) onRay (event) {  const { scene, camera } = this  // 归一化坐标  const pickPosition = this.setPickPosition(event)  // 通过摄像机和鼠标位置更新射线  this._raycaster.*setFromCamera*(pickPosition, camera)  // 计算射线和场景的交集  const intersects = this._raycaster.intersectObjects(scene.children, true)   if (typeof this.onPicked === 'function') {    this.onPicked.apply(this, [{ targets: intersects, event }])  }  return intersects} // 获取鼠标在three.js 中的归一化坐标setPickPosition (event) {  const pickPosition = { x: 0, y: 0 }  const rect = this.container.getBoundingClientRect()  // // 将鼠标位置归一化为设备坐标, x 和 y 方向的取值范围是 (-1 to +1)  pickPosition.x = (event.clientX / rect.width) * 2 - 1  pickPosition.y = (event.clientY / rect.height) * -2 + 1  return pickPosition} // 拾取到物体时,处理拾取事件onPicked ({ targets, event }) {  let attrs = null  // 选中状态颜色  const color = new THREE.Color(0xff0000)   const cMesh = this.getParentObject(targets[0], { name: this._impactName })   if (cMesh?.isInstancedMesh) {    const intersection = this._raycaster.intersectObject(cMesh, false)    // 获取当前模型实例的编号,其实是生成序号    // 通过这个编号就可以操作指定的模型了    const { instanceId } = intersection[0]    // 把拾取对象变成指定颜色    cMesh.setColorAt(instanceId, color)    // 强制颜色实时更新,必须!    cMesh.instanceColor.needsUpdate = true    return  }

总结

这个方案还能继续优化吗?答案是可以的,只要将模型的可见面数合并、不可见面删除,我们还可以进一步支持更大的数据量。这里试了下将灯杆精简为一个立方体之后加载5万多数据量的效果,看看左上角的帧率,性能是完全没问题的。

图片

相关链接

THREE实例化网格

https://threejs.org/docs/index.html?q=instance#api/zh/objects/InstancedMesh

blender如何完全合并物体

https://www.bilibili.com/video/BV1Lm4y1y7dr/?spm_id_from=333.337.search-card.all.click

Blender展UV基础

https://www.bilibili.com/video/BV1vZ4y1v71s/?spm_id_from=333.999.0.0

本文由高德开发者大本营成员—张林海提供

仅代表作者个人观点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值