WebGL 渲染管线在 Web3D 地图中的应用

1,722 阅读7分钟

导读

阅读完此文,你会了解:

  1. WebGL 渲染管线是什么
  2. 如何利用 WebGL 渲染管线解决渲染问题

地图渲染中遇到的问题

之前的文章中[数据源与存储计算]提到过,地图的瓦片数据源是按照瓦片金字塔进行切割和索引的,地图引擎根据当前视窗的范围推算所需瓦片的索引值,然后加载和渲染。

image.png

image (1).png

在瓦片切割上,会遇到一个建筑或一个面,落在多个个瓦片的边缘上的情况,这种情况下,服务端的切割策略会有:

  1. 沿着边缘切开,这种处理方式的弊端在于会因为精度而产生缝隙;
  2. 沿着边缘切开,其中和瓦片相交面积最大的一块儿不裁切,这样就会导致有的部分会画多次;

image (2).png
右侧瓦片切为一半

image (3).png
左侧瓦片切为整体

上图中的这个建筑就被切分开了,当我们使用 WebGL 渲染为 3D 地图时,相交部分因为重叠就会画1+次,会伴有:

  1. z值fighting问题;
  2. 透明物体重叠部分渲染结果不一致的问题;

image (4).png   我们分析了 Mapbox 的效果和代码,我们发现他们是利用渲染管线中的 「模板测试」 解决了这个问题。

WebGL 渲染管线是什么

image (5).png

WebGL 进行图形渲染时会经过多个步骤,这些步骤组成了 WebGL 的渲染管线。当图形数据通过整个 WebGL 流水线后,GPU 就把结果写入到 WebGL 的绘制缓存中。如果一个片元在渲染管线中未能通过某个步骤,那么它就不会最终被写入绘制缓存。

我们主要介绍一下在本文中使用到的模板缓存、深度缓存:

  • 模板缓存可以用来控制颜色缓冲在某个位置写入操作。
  • 深度缓存存储的的是片元距离相机的远近的值,控制片元之间的遮挡关系。

在 ThreeJS 中,控制模板缓存的参数由以下几个:

stencilWrite: 是否打开模板测试,默认 false
stencilWriteMask: 写入模板值的掩码,默认 0xff
stencilFunc: 进行模板测试的函数。THREE.AlwaysStencilFunc
stencilRef: 模板测试的参考值,默认 0
stencilFuncMask: 与模板缓冲区比较时的掩码,默认 0xff
stencilFail: 模板测试失败,接下来要执行的操作
stencilZFail: 模板测试成功,深度测试失败,接下来要执行的操作
stencilZPass: 模板测试和深度测试都成功,接下来要执行的操作

深度缓存的控制有:

depthFunc:使用哪种深度比较函数
depthTest:是否开启深度测试
depthWrite:是否写入深度缓存

如何利用 WebGL 渲染管线解决地图渲染的问题

通过模板测试解决重复渲染问题

我们以跨瓦片的建筑为例来介绍一下如何解决重复渲染问题。

思路: 我们需要一个状态来记录一下某个位置的建筑是否已经被渲染了;当建筑数据被渲染时,根据这个状态来判断如果这个位置没有被渲染了建筑数据,那么这次渲染就能通过渲染管线,最终被写入到绘制缓存,并修改当前的状态值,记录当前位置已被了渲染建筑数据;反之,那么这次渲染就不会通过 WebGL 渲染流水线,最终也不会被写入到绘制缓存。

从 WebGL 渲染管线的介绍中,可以发现,模板缓存正是可以控制片元是否能够通过渲染管线的,并能在通过渲染管线之后记录一下状态。

于是,在 3D 地图渲染中,设置了如下的模板缓存配置:

stencilWrite: true
stencilRef: 1
stencilFunc: THREE.GreaterStencilFunc // 参考值大于模板值时通过模板测试
stencilZPass: THREE.ReplaceStencilOp // 模板测试和深度测试都成功时,将模板缓存替换为参考值

