5渲染判断_地图建筑渲染

导读

阅读完此文,你会了解:

1. 建筑三角面生成及渲染

2. 大量建筑渲染的性能优化

3. 使用精细建模替换建筑白模

建筑数据源

11bc871f0768a21a1069a15f850cee91.png

建筑数据

上图“建筑数据”中,数组中的每一项都是一个独立的建筑相关信息的描述。coordinates 描述了建筑在地面上投影的顶点的经纬度坐标;height 描述的是建筑的高度;id 是建筑的编号,在替换精细模型时会用到;info 描述的是其他相关信息。有了 coordinates 和 height 就足够渲染出建筑模型了。

建筑Geometry生成

多边形网格

建筑面属于不规则的简单多边形,如图《建筑投影》中的“北京站”。在做图形渲染的时候,需要将原始数据中的坐标信息转化成多边形,就要做多边形三角剖分。

首先定义什么是简单多边形,简单多边形是由一组有序顶点组成的,例如:点 V0 ~ 点 Vn-1。相邻的顶点之间通过边( Vi, Vi-1 )连接,并且边(Vn-1, V0)连接起始点。每个顶点被两条边所共享,而边的所有交点都是顶点。下图中,左边的多边形是简单多边形,中间的多边形点 1 被四条边共享,不符合定义的条件,不算是简单多边形,右侧的多边形中边 1-4,边 0-2 的交点不是我们定义的顶点之一,因此该图形也不符合简单多边形的定义。

754deab0ec14fa50f5b884a9ba4ba749.png

简单多边形和非简单多边形

三角剖分的算法有很多,最常用的是 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 }。

f9504edb2d622d90fe81f9230536d431.png

步骤1

2. 继续移除点 4,此时的三角形对应是T1 = < 2, 4, 5 >。图“步骤2”展示了变化后的效果。

ff786941a6ff90f6e76996c273e77bc1.png

步骤2

3、4、5、6按照上述方法做递归。

7. 相邻顶点 8 和 1 都是凸节点,顶点 8 依旧是个耳朵,顶点 1 依旧不是耳朵。因此凹节点集合不变,耳朵列表变为E = { 2, 8 },最后,移除耳朵2顶点,对应的耳朵是 T6 = < 1, 2, 7 >。图“步骤7”展示了操作前后的多边形对比。

30893d09e45ff16f54692a64a02abeee.png

步骤7

到此为止只剩下了三个顶点,这三个顶点组成最后的三角形T7 = < 7, 8, 1 >。所有的三角形分割线是如图“步骤8”。

6034aa0a033653a7598a84719213401e.png

步骤8

此外,还有含有岛洞(带孔多边形)的多边形三角化方法。算法的详细介绍请戳 https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf

8ec739647b2a2f44f7eece0c1274cf79.png

建筑投影

70f38050249a42015e77e4c315251717.png

进行拉伸后

物理信息绑定

建筑侧面的材质通常会有一些动画,或者做分楼层的需求,因此我们将侧面与顶面分开进行渲染。一个建筑物,由多个侧面和一个顶面构成。

ae3842c136371689dc8cb410cb2b35be.png

建筑的顶点下标

每个侧面,由两个三角形面片构成,具有 4 个顶点,每个顶点,除了顶点坐标外,还有一个类型为 vec2 的 uv 属性:

c7545fc223b8ea3cd9f4277e4851c933.png

uv

从建筑的俯视图看,uv 的 s 为:

1fe08b2d4ef85a9f4f475d360200d708.png

uv(s)

同时,为每个顶点绑定一个类型为 vec3 的 uv1,用来存储建筑面的实际长度(单位:米)和一个随机值:

f692a703af26524ede564fec7bfea5b0.png

每个顶点的 uv1

在着色器中,可以通过 uv 与 uv1 组合,拿到建筑单面的宽高比例、距离屋顶的距离、距离地面的距离、建筑的高度等信息,同时 random 结合 uniform 中的 u_time 时间变量,就可以做出建筑侧面动画效果。

建筑侧面效果

a9afab8c713b902a0e764fa8eb8ee622.png

建筑侧面的渐变效果

顶点着色器:

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);}

实现更加复杂点亮城市效果:

f9a8788737d01e7aa145e1ac03818252.gif

点亮城市

在《侧面生成》中,我们说到侧面顶点绑定的uv1变量,其中包含了width、height和random。利用这三个变量,我们可以自定义着色器实现“点亮城市”。

在uv1中能拿到每一条边的width、height和random,在uniforms中传入设定的每个楼层的高度u_floorHieght,同时利用uv可以计算出每个楼层的位置;需要一个噪声纹理来给高亮的楼层着色;在uniforms中同时传入u_time来表示时间的变化量,每一帧不断更新u_time,噪声纹理根据uv的取值随着u_time有规则的变化。

7baea9eb25bf65227d8dc99e96ee1c92.png

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)}

22d46debb5c244389abc2ad1ce9728de.gif‍建筑geometry合并渲染之前

‍‍‍

5d6e0d28-3d41-eb11-8da9-e4434bdf6706.gif

建筑geometry合并渲染之后

‍‍

从上面图左上角的状态框可以看出,在相同的地图姿态下,geometry合并渲染之后的FPS是合并之前的5倍左右,合并渲染之后能到达60FPS左右流畅运行。

建筑模型加载与替换

49126046218bc86cc922d721db80bf80.png

建筑白模替换前

bed2076db403d4382783130327f08eda.png

替换为精细建模后

地图上每个瓦片和建筑都有唯一的id与之对应,建筑的位置以瓦片左下角为原点进行偏移。3D设计师进行建模时将模型放入瓦片对应的偏移位置,然后生成一份配置文件,描述建筑所在的瓦片id、建筑id。

根据瓦片的二进制数据,可将建筑模型替换分为3种情况:

  1. 无建筑数据层数据,需要加载精细模型。比如,银川中国枸杞博物馆。

  2. 有建筑数据层数据,但是需要替换的建筑白模不存在。比如,武汉江城之门。

  3. 有建筑数据层数据,建筑在建筑数据层存在,比如,重庆来福士广场。

无建筑数据层数据时,直接按照瓦片id加载建筑模型即可;有建筑模型数据时,需要在渲染建筑白模时判断瓦片id与建筑id是否与该建筑的配置文件匹配;如果匹配,只渲染精细模型。

往期回顾

  1. 《打造服务B端客户的酷炫3D地图可视化产品》

  2. 《数据源与存储计算》

  3. 《地图交互与姿态控制》

  4. 《地图文字渲染》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值