cesium抗锯齿像素线绘制

阅读本文需要一些cesium的基础知识,关于cesium相关基础API不会做过多介绍,本文介绍只涉及cesium中抗锯齿像素线的绘制算法。

前言

cesium中提供了相当多类型线段的绘制,比如像素线、米级线、虚线、箭头线等。

image.png image.png image.png image.png

以上箭头基本可以满足大部分业务场景,但也有些业务场景是无法满足的,其原因主要有以下几点:

  1. 它们appearance不一致,无法创建一个primitive,渲染性能方面也就无法极致优化
  2. cesium线段本身并没有做抗锯齿处理,渲染效果不是很理想
  3. 箭头线样式不满足大多数地图业务场景
  4. cesium线段不支持outline
  5. 虚线部分业务场景不需要随拖动地图时移动

由于以上种种缺陷,就会导致在一些地图项目中必须得自己写相关的自定义primitive,才能完成相关功能,比如以下业务场景,mapbox中的方向箭头线段:

image.png 如果要实现以上效果,我们先拆解需求:

  1. 像素宽度线段,并且支持反锯齿优化。
  2. 线段中箭头纹理贴图,并需要随层级变化保持箭头直接箭头间距

读过mapbox源码会发现,实现以上效果mapbox线段和箭头是单独分为两个图层渲染,分别是line和symbol图层,line图层只渲染线段,而text或者icon则是在symbol图层中绘制,并且随着zoomLevel变化实时计算单个symbol的位置和方向,再通过更改传入着色器的atrribute来达到这种效果。

但是在cesium中我们需要箭头和线段共用一个appearance,这样就能直接对geometry进行批处理,合并成一个primitive渲染,这样性能会提升非常多倍在实际项目中。本文以此为前提,首先在cesium中实现抗锯齿像素线段,下一篇再对道路箭头的实现进行相关讲解。

以下内容需要一定的webgl知识,如有对着色器相关语法不理解支持,建议先学习完着色器相关语法。另外文章中着色器代码片段只列举了关键的算法内容部分。

像素线段算法实现原理

目前实际像素线段实现网上的算法实现文章已经很多了,包括开源库THREE.MeshLine的实现也非常优秀,本文算法实现与其一致,只是对其实现思路进行详细讲解,对于自己来说也是一次复盘。

像素线绘制最核心的思想就是利用三角形来绘制线,将一根有像素宽度的线,看成是多个三角形拼接的结果,如下所示:

image.png

根据路径数据P0、P1、P2,我们暂且分为两组,P0,P1P1,P2,由P0P1以及P1P2两方向单位向量,分别得到其上下两单位法向量,这样单位法向量乘以代表线宽度的标量则可以求得其外扩的两点,也就是P0A,P0B,P1A,P1B以及P1C,P1D,P2A,P2B一共八个点,一共可以构建四个三角形。但是很显然,P1附近会生成4个点,如果按照四个三角形的方式绘制,拐角部分肯定会出现空缺的情况,这样的渲染效果明显不符合业务需求,所以P1处(也就是中间点)这种情况也就需要特殊处理来。

image.png

拐弯处算法我配合如下图示讲解,首先分别取得current - last以及next - current的单位向量,两向量相加便可以得到如图所示Avg.Normal单位向量,得到这个向量其上下单位法向量便很容易可以轻松得到,拿到这两个法向量便可以取到拐角处两点了。

image.png

这里有个地方需要注意,拐角处单位法向量此刻已经知道,但是需要相乘的宽度标量却不能直接已线宽为准,这里看图便可知其宽度是要比线宽长的,这个地方便需要用到点积来计算,如下图所示,此时我们是已知normal1、normal2,取他们的点积也就是得到其夹角的cos,也就是 d/d2 = cos,此时已知d也就是线宽,则d2也就很容易的就能求出来了。

image.png

这样关于像素线的算法原理也就讲解到这里,知道怎么求解的方法后面写代码就是水到渠成的事情了。

attribute vec3 position3DHigh;
attribute vec3 position3DLow;
attribute vec3 nextPosition3DHigh;
attribute vec3 nextPosition3DLow;
attribute vec3 prePosition3DHigh;
attribute vec3 prePosition3DLow;

