在 WebGL 中渲染 Polyline(一)
CesiumJS 在渲染 Polyline 这事儿上下了不少功夫。纵观 WebGL 发展的这十几年,对 Polyline 的渲染已经有很多资料,然而 CesiumJS 却停留在当年的效果,对斜接接头过于锐角的情况、抗锯齿的优化都没有改进,这点是比较遗憾的。
3D模型在线预览提供多种低代码平台3D模型在线预览解决方案,实现了将多种3D模型格式无缝集成到低代码业务表单中。这意味着用户可以在不离开低代码平台的情况下,直接查看和操作3D模型,极大地提升了数据可视化的效果和用户交互体验。
如果使用 WebGL 原生线类型渲染
使用 WebGL 原生的 LINES
、LINE_STRIP
或 LINE_LOOP
渲染线会有很多问题,例如,如果你的机器运行的 WebGL 底层是 ANGLE,那么最大线宽(像素)只能是 1.
还有,在 ANGLE 为底层的 WebGL 下,在公共顶点处是没有拐弯效果的,如下图所示:
使用「miter」接头类型绘制的线应该是这样的:
哦对,上面这俩图是有线的外边线效果的,如果没有「z-fighting」的前提下,要实现外边线,那么要使用模板缓冲技术渲染三次。
Cesium的做法
绘制折线,Cesium 官方团队使用的方法是为每段线段绘制一个面朝屏幕的四边形。
WebGL 没有几何着色器(Geometry Shader),我们只能把折线的顶点复制一遍,然后在顶点着色器里用宽度值来偏移顶点,形成屏幕坐标系的四边形。
为了偏移顶点,首先要知道左右顶点的位置。因为没有几何着色器,所以需要额外的顶点属性(VertexAttribute)来告诉顶点着色器相邻顶点。
一旦有相邻的顶点坐标,就能在 VS 中计算出前后线段的方向,进而知道线段的垂直方向,就可以沿着垂直于线段的方向偏移当前被重复的两个顶点坐标。
对折线的所有顶点进行如上所述的计算、渲染后,就能得到文章开头使用线宽绘制的线段效果了。
到此,虽然具备了宽度,但是在顶点处仍然没有拐弯效果,即文章开头的图1。想要达到图2的效果,要把 VS 中顶点计算转换到屏幕空间中,就不能在拐点处沿着线段的法线方向偏移,而是要沿着屏幕坐标系的前后两个向量和的方向进行偏移。这些向量加减等需要一些线性代数、三角函数等简单知识,后面会讲。
CesiumJS 会把顶点属性保持 8 个以内(因为 WebGL 1.0 时,规定顶点属性最大个数最少要是 8 个),以保证跨设备 WebGL 的兼容性。
对于每个顶点坐标,就至少需要 4 个 vec3
—— 其中,2 个 vec3
用于表示 3D 坐标,2 个则用来表示 2D 的。这样的用法 CesiumJS 特有的。之所以需要用两个 vec3
来表示一个 3D 位置,是因为单个 vec3
在顶点着色器中不足以表达双精度浮点(vec3 是单精度浮点),容易在距离较近时发生坐标抖动问题。
如果再加上每个顶点的前后两个顶点数据,在顶点着色器里就需要 12 个顶点属性,这就有可能在一些平台中发生问题(譬如这个平台对 WebGL 的实现就只支持最多 8 个顶点属性)。
将顶点属性数量限制在 8 个之后,有一个解法,那就是只向顶点着色器传递 4 个顶点坐标属性,以及到相邻顶点的方向,CesiumJS 需要 3D 和 2D 的方向向量,减去 4 个顶点坐标属性后,还有 4 个顶点属性就可以分配给 4 个 vec3
来表示方向。
但是,这还可以进一步优化。
Cesium 团队希望在顶点属性中传递更多信息,例如纹理坐标、线宽等,那么就需要使用一种技术进一步减少或者说压缩顶点属性,最终只需要 2 个 vec4
即可表示方向。这种技术叫作「压缩单位向量」,在后面的章节会有详细实现。
但是,在压缩法线向量时,遇到了精度问题,当接近折线端点且距离另一个相邻端点超过 9w 米的时候,端点的平移会发生抖动:
有一种办法是对超长距离的端点之间进行增稠细分。但是这个办法对于下图所示的 Geoeye 1 和 ISS 之间的连接线来说,从 2 个顶点增加到了 50 个顶点。这就带来数据的臃肿问题了。
所以,Cesium 官方团队决定使用 12 个顶点属性,而不是增稠顶点数量。
回到顶点着色器,在屏幕坐标系下对顶点坐标进行沿着向量偏移后,要把偏移后的屏幕坐标转回顶点着色器应喂给下一步用的裁剪坐标系下。
屏幕坐标系下偏移完的坐标值应是一个 vec4
,它具有 x、y、-z 和恒为 1.0 的 w。之所以使用 -z,是因为视空间(相机坐标系)之后的空间(包括它自己)中,视线方向都沿着 -z 轴。w 分量用于进行透视除法,使得物体有近大远小的效果。w 分量设为 1.0 是因为希望无论视角咋样,线的宽度都保持不变,即放弃近大远小。
在上面的计算中,顶点着色器是先一步到位在屏幕坐标系(也就是完成了视口变换)下进行了计算,然后再反转视口变换、透视除法等步骤还原回顶点着色器本该往下一阶段传的裁剪坐标。
但是,把 w 设为 1.0,放弃近大远小又会产生另一个问题。当折线有一部分在近平面之外、一部分在近平面之内,也就是相交时,如下图所示:
调整相机往前一点,这根线就迅速变成这样:
很容易发现右边的线突然从下面指向屏幕的上面,很突兀。
为了解决这个问题,需要在反算到裁剪坐标之前,把线裁剪到近平面,具体而言,就是把近平面之外(Frustum 之外)的部分切断,保留与近平面的交点。举例,如下图所示应该怎么办?
左图,两条线段共享蓝色圆圈处的顶点,顶点在近平面之外。这两根红色线段与近平面相交于绿色圆圈处。那么,应该把蓝色顶点裁剪到哪一个绿色圆圈的交点处呢?Cesium 团队已经通过算法实现了这个问题,即平移共享点到近平面。
线段的共享顶点,也就是图中蓝色圈处的顶点,其实是有相同的 4 份的,前后的线段都持有 2 份,用于平移。所以同理的,Polyline 的起始端点则复制了 2 次。
除此之外,还需要知道这个顶点坐标属于哪根线段,才能把它裁剪正确。
右图是另一个情况,如果有两个蓝色圆圈处的顶点被甩在了近平面之外,也就是说有完整的一根线段被 Frustum 剔除、裁剪,那么啥都不用做,让 WebGL 把这根在近平面之外的线段完全裁剪掉即可,压根就没什么问题。
在 WebGL 中渲染 Polyline - 索引