导读
阅读完此文,你会了解:
1. 建筑三角面生成及渲染
2. 大量建筑渲染的性能优化
3. 使用精细建模替换建筑白模
建筑数据源
建筑数据
上图“建筑数据”中,数组中的每一项都是一个独立的建筑相关信息的描述。coordinates 描述了建筑在地面上投影的顶点的经纬度坐标;height 描述的是建筑的高度;id 是建筑的编号,在替换精细模型时会用到;info 描述的是其他相关信息。有了 coordinates 和 height 就足够渲染出建筑模型了。
建筑Geometry生成
多边形网格
建筑面属于不规则的简单多边形,如图《建筑投影》中的“北京站”。在做图形渲染的时候,需要将原始数据中的坐标信息转化成多边形,就要做多边形三角剖分。
首先定义什么是简单多边形,简单多边形是由一组有序顶点组成的,例如:点 V0 ~ 点 Vn-1。相邻的顶点之间通过边( Vi, Vi-1 )连接,并且边(Vn-1, V0)连接起始点。每个顶点被两条边所共享,而边的所有交点都是顶点。下图中,左边的多边形是简单多边形,中间的多边形点 1 被四条边共享,不符合定义的条件,不算是简单多边形,右侧的多边形中边 1-4,边 0-2 的交点不是我们定义的顶点之一,因此该图形也不符合简单多边形的定义。
简单多边形和非简单多边形
三角剖分的算法有很多,最常用的是 EarClipping(耳切法),算法的复杂度 O ( N2 )。
下面简单介绍一下耳切法的思路。顾名思义,得先知道“耳朵”是啥,才能去切。简单多边形的耳朵,是指由连续顶点 V0, V1 和 V2 组成的内部不包含其他任意顶点的三角形。在计算机几何术语中,V0 与 V2 之间的连线称之为多边形的对角线,点 V1 称之为耳尖。虽然可以将耳尖放到三角形的任意一个顶点上,但是我们定义三角形只包含一个耳尖。一个由四个顶点(或者更多)组成的多变形至少有两个不重叠的耳尖。这个特性提供了一个通过递归来完成三角分割的方法。针对由 N 个定点组成的多边形,找到它的耳朵,移除耳尖,此时剩余顶点组成了一个 N-1 个顶点的简单多边形。重复这个操作直到剩余三个顶点。
通过一些细节改进,耳朵的消除可以在 O ( N2 ) 的时间来完成。第一步是将简单多边形使用双向链表存储,这样可以快速的移除耳尖。列表的构建复杂度是 O ( N ),第二步是遍历顶点寻找耳朵。对于每一个顶点 Vi 和围绕该顶点的三角形 < Vi-1, Vi, Vi+1 >,(总长度为 N,所以 Vn = V0, V-1 = Vn-1),测试其他顶点是否在当前三角形中,如果有一个顶点在三角形里面,则不是耳朵,只有都不在的情况,才算是找到一个耳朵。具体实现的时候可以考虑以下因素让这个算法更为高效。
1. 当发现有其他任意点在三角形里面的时候便放弃当前测试。怎么判断点在三角形内呢?顺/逆时针判定法。该方法要求点的顺序是顺时针或逆时针的,如果是顺时针的点,沿着3条边走,如果目标点 P 在三角形内,那么 P 始终在边的右侧。同理,如果是逆时针的话,目标点 P 应该始终在边的左侧。例如,逆时针的三个点 A, B, C,判断AB x AP, BC x BP, CA x CP, 如果这三个向量叉积的 Z 值都同向,并且都为负的话,说明 P 点在三角形内部。
2. 只需要考虑凸顶点(两边夹角小于180度)即可。怎么判断顶点的凹凸?叉积计算。连续的三个点 A, B, C。判断顶点 B 的凹凸性,可以通过计算 |AB x AC|, 结果大于 0, 则为凸顶点。
考虑这些因素后,可以快速构建出耳尖列表和凹凸顶点列表。
下面以图“简单多边形和非简单多边形”中的简单多边形为例,介绍一下算法流程:
1. 初始构建的凸顶点集合 C = { 0, 1, 3, 4, 6, 9 },初始凹顶点集合 R = { 2, 5, 7, 8 },初始的耳尖集合 E = { 3, 4, 6, 9 },当顶点 3 被移除的时候,其对应的耳朵是三角形 T0 = < 2, 3, 4 >。图“步骤1”展示了操作后的多边形效果。相邻点 2 是个凹节点,变化后依旧是凹的,顶点 4 之前是个耳朵,现在依旧耳朵,所以凹节点结合 R 保持不变,耳尖集合现在变成了E = { 4, 6, 9 }。
步骤1
2. 继续移除点 4,此时的三角形对应是T1 = < 2, 4, 5 >。图“步骤2”展示了变化后的效果。
步骤2
3、4、5、6按照上述方法做递归。
7. 相邻顶点 8 和 1 都是凸节点,顶点 8 依旧是个耳朵,顶点 1 依旧不是耳朵。因此凹节点集合不变,耳朵列表变为E = { 2, 8 },最后,移除耳朵2顶点,对应的耳朵是 T6 = < 1, 2, 7 >。图“步骤7”展示了操作前后的多边形对比。
步骤7
到此为止只剩下了三个顶点,这三个顶点组成最后的三角形T7 = < 7, 8, 1 >。所有的三角形分割线是如图“步骤8”。
步骤8
此外,还有含有岛洞(带孔多边形)的多边形三角化方法。算法的详细介绍请戳 https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
建筑投影
进行拉伸后
物理信息绑定
建筑侧面的材质通常会有一些动画,或者做分楼层的需求,因此我们将侧面与顶面分开进行渲染。一个建筑物,由多个侧面和一个顶面构成。
建筑的顶点下标
每个侧面,由两个三角形面片构成,具有 4 个顶点,每个顶点,除了顶点坐标外,还有一个类型为 vec2 的 uv 属性:
uv
从建筑的俯视图看,uv 的 s 为:
uv(s)
同时,为每个顶点绑定一个类型为 vec3 的 uv1,用来存储建筑面的实际长度(单位:米)和一个随机值:
每个顶点的 uv1
在着色器中,可以通过 uv 与 uv1 组合,拿到建筑单面的宽高比例、距离屋顶的距离、距离地面的距离、建筑的高度等信息,同时 random 结合 uniform 中的 u_time 时间变量,就可以做出建筑侧面动画效果。
建筑侧面效果
建筑侧面的渐变效果
顶点着色器:
varying float vFragDepth;uniform float logDepthBufFC;varying vec2 v_uv;void main () { v_uv = uv; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * mvPosition; #ifdef USE_LOGDEPTHBUF #ifdef USE_LOGDEPTHBUF_EXT vFragDepth = 1.0 + gl_Position.w; #else gl_Position.z = log2( max( EPSILON, gl_Position.w + 1.0 ) ) * logDepthBufFC - 1.0; gl_Position.z *= gl_Position.w; #endif #endif}
片元着色器:
varying vec2 v_uv;uniform vec3 color;uniform vec3 color1;uniform float opacity;varying float vFragDepth;uniform float logDepthBufFC;void main () { #if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT ) gl_FragDepthEXT = log2( vFragDepth ) * logDepthBufFC * 0.5; #endif gl_FragColor = vec4(mix(color, color1, v_uv.t), opacity);}
实现更加复杂点亮城市效果:
点亮城市
在《侧面生成》中,我们说到侧面顶点绑定的uv1变量,其中包含了width、height和random。利用这三个变量,我们可以自定义着色器实现“点亮城市”。
在uv1中能拿到每一条边的width、height和random,在uniforms中传入设定的每个楼层的高度u_floorHieght,同时利用uv可以计算出每个楼层的位置;需要一个噪声纹理来给高亮的楼层着色;在uniforms中同时传入u_time来表示时间的变化量,每一帧不断更新u_time,噪声纹理根据uv的取值随着u_time有规则的变化。
noise
片元着色器关键代码:
void main() { float floorHeight = u_oneMeter * u_floorHeight; float uv_t = fract((v_uv.t * v_uv2.y) / floorHeight); float floorNumber = floor((v_uv2.y - v_uv.t * v_uv2.y) / floorHeight); vec3 lightColor; if (v_uv2.y / floorHeight < 8.0) { lightColor = u_lightColor; } else { if (mod(floorNumber, u_stepFloor) == 1.) { lightColor = u_lightDiffColor; } else { lightColor = u_lightColor; } } float useEmissive = floor(uv_t * 3.0); float a; if (useEmissive >= 2.0) { a = 1.0; } else { a = 0.0; } vec2 uv_dist = v_uv * v_uv2.xy; vec4 noise = texture2D(u_noise, vec2(fract(uv_dist.s * u_uvScale.x), floor(uv_dist.t / floorHeight) * u_uvScale.y)); float b = noise.r * 2.0 + v_uv2.z * 5.0; b = 1.0 - (sin(b * 3.0 + u_time * 1.0) + 1.0) / 2.0; float topAlpha = 0.9; float bottomAlpha = 0.2; gl_FragColor = mix(vec4(emissive, 0.0), vec4(lightColor, 1.0), a * b * distAlpha); gl_FragColor.a = mix(bottomAlpha, topAlpha, v_uv.t);}
性能优化
渲染建筑图层的数据需要进行三角剖分,生成大量的三角面,并进行多次渲染,十分消耗性能。为了减少性能的消耗,需要加速三角剖分、减少图形API调用。
加速三角剖分
耳切法已经被尽可能的优化了,想从算法层面提升建筑渲染的效率,已不太现实。我们想到了WebAssembly。利用 C++ 语言效率高的特性,将耳切法用 C++ 实现,最终编译为 WASM 文件供 JS 调用,达到性能的提升。具体如何进行 WebAssembly 编程以及编程过程中需要注意的事项,可以查看往期的文章《矢量瓦片数据源处理》。这里我们着重对比一下使用 JS 和 WASM 之间的差异。
对于单个地图建筑所包含的顶点数,一般介于 10 - 100 个之间,因此生成 100 级别顶点的三角面属于常态。对于一个包含 100 个顶点图形来说,WASM 相对于 JS 平均提效 4.4 倍左右。
Javascript | WASM | 顶点数 | 效率提升 |
1.047851563ms | 0.489013672ms | 10 | 2.1 |
2.384765625ms | 0.54296875ms | 100 | 4.4 |
26.51806641ms | 2.228027344ms | 1000 | 11.9 |
214.0541992ms | 69.7019043ms | 10000 | 3.1 |
对于一次性加载一屏的建筑物,大约 100 级别的图形数,每个图形以 100 个顶点为例,WASM 相对于 JS 平均提效 5.5 倍左右。
JavaScript | WASM | 图形数 | 效率提升 |
2.384765625ms | 0.54296875ms | 1 | 4.4 |
14.496826171875ms | 2.193115234375ms | 10 | 6.6 |
59.2490234375ms | 10.713134765625ms | 100 | 5.5 |
115.946044921875ms | 59.777099609375ms | 1000 | 1.9 |
743.798095703125ms | 509.175048828125ms | 10000 | 1.5 |
减少图形API调用
每个建筑都是独立生成的mesh,单独渲染会造成大量的性能损耗;如果能进行合并渲染,将会提升渲染效率。
解析出每一个瓦片的建筑数据之后,单个瓦片的建筑geometry合并使用了ThreeJS提供的BufferGeometryUtils.mergeBufferGeometries方法:
for (let i = 0; i < tiles.length; i++) { const buildingData = tiles.getBuildingData() const geometries = [] for (let j = 0; j < buildingData.length; j++) { const geometry = createBuildingGeometry(buildingData[j]) geometries.push(geometry) } const mergeGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries) const mesh = new THREE.Mesh(mergeGeometry, lambertMaterial)}
建筑geometry合并渲染之前
建筑geometry合并渲染之后
从上面图左上角的状态框可以看出,在相同的地图姿态下,geometry合并渲染之后的FPS是合并之前的5倍左右,合并渲染之后能到达60FPS左右流畅运行。
建筑模型加载与替换
建筑白模替换前
替换为精细建模后
地图上每个瓦片和建筑都有唯一的id与之对应,建筑的位置以瓦片左下角为原点进行偏移。3D设计师进行建模时将模型放入瓦片对应的偏移位置,然后生成一份配置文件,描述建筑所在的瓦片id、建筑id。
根据瓦片的二进制数据,可将建筑模型替换分为3种情况:
无建筑数据层数据,需要加载精细模型。比如,银川中国枸杞博物馆。
有建筑数据层数据,但是需要替换的建筑白模不存在。比如,武汉江城之门。
有建筑数据层数据,建筑在建筑数据层存在,比如,重庆来福士广场。
无建筑数据层数据时,直接按照瓦片id加载建筑模型即可;有建筑模型数据时,需要在渲染建筑白模时判断瓦片id与建筑id是否与该建筑的配置文件匹配;如果匹配,只渲染精细模型。
往期回顾
《打造服务B端客户的酷炫3D地图可视化产品》
《数据源与存储计算》
《地图交互与姿态控制》
《地图文字渲染》