高德地图实现加载大量模型
介绍
有一天大佬跑过来跟我说:"目前我们在NS区部署了5万多根灯杆,你想想办法把全部的灯杆模型在NS区地图上全部show出来。不要搞那种放大了视角就四叉树聚合减少显示数量,聚焦到局部地区才显示百来个模型的投机取巧的操作。"
我听完马上震惊了,没想到大佬居然看穿了我的鬼蜮伎俩,并且提出了更高的要求。作为骄傲的工程师(不是)我哪能服软,马上安排。
经过几天断断续续的捣鼓研究,终于在大佬的PUA下搞定了这个事情。本文的内容主要就是对" 如何在高德地图上加载万级的gltf模型,仍然保持流畅的体验。"的实现方案做一个分享。
名词解释
网格: 在计算机图形中,网格是用一系列三角形来表示的 3D 对象,对应到three.js中就是Geometry类,Geometry包含了3D对象的顶点、边和面信息。
实现思路
常规做法
网上对加载gltf模型的常规的做法是直接Loader加载完模型后,使用Mesh.clone()批量复制模型。这种做法只能支持1000个以内的数量级,如果是面数复杂的模型估计上了100个都会有卡顿现象。
实例化网格做法
使用 InstancedMesh 来渲染大量具有相同几何体与材质、但具有不同世界变换的物体,特别适合制作森林、砂石、大量城市路灯、大量城市垃圾桶等场景 。 使用 InstancedMesh 可以有效地减少 draw call 的数量,从而提升应用的整体渲染性能。
这种方法有个限制,instanceMesh(geometry, material, instanceCount
) 的第一个参数gemetry就要求模型是的geometry是1个单独的Mesh,没有子节点或兄弟节点。换言之如果你的gltf模型如果是3个独立的几何体组成,那么就需要实例化3个instanceMesh,且需要办法保证几何体的相对位置是固定的。
关于模型优化
无论采用哪种方案去实现最终效果的高性能,都需要渲染技术和原始模型的配合。再高效的渲染技术,如果遇到过于三角面数过于庞大的模型还是会有心无力,因此我们需要在保证模型外观过得去的情况下尽量优化模型。
对于模型的优化我们可以从以下几个方面入手,具体步骤再另外的文章分享:
- 合并模型网格
- 精简面数
- 模型展UV贴图
- 删除模型中看不见的内部面和无用边
经过这几步优化,我把原模型的三角形面数从9044个减少到1492个,文件体积从1060KB缩减到159KB。且保证了外观上没有太大的变化。
实现代码
下面我以"在地图上加载10000个模型"为例,讲解编码步骤。
1.预先加载模型,创建实例化网格
jsx
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
// 创建实例化网格
const instanceCount = this._data.length
// 3个参数分别是几何体、材质、数据量
const instanceMesh = new THREE.InstancedMesh(geometry, material, instanceCount)
this.scene.add(instanceMesh)
}
2.遍历数据,调整实例化网络
jsx
const dummy = new THREE.Object3D()
const size = 4.0
dummy.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)
}
- 处理网格的鼠标拾取事件,鼠标拾取还是用Raycaster射线去实现
jsx
// 整个地图容器添加鼠标事件
const t = this
this._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万多数据量的效果,看看左上角的帧率,性能是完全没问题的。
当然使用WebGL的方案要做到流畅体验,不仅仅需要技术上的优化,也需要性能良好(独立显卡)的终端设备的支持。这时候就可以理直气壮地跟老板说:加载模型上10万可以,得加钱(手动狗头)。
相关链接
THREE实例化网格
blender如何完全合并物体
www.bilibili.com/video/BV1Lm...
Blender展UV基础