WebGL绘制有端头的线

  关于WebGL绘制线原理不明白的小伙伴,可以看看我之前的文章WebGL绘制有宽度的线。这一篇我们主要来介绍端头的绘制,先看效果图。

  端头一般被称为lineCap,主要有以下三种形式:

  butt最简单等于没有端头,square一般是多出lineWidth/2的长度,round是一个以lineWidth/2为半径的圆。一般情况下绘制lineCap的思路都是添加额外的三角形,如一些开元库或者mapbox的方法,一般来说mapbox的方法已经可以了,但是我还是感觉顶点太多,甚至对square的情况都不愿意在增加两个顶点。

  方法总是自己去探索的,在绘制宽度线中,我已经总结了自己的一套理论,将线距离映射成uv坐标的思路来绘制一些线的效果,那么在这里仍然沿着这种思路去思考。

  对于square来说只要在绘制顶点的时候,在线的开头和结尾分别做一定的偏移即可。对于round我们可以在suqare的基础上,在片元着色器中做一些判断,距离中心点距离大于lineWidth/2的像素全部抛弃掉。

  

  所以对于线起点和终点处顶点的位置在上一篇文章的基础上,做了沿着线方向的偏移

// flag -1代表线起点, 1代表线终点;同时为了防止一些精度问题,同样使用纹理值来做一些判断,让真正的起点和终点做偏移,防止其中的一些小碎线段影响结果
'    vec2 vertical_offset = normal * lineWidth * realSide * 0.5;',
'    vec2 horizontal_offset = vec2(0.0, 0.0);',
'    if (flag == -1.0 && (uv.x == 0.0 || uv.x == 1.0)) {',
'        horizontal_offset = -dir * lineWidth * 0.5;',
'    } else if (flag == 1.0 && (uv.x == 0.0 || uv.x == 1.0)) {',
'        horizontal_offset = dir * lineWidth * 0.5;',
'    }',
'    if (useLineCap <= 0.0) {',
'        horizontal_offset.xy = vec2(0.0, 0.0);',
'        vUV.x = uv.x;',
'    }',
'    vec2 pos = currentP + vertical_offset + horizontal_offset;',
'    gl_Position = unproject(pos, finalPosition.z, finalPosition.w);',

  这样的话我们就能绘制出square类型的lineCap。但是对于round仅仅这样做还不行,还需要在片元着色器中根据中心点做一次剔除。那么问题来了,经过偏移后如何知道圆心点的位置,在来根据像素距离来进行剔除。这时候就轮到上篇文章利用纹理坐标来表示线长度的思路了,上一篇中我们把路线长度映射成从0-1的uv坐标,那么对于端头来说,我们可以把超出的那一半线宽的像素映射成纹理坐标,可以想象这部分长度对于起点来说等于负的线长度,对应的纹理坐标就是负的纹理坐标;对于终点来说多出的这部分像素等于增加了的线长度,那么对应纹理坐标就是超过1的部分。那么接下来的问题就是如何把线上的像素长度对应于线的长度等于把像素与3d世界中的单位进行映射。

  如果对投影矩阵不是很了解的同学,最好看看我的这篇文章webgl开发第一道坎——矩阵与坐标变换,这里我们需要用到投影矩阵的中的元素

  resolution.x代表canvas显示元素的宽度,这里恐怕有些地方不太好理解。n其实是代表相机的近平面,我们先假设为1;那么pixelWidthRatio表示像素与3d单位的一个比值的一半。后面我们先暂时也假设finalPosition.w的值也为一,那么最终vPixelWidth的值表示每像素代表多少3d单位。但是由于视锥体的投影变换并不是线性的,所以这样得到的vPixelWidth并不适用视锥体中的所有地方。这时候我们回来看pixelWidthRatio有一个分母n代表近平面,finalPosition.w代表投影后的点距离相机坐标中的z值距离。w/n这里是想用线性来补充一部分非线性变换带来的影响,让vPixelWidth表示每像素代表多少3d单位这个结果尽量的准确。实际效果也确实可以达到预期。

'    float aspect = resolution.x / resolution.y;', // 屏幕宽高比
'     float pixelWidthRatio = 1. / (resolution.x * projectionMatrix[0][0]);', // (r-l)/(2n*Width)
'',
'    vColor = vec4( color, opacity );',
'    vUV = uv;',
'',
'    vec4 finalPosition = transform(position);',
'    vec4 prevPos = transform(previous);',
'    vec4 nextPos = transform(next);',
'',
'    vec2 currentP = project(finalPosition);',
'    vec2 prevP = project(prevPos);',
'    vec2 nextP = project(nextPos);',
'','    float pixelWidth = finalPosition.w * pixelWidthRatio;',
'    vPixelWidth = pixelWidth * 2.0;',

  现在我们可以将端头多出的一半线宽的像素距离转化成线的距离,同时在转化成纹理单位。

'    vec2 horizontal_offset = vec2(0.0, 0.0);',
'    if (flag == -1.0 && (uv.x == 0.0 || uv.x == 1.0)) {',
'        horizontal_offset = -dir * lineWidth * 0.5;',
'        float radio = length(horizontal_offset) * vPixelWidth / repeat.x;',// repeat.x代表线的距离长度
'        vUV.x -= radio;',
'    } else if (flag == 1.0 && (uv.x == 0.0 || uv.x == 1.0)) {',
'        horizontal_offset = dir * lineWidth * 0.5;',
'        float radio = length(horizontal_offset) * vPixelWidth / repeat.x;',
'        vUV.x += radio;',
'    }',

  接下来我们可以在片元着色器中进行剔除工作。这里我们需要做几步工作:

  1. 纹理坐标转换成线的距离长度
  2. 线的距离长度转换成像素单位
  3. 对大于lineWidth/2长度的像素进行剔除
'varying float vPixelWidth;',
'',
'void main() {',
'',
'  vec4 c = vColor;',
'  float uvx = vUV.x * repeat.x;',
'  vec2 coord = vec2(0.0, 0.0);',
// '  float horizontal = 0.0;',
// '  float vertical = 0.0;',
'  if (uvx < 0.0) {',
'    coord.x = abs(uvx) / vPixelWidth;',
'    coord.y = abs(vUV.y - 0.5) * lineWidth;',
'  } else if (uvx > repeat.x) {',
'    coord.x = abs(uvx - repeat.x) / vPixelWidth;',
'    coord.y = abs(vUV.y - 0.5) * lineWidth;',
'  }',
'  if (length(coord) > lineWidth * 0.5) {c.a = 0.0;}',
// '    float uvx = vUV.x * repeat.x;',
// '    if (uvx < 0.0 || uvx > repeat.x) {c.x=1.0;c.y=0.0;c.z=0.0;c.a = 1.0;}',
// '     if( c.a < alphaTest ) c.a = 0.0;',

'    gl_FragColor = c;',
// '     gl_FragColor.a *= step(vCounters,visibility);',
'}' ];

  这里我们需要用到varying变量对vPixelWidth进行差值。最终我们绘制出round的lineCap效果。

猜你喜欢

转载自www.cnblogs.com/dojo-lzz/p/9461506.html
今日推荐