一、文章来源
Robust Polyline Rendering with WebGL – Cesium
二、翻译
2.1、使用线图元渲染
在使用线条图元(LINES、LINE_STRIP或LINE_LOOP)渲染线条时,我们遇到了一些问题。首先,使用ANGLE时的最大线宽为1.0。其次,使用lineWidth(不使用ANGLE)绘制直线图元不会在共享顶点处连接线段:
下图显示了两个斜接的连接线:
[图片上传中...(image.png-146149-1675392780803-0)]
最后,我们绘制轮廓线的方法是使用三个通道和模板缓冲区,而不使用z轴冲突。
2.2、我们的办法
我们绘制多段线的新方法是为每条线段绘制屏幕对齐的四边形。在WebGL中,我们没有几何体着色器,因此我们复制线的每个位置,并在顶点着色器的屏幕空间中挤出它们。要挤出位置,我们需要知道相邻顶点的位置。同样,我们没有几何体着色器,因此需要为相邻位置信息创建其他顶点属性。
一旦我们有了相邻的顶点位置和位于同一位置的两个顶点,我们就可以在垂直于直线的方向上挤出这些位置。如果对所有顶点位置都这样做,我们将得到类似于使用lineWidth绘制直线图元的结果,而上面没有ANGLE图像。这对于多段线的端点很好,但我们希望连接多段线线段连接的点。若要找到挤出位置的方向,请将所有位置转换为窗口坐标,并找到从相邻位置指向顶点位置的两个矢量之和的一半。在该方向上移动顶点位置的量将涉及一些三角,稍后将在文章中讨论。
起初,我们试图将顶点属性的数量保持在8以下,因为这是支持WebGL所需的最小值。对于每个位置,我们需要四个向量。两个用于3D中的位置,两个用于2D中的位置。铯在两种不同模式下的位置需求是独一无二的。我们需要两个向量用于单个3D位置的原因是在顶点着色器中模拟了双倍精度。有关该主题的更多信息,请参见 Precisions, Precisions. 要包括每个顶点位置加上两个相邻顶点位置,我们至少需要12个顶点属性。保持顶点属性数为8的一种方法是仅使用从当前顶点到相邻顶点的方向。我们需要3D和2D的方向。这仍然给我们留下了四个方向向量和总共8个顶点属性。我们希望在其他顶点属性中包含更多信息,如纹理坐标、宽度等。我们可以通过压缩单位向量方向来进一步减少属性的数量。这将把我们带到两个向量的方向。压缩它们的方法将在后面的文章中描述。
我们在使用压缩法线时遇到了精度问题。当放大到一个多段线端点附近时,该多段线的另一个端点位于大约9万米之外,拉伸位置将抖动,如下图所示:
一种解决方案是细分线;然而,对于下图所示 the Geoeye 1和 the ISS之间的接入线,位置数量可以是2个到大约50个。
我们决定对当前顶点位置和相邻位置使用12个顶点属性,而不是为一条简单接入线添加那么多额外的点。
在挤出屏幕空间中的位置后,我们需要将位置转换为顶点着色器输出的裁剪坐标。屏幕空间中的最终位置是修改后的x和y坐标、负z坐标和1.0的w坐标的平均值。我们使用负z坐标,因为在眼睛空间之后(包括眼睛空间)的每个空间中,视图方向都沿着负z轴。w坐标用于透视分割,使离眼睛更远的对象看起来更小。我们将w坐标设置为1.0,因为我们希望宽度保持恒定,而不考虑透视。当直线与近平面相交时,这会导致问题。然后,我们反转视口变换的操作,以获得剪辑空间中的最终顶点着色器输出位置。下面显示了与近平面相交之前的一条线:
下一幅图像显示了放大后与近平面相交的同一条线:
有关发生这种情况的更多信息,请参见Clipping using homogeneous coordinates.
为了解决这个问题,我们需要在转换到屏幕空间之前将线裁剪到近平面。如果出现下图所示的情况,该怎么办
在左侧,两条线段应该被近平面剪裁,并且它们共享相同的顶点位置。线段在绿色圆中的点处与近平面相交。需要剪裁的位置用蓝色圆圈表示。我们应该将蓝色位置裁剪到哪个绿色位置?两条线段共享的每个顶点都需要再次复制。对于每条连接的线,每个位置在端点复制两次,在共享位置复制四次。我们还需要知道该位置属于哪个线段,并将其沿正确的方向剪裁。
一旦我们处理了左边的情况,右边的情况就更容易处理了。如果顶点位置属于被剔除的线,只需输出位于近平面后面的裁剪坐标,以确保不绘制该线。
2.3、顶点着色器细节
2.3.1、在屏幕空间挤压
下图显示了一条用黑色绘制的线,同一条线在红色轮廓的屏幕空间中被拉伸到更大的宽度。要在端点处拉伸线,只需在线段的法线方向上将顶点移动所需的像素数。
下图显示了上图中由橙色虚线包围的区域的特写。让绿色向量为u,黑色向量为v。我们想找到向量u。
我们知道v的方向,因为它是到直线上下一点或上一点的方向。我们可以找到u的方向,因为它位于直线上下一点和上一点之间的中间位置。现在,我们只需要求出u的大小,从三角恒等式,我们知道它是||u||=width/sin(a)。我们知道,对于任何向量p和q,||pxq||=||p||||q|||sin(a)|。如果我们将uê和vê视为xy平面上的三维向量,我们可以将uê和vê代入表达式中。由于uÜ和vÜ是单位向量,z坐标为0.0,因此最后一个表达式可以简化为sin(a)=|uÜ.xvÜ.y-uÜ.yvû.x|。GLSL代码如下:
float sinAngle = abs(u.x * v.y - u.y * v.x);
width /= sinAngle;
vec2 offset = direction * directionSign * width * czm_highResolutionSnapScale;
gl_Position = czm_viewportOrthographic * vec4(positionWC.xy + offset, -positionWC.z, 1.0);
必须注意确保sinAngle不接近零,并且我们在正确的方向上移动顶点(在我们的示例中为u或-u)。
2.3.2、裁剪到近平面
如上所述,我们需要将线段的端点剪裁到近平面。以下是GLSL函数,用于在眼睛坐标中将点剪切到近平面:
void clipLineSegmentToNearPlane(
vec3 p0,
vec3 p1,
out vec4 positionWC,
out bool culledByNearPlane)
{
culledByNearPlane = false;
vec3 p1ToP0 = p1 - p0;
float magnitude = length(p1ToP0);
vec3 direction = normalize(p1ToP0);
float endPoint0Distance = -(czm_currentFrustum.x + p0.z);
float denominator = -direction.z;
if (endPoint0Distance < 0.0 && abs(denominator) < czm_epsilon7)
{
// the line segment is parallel to and behind
// the near plane
culledByNearPlane = true;
}
else if (endPoint0Distance < 0.0 && abs(denominator) > czm_epsilon7)
{
// ray-plane intersection:
// t = (-plane distance - dot(plane normal, ray origin))
// t /= dot(plane normal, ray direction);
float t = (czm_currentFrustum.x + p0.z) / denominator;
if (t < 0.0 || t > magnitude)
{
// the segment intersects the near plane,
// but the entire segment is behind the
// near plane
culledByNearPlane = true;
}
else
{
// segment intersects the near plane,
// find intersection
p0 = p0 + t * direction;
}
}
positionWC = czm_eyeToWindowCoordinates(vec4(p0, 1.0));
}
函数中的注释描述了剪裁或剔除时的条件。基于近平面法线为vec3(0.0,0.0,-1.0)且距离原点为czm_currentFrustum.x的假设,该函数已被简化。
2.3.2、编码/解码单位向量
当我们将自己限制为8个顶点属性时,我们使用 Spheremap Transform.对指向线上上一点和下一点的两个单位向量进行了编码。这允许我们将两个vec3顶点属性压缩为一个vec4顶点属性。以下是用于将单位向量压缩为两个组件的Javascript代码:
function encode(cartesian) {
var p = Math.sqrt(cartesian.z * 8.0 + 8.0);
var result = new Cartesian2();
result.x = cartesian.x / p + 0.5;
result.y = cartesian.y / p + 0.5;
return result;
}
这里是用于解压缩顶点着色器中的单位向量的GLSL代码:
vec3 decode(vec2 enc)
{
vec2 fenc = enc * 4.0 - 2.0;
float f = dot(fenc, fenc);
float g = sqrt(1.0 - f / 4.0);
vec3 n;
n.xy = fenc * g;
n.z = 1.0 - f / 2.0;
return n;
}
有关压缩单位向量的不同方法的更多信息,请参阅Compact Normal Storage for Small G-Buffers. 。