当第一次绘制建筑时,使用参考值 1 与默认的模板缓存模板值 0 使用 stencilFunc 进行比较,1 > 0,符合这个模板测试的条件,所以能够通过模板测试;能够通过深度测试的,执行 stencilZPass 设定的操作,将模板缓存的值使用参考值 1 替换;此时,有建筑的区域的模板缓存的值为 1。当此建筑区域有重复数据进行第二次绘制时,使用参考值 1 与模板缓存的值根据 stencilFunc 进行比较,由于第一次绘制建筑时,此区域的模板缓存值已被替换为 1,所以参考值 1 并不大于已有模板值 1,所以不能通过模板测试,重复绘制的片元就被舍弃掉了,不再参与渲染流水线接下来的处理。

通过深度测试解决侧面与顶面之间的遮挡问题

image (6).png

之所以在去重渲染之后建筑会产生图中的侧面被穿透了,是因为先渲染了下方四周包围着的建筑,然后再渲染了中间的建筑,导致四周已绘制的建筑区域的模板缓存值被设置为了 1;当绘制上方的建筑时,上方建筑无法通过模板测试。

解决方案:先写入深度缓存,不写颜色缓存;然后在按照上面“通过模板测试解决重复渲染问题”的步骤执行一次。

设置深度缓存配置如下:

colorWrite: false // 不写入颜色缓存
depthWrite: true // 写入深度缓存

深度缓存写入正确之后,当执行“通过模板测试解决重复渲染问题”进行绘制时,先绘制下方四周的建筑时,由于深度缓存已经有了上方建筑的深度信息,所以上方与下方四周建筑重叠部分的片元不能通过深度测试;根据 stencilZPass 的设定,当只有通过深度测试时,才用模板参考值替换模板缓存的值,所以此时模板缓存的值仍然保持默认值不变。当绘制上方建筑时,重叠区域的值为 0,上方建筑的参考值为 1,根据 stencilFunc 的设定,片元能通过模板测试,所以上方建筑的片元能被写入绘制缓存。

最终的效果如下: image (7).png

WebGL 渲染管线的其他用途

除了使用渲染管线解决地图数据重叠问题之外,还可以实现渲染地图的指定区域渲染。例如在一些场景中,我们的用户希望地图只渲染他关注的行政区,范围外的希望能够裁减掉。在使用模板测试前,是盖了一层蒙层实现的。

全部渲染:
图片3.png

按照给定区域渲染:
图片4.png

如果在切割地图瓦片时进行数据的过滤,会产生很大的计算量,并且对于不同的区域,都需要切割一次;如果在前端进行数据过滤,计算量同样会很大。所以通过过滤对地图瓦片数据进行处理是不现实的。模板缓存可以通过设置条件来控制片元能否通过渲染管线,所以可以通过渲染来解决这个问题。

下面来具体介绍一下实现过程:

模板缓冲值示意图:
图片6.png

  1. 屏幕上未绘制任何图形时,默认的模板缓冲值为 0;
  2. 然后设定如下的配置,绘制一个杭州区域的 geojson 多边形;这个 geojson 的作用只是为了确定杭州区域的片元应该通过渲染管线,最终写入到绘制缓存;所以 geojson 不需要写入深度缓冲与写入颜色缓冲,只需要按照模板参数设置将默认模板值替换为 2 ;
stencilWrite: true
stencilRef: 2
stencilZPass: THREE.ReplaceStencilOp


colorWrite: false
depthWrite: false
  1. 最后按照如下设置绘制地图瓦片数据;根据 stencilFunc,只有片元的参考值小于模板值才能通过模板测试,所以杭州 geojson 绘制区域外的片元是无法通过模板测试的,只有 geojson 内部的片元才能通过模板测试。
  stencilWrite: true
  stencilRef: 1
  stencilFunc: THREE.LessStencilFunc