void main() {
  // 相对于相机的坐标
  vec4 position = czm_computePosition();
  vec4 previous = czm_computePrePosition();
  vec4 next = czm_computeNextPosition();
  
  // 屏幕坐标
  vec4 positionWindow = czm_eyeToWindowCoordinates(positionEC);
  vec4 previousWindow = czm_eyeToWindowCoordinates(prevEC);
  vec4 nextWindow = czm_eyeToWindowCoordinates(nextEC);
  
  // prev
  vec2 directionToPrevWC = positionWindow.xy - previousWindow.xy;
  // next
  vec2 directionToNextWC = nextWindow.xy - positionWindow.xy;
  
  vec2 normalDirection = vec2(0.0, 0.0);
  float meterScaler = width;
  if(abs(positionWindow.x - previousWindow.x) == 0. && abs(positionWindow.y - previousWindow.y) == 0.) { // 第一个点
     directionToPrevWC = normalize(directionToPrevWC); // 取得单位向量
     normalDirection = vec2(-directionToPrevWC.y, directionToPrevWC.x); // 取法向量
  } else if (abs(positionWindow.x - nextWindow.x) == 0. && abs(positionWindow.y - nextWindow.y) == 0.) { // 最后一个点
     directionToNextWC = normalize(directionToNextWC);
     normalDirection = vec2(-directionToNextWC.y, directionToNextWC.x);
  } else { // 中间点
     directionToNextWC = normalize(directionToNextWC); // next方向向量
     directionToPrevWC = normalize(directionToPrevWC); // last方向向量
     vec2 lastNormal = vec2(-directionToPrevWC.y, directionToPrevWC.x); // 取last法向量
     vec2 normalAvg = directionToPrevWC + directionToNextWC; // Avg向量
     normalDirection = normalize(vec2(-normalAvg.y, normalAvg.x)); // 拐角处法向量
     meterScaler = width / dot(normalDirection, lastNormal); // 取拐角长度
  }
  
   vec2 offset = normalDirection * direction * meterScaler * czm_pixelRatio * positionWindow.w;
   vec2 windowCoords = positionWindow.xy + offset;
   vec4 translatePosition = vec4(windowCoords.x, windowCoords.y, -positionWindow.z, positionWindow.w);
   gl_Position = czm_viewportOrthographic * translatePosition;
}
复制代码

如此像素宽度线段便可以绘制出来,如下图所示:

image.png

像素线段抗锯齿的实现

但是由上图可以发现线段的锯齿很严重,这种渲染效果很不理想,接下来便需要继续对锯齿进行处理了,在刚开始这个需求的时候,我最先参考的是这篇博客中的方案:在 WebGL 中绘制直线,核心部分也就是如下着色器代码:

varying vec2 v_normal; // 每个顶点的法向量

float blur = 1. - smoothstep(0.98, 1., length(v_normal)); // 根据法向量的长度让其在 0.98 - 1.0直接做平滑模糊处理 (此算法像素线宽度较小时缺陷比较明显)
gl_FragColor = v_color;
gl_FragColor.a *= blur;
复制代码

我们先看看效果

image.png

效果相比上面却是好了一些,但是仍然存在些许锯齿感,并且在像素宽度很小的时候渲染抗锯齿效果极其不明显,这也是上面的算法缺陷所在,如下图所示:

image.png

所以该方案很明显被pass了,接下来在阅读了多种优秀开源项目的源码之后,发现目前主流对线段进行抗锯齿处理的方案有两种,一种增加顶点数量,这种方案在上面那片博客中也有提到,也就是mapbox早期的方案,第二种则还是依赖法向量进行模糊处理的方案,也就是如今mapbox的方案,最后进行了反复思考对比,最终选定了现如今mapbox的抗锯齿方案,首先其不需要生成多的顶点数,对性能影响较少,其次在当前代码上修改更为容易。接下来就来介绍此种线段的抗锯齿方案:

varying vec2 v_normal; // 每个顶点的法向量

float dist = length(v_normal) * lineHalfWidth; // 计算出像素到线的距离(以像素为单位)
float blur = clamp(lineHalfWidth - dist, 0., 1.); // 算出blur值,该算法巧妙之处在于此,像素距离线中间越近则dist越小,所以blur越大,像素越靠近线边缘dist越大所以blur越小
gl_FragColor = v_color;
gl_FragColor.a *= blur;
复制代码

再试试优化抗锯齿后的渲染效果,如下图所示发现不管线段的粗细都能有非常不错的抗锯齿处理了。 image.png

image.png

总结

至此我们已经能够在cesium中实现出具有出色性能的抗锯齿像素宽度线段,可以发现其实在webgl中看似复杂的需求实际其实大多都是由几段算法数学公式实现的,所谓事学好数学事半功倍真是不假。

后面关于实现outline、虚线以及方向线段的教程后面有时间再一一介绍。相信掌握这些知识,在webgl中关于线段的需求都可以不用再惧怕了吧!

Guess you like

Origin juejin.im/post/7063051505355456548