关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期

关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期   作者:J


大多数 GPU 只支持32位浮点数,因此在 GLSL(OpenGL Shading Language) 中使用 32bit 浮点数,当计算较大数值时会出现精度丢失的问题。


矩阵精度丢失问题和解决


在地图场景中,道路、绿地、水系这些元素的坐标是相对于一个网格的,因此坐标基本在0到256之间,在进行坐标转换时,程序将相对于网格的坐标转换为相对于屏幕中心点的坐标,每个网格的视图模型变换矩阵都要实时计算。在该矩阵中,位移的距离都比较小,因此不会出现精度丢失的问题。而另一类元素,比如文字、标注等,这类元素使用的坐标是相对于整个地图平面坐标原点的像素坐标,随着地图级别的放大,像素坐标值也跟随一起放大。 


关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期_第1张图片


如果不对精度丢失问题做处理,那么当放大到达一定级别后,移动地图时就会发现这些文字、标注会出现抖动。其本质原因是文字元素的视图模型变换矩阵的位移值较大,在 shader 中计算时会有精度的丢失,从而出现抖动。


不同的解决方案对比


一、提前用 JavaScript 计算好相对于屏幕中心点的坐标,再交给 shader 计算


这个方案能够解决精度丢失问题,但它的坏处是增加了 CPU 的开销,而且只要地图位置、级别发生变化,所有元素的屏幕坐标都会随之更改,因此要求在每一帧渲染前都要同步计算好屏幕坐标。随着添加到地图上的标注点的数量增多,计算量也随之增加,这会影响地图的渲染性能,因此该方案不太可行。


二、划分子坐标系


既然在一个坐标系中坐标值较大,那么在一个大坐标系下划分出若干小坐标系,当大于某个级别时使用这些小坐标系,那么每个元素的坐标值就能够变小。


关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期_第2张图片

坐标值变小后,矩阵中表示位移部分的值也随之降低,因此可以解决精度丢失问题。


这种解决方案避免了方案一需要频繁计算坐标的问题,只需要在一定级别下更换一下原始坐标值即可。但这个方案也不是十全十美,起初地图只显示到19级,这个方案没有问题,但是如果想继续扩大地图显示级别,比如到21或更高,那么使用之前划分的小坐标系的精度仍然不够,因此还要继续细分。虽然理论上可行,但是增加了程序复杂度,需要程序判断每个元素坐标落在哪个子坐标系中,以便选取对应的坐标值,在地图级别变化过程中也要变换使用不同的坐标。


三、采用相对于视点的坐标系,并在 shader 中计算


该方法与方法一类似,唯一不同之处是将元素坐标统一转换成相对于视点(Relative to Eye,RTE)位置的坐标。




为了避免在 CPU 频繁计算相对坐标,我们将计算过程迁移到 GPU 当中,同时使用两个 32bit 浮点数来表示一个 64bit 浮点数。转换方法如下:

function doubleToTwoFloats(value) {
   var high;
   var low;
   if (value >= 0) {
       tempHigh = Math.floor(value / 65536) * 65536;
       high = tempHigh;
       low = value - tempHigh;
   } else {
       tempHigh = Math.floor(-value / 65536) * 65536;
       high = -tempHigh;
       low = value + tempHigh;
   }
   return [high, low];
}


上面的函数将数值中超过 65536 的部分用一个 32bit 数表示,其余部分用另一个 32bit 数表示。


计算相对坐标公式如下:




在 shader 中需要额外增加两个 uniform 分别存储视点位置的 high 和 low 部分,在元素的 vertex 数据中每个坐标值也分别用两个数值描述 high 和 low 部分。最终每个元素的坐标值仍然是原始的坐标,不会根据地图状态变化而变。


Z缓冲区精度丢失问题和解决


Z-Fighting 一词对于做图形开发的人一定不陌生,它表示两个物体在渲染时无法确定遮挡关系,物体A时而遮挡物体B时而被物体B遮挡。在地图渲染中也有类似问题发生:


关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期_第3张图片


在上图中,当地图倾斜的时候远端的绿地与区域面产生 z-fighting。大多数系统中深度缓冲区(z 缓冲区)为 24 位,相比 32 位更容易出现精度丢失问题,那么为什么又会在远端出现呢?深度缓冲区所存储的是经过一系列变换之后的 z 深度(具体可参考相关图形学的书籍,变换过程不在这里赘述),它并非均匀分布在近平面和远平面之间,因为它与观察空间的 z 坐标是倒数关系,因此深度缓冲的大部分精确度集中在靠近近平面的地方。


地图是由多层不同的元素叠加绘制而成,不同元素之间有严格的遮挡关系。我们在绘制的时候首先将元素进行归类,某一类的元素之间遵循一定的压盖关系。另外同一类的元素之间也有压盖关系。下面的表格列举了地图上主要的元素:


关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期_第4张图片


首先,标注一定覆盖其他元素,因此标注在最后绘制,并关闭深度检测。


在绘制不同大类的元素时,程序通过调用 gl.depthRange 方法来给深度值做分层。比如底层面统一使用 gl.depthRange(0, 0.2),道路使用 gl.depthRange(0.2, 0.4)。这如同给每一大类的元素增加了一个容器,使其 z 缓冲值限定在一定的范围,不会对其他类别元素造成影响。


如果多个层次的元素想一次绘制,gl.depthRange 方法就不行了。这种情况下,可以在 vertex 数据中增加一个表示层次的数据,在 shader 计算时可以将剪裁坐标的 z 改写:

gl_Position = u_proj_matrix * u_mv_matrix * vec4(a_pos, 0, 1);
gl_Position.z -= a_layer_index;


a_layer_index 表示元素的层次索引,索引值高的元素会覆盖索引值低的元素,我们将 z 统一减去这个索引值。这样 z 的取值完全可以自行控制,也能够避免精度丢失问题。注意剪裁坐标的 z 符号也发生了变化,因此使用减法,另外也要确保经过计算后 z 值也仍然在剪裁空间范围内。


JS计算精度丢失问题和解决


有时需要通过 JavaScript 进行坐标转换,比如屏幕坐标和元素原有坐标的互换,一般的矩阵函数库默认使用 Float32Array 来表示矩阵,这也会导致精度丢失。需要对这些函数库进行一定改写,使其支持 Float64Array,这样能够解决 JS 计算精度丢失问题。


小结


  • 元素数量不多时,可以转换为相对屏幕的坐标,但是元素过多会导致 CPU 开销增大,也不利于缓存。

  • 将 CPU 计算迁移到 GPU,用两个 32bit 浮点数代表一个 64bit 浮点数进行近似计算。

  • 通过 gl.depthRange 方法限制 z 缓冲的取值范围。

  • 改写 shader 中剪裁坐标的 z 值。

  • JS 计算中使用 Float64Array 进行矩阵计算。


           关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期_第5张图片

你可能感兴趣的:(关于 shader 计算精度丢失问题的解决|WebGL地图引擎系列第四期)