等宽线条

回顾曲线的绘制

前面使用造型函数绘制曲线的一般方法是, 计算出当前x坐标对应的曲线的y坐标y1,和当前片元凡人y坐标y0相减, 取其绝对值。

显然,当差值为0 的时候,说明这个点,精准的落在曲线上。然后,加上线宽d, 差值小于d/2的时候,就是在这个宽度的线条内。 但是这个宽度描述的是y方向上的,但曲线十分陡峭,也就是斜率很大的的时候,问题就很明显了。

来一个简单的3次函数看看效果。

float line =   x*x*x ;
float dy = dFdy(st.y) ;
float thickness  =dy * 8.0 ;
float d = abs(line  - st.y);
float linefractor  = smoothstep( thickness,thickness -0.01 ,d ) ;
color =  mix( color, vec3(0), linefractor);
fragColor = vec4(color, 1.);

效果就是这样的,可以看到斜率越大,这个线的宽度越小。
等宽线条

矫正宽度

原因

前面已经说过, 我们的线宽是基于y的变化来绘制的,但是实际的线宽,却应该是垂直于当前点的切线方向。

下图用一条斜线做说明。

等宽线条

解决

由上图可知, 期望宽度是d , 结果是d * (cos θ) 。 曲线的斜率是tan(θ)。

要让宽度恢复为 d, 显然只需要在原本的基础除一个 cos(θ) 。

那其实就是 d * (√(1+ tan(θ)^2)) ;

等宽线条

要知道曲线上一点的斜率,只需要对其进行求导。

代码里的d是 当前片元到曲线的竖直距离, 所以用除。


float d = abs(line  - st.y);
float fy = 3.* x*x  ; // 导函数 斜率
 d/= sqrt(1.  + fy*fy);
  

矫正后结果如下
等宽线条

边界情况

看上去不错,但是有时候也会出现问题, 尤其是在斜率突变到0的时候, 典型的就是三角函数类的。

float line =   sin(10. *x);

等宽线条
等宽后, 可以看到在波峰处出现了断裂。

等宽线条

解决的方式也比较简单,那就是限制斜率的最小值。

d/= sqrt(1.  + fy*fy);

虽然还有瑕疵,但是可以看。需要注意的是, 我这种处理只适用这种情况,并不通用。
等宽线条

另一种绘制方式

之前采用的方法是可以绘制任意能用函数表达的曲线的方式, 包括隐函数。 用△y来判定,点是否在图形内,线宽会受到斜率影响。

绘制直线

当斜率恒定的时候, 线宽自然恒定。 这里要说的就是特殊情况, 直线。 要绘制一条一定宽度的直线,可以采用另一种方式,数学上结果可能是一样的。 那就是,判断,当前片元到直线的距离,来确定是否在直线上。

直线方程有两种,点斜式和两点式 , 这里为了方便后续操作,采用两点式。

用当前点st和要过的点连线AB , 通过叉乘(二维叉乘是模的积乘正弦) , 计算出当前点到目标直线的距离,即为线宽的一半 。

float line3 (vec2 A , vec2 B ,vec2 st,  float lineWidth){  
    vec2 a = st - A ;
    vec2 b  = normalize(A - B) ;
    float d = abs(a.x * b.y - a.y * b.x) ;
    return step(d,lineWidth/2.  ); 
}

等宽线条

绘制线段

上面的方法只能绘制直线, 对于任意曲线来说, 其实可以分解为若干线段,微分到极致,它就是平滑的。

所以,来看看线段的绘制,同样是计算点到直线的距离,只是多了范围的限制。

下面的d,算的是点C到AB所在直线的垂线向量的模 。 垂足在AB之间时和之前没有什么不同。

当垂足向左越过A点时, 上面一步e的计算, 向量积为负, 结果就是0 , d计算的就是AC的长度, 向右超出B点时,e为1, d 就是|BC| 。 这种算法,结果出来就是一个圆角的线段。

float segment(vec2 A , vec2 B ,vec2 st,  float lineWidth){ 
    vec2 AC= A -st  ; //假设当前点为C
    vec2 AB  = A - B ;

    float e = clamp(dot(AB, AC)/dot(AB,AB),0.,1.  ); // 这样就算出 ac在AB上的投影 
    float d = length(e * AB - AC);// 只要宽度 比1个单位的情况要小就不会出问题  
    return smoothstep(lineWidth/2.,lineWidth/2. -.001,d ) ;

}

等宽线条

曲线转线段

曲线转线段其实不断在曲线上取点, 每两点组成一个线段,显然,分段数越多,曲线越接近它本来的样子。 理想状态是,斜率变化大的地方取点就密集些,我是做不到。 下面,就是一个均匀取点的方法。

下面以正弦为例。用的是加法, 没有限制不超出1,这样便于观察。

float sinFn(float x , float A ,float theta){ 
    return sin(x * A  + theta)* .2; 
}

float sinLine(vec2 st , float start , float end , int segs , float lineWidth, float a ,float theta){ 
    float res = 0. ;
    float step = (end - start)/float(segs) ; //步长
    for(int i = 0; i< segs; i++){ 
        float  x   = start + float(i) *step ;
        float  x2  = x + step ;

        vec2 A = vec2(x,sinFn(x,a,theta ) );
        vec2 B = vec2(x2,sinFn(x2,a,theta ) );
        res += segment(A, B ,st , lineWidth);
    }
    // 当然可以约束一下 不大于1
    return res; 
}

等宽线条

结束


本文讲述了,通用绘制曲线时,用斜率矫正线宽,以及线段法绘制曲线的方法。

两种方法各有其应用场景, 对于已知表达式的曲线,用第一种方式最方便。

第二种方式,线段法,其实更适合结合点数据,不用纯shader, 用uniform 输入点位数据, 绘制任意点集形成的曲线。

原文链接:https://juejin.cn/post/7263113577154183226 作者:莫石

(0)
上一篇 2023年8月3日 上午10:44
下一篇 2023年8月4日 上午10:06

相关推荐

发表回复

登录后才能评论