2D SDF推导6: 双圆图形

536 阅读3分钟

image.png

理解了向量夹角,极坐标,对称性,线段的SDF函数之后,常见多边形基本思路都是一致的,本文开始对一些带曲线的形状进行推导, 这些形状都是有两个不同大小的圆组成。 本节的Shader都在

Vesica

关于vesica形状,网上有做一个一些判断,但是我觉得只需要判断 PP 点到 BB 点的距离,与 PP 点到 GG 点的距离就行了。 代码如下

float sd_vesica(vec2 P, float r, float d)
{
    P = abs(P);
    vec2 PB = vec2(-d, 0.0);
    float h = sqrt(r*r - d*d);
    vec2 PG = vec2(0.0, h);
    
    return min(
        distance(P, PB) - r,
        distance(P, PG)
    );
}

Uneven Capsule

假定有圆c1, 圆心位于中心,半径为r1, 圆c2, 圆心位于(0, h), 半径为r2. 最关键是找到上图中蓝色的v1向量,v2与v1垂直, v1的角度 θ\thetasin(θ)=(r1r2)hsin(\theta)=\cfrac{(r1-r2)} h
于是有v1向量为 (sin(θ),cos(θ))(sin(\theta), cos(\theta))
有v2向量为 (cos(θ),sin(θ))(-cos(\theta),sin(\theta))
根据向量 OP\overrightarrow{OP} 在 v2上的投影k的大小, 可以划分出3个区域。 最后得到代码

float sdf_uneven_capsule(vec2 p, float r1, float r2, float h) {
    p.x = abs(p.x);
    
    float sin_theta = (r1-r2) / h;
    float normalize_y = sin_theta * 1.0;
    float normalize_x = sqrt(1.0 - (normalize_y * normalize_y));
    float cos_theta = normalize_x;
    vec2 v2 = vec2(-normalize_y, normalize_x);
    vec2 v  = vec2(normalize_x, normalize_y);
    float k = dot(p, vec2(v2));
    
    // area1
    if( k < 0.0 ) return length(p) - r1;
    // area3
    if( k > cos_theta*h ) return length(p-vec2(0.0,h)) - r2;
    // area2
    return dot(p, v ) - r1;
}

Egg

如何画一个蛋参考上图,点A的坐标为 (d,0)(d, 0) 半径为 rr . 求解一个蛋的SDF,主要需要确定上图的三个点 BB DD OO 为圆心做的图。 通过 DFDF 左右边判判断 选择 BB 还是 DD ,通过 PPyy 正负号判断选择 BB 还是 OO 点,最后代码有


float sdf_mossegg(vec2 P, float r, float d ) 
{
    P.x = abs(P.x);
    
    vec2 B = vec2(-d, 0.);
    float hh = r-d;
    vec2 D = vec2(0, hh);
    
    
    if (P.y < 0.) {
        return length(P) - hh;
    } else {
    
        float t = cross2(P-B, D-B);
        if (t < 0.0) {
            float tt = sqrt(hh *hh + d * d);
            float rr =r - tt;
            return distance(P, D)  - rr ;
        } else {
            return distance(P, B) - d - hh;
        } 
    
    }
    
}

Moon

如何画一个月亮参考上图,点B的坐标为 (d,0)(d, 0) , 半径为 r2r2 . 中心圆的半径为 r1r1 . 上图黑色加粗部分为d,r1,r2. 对于月亮的距离划分为三个区域, 分别是求P到B,C, D 三个点的距离。区域判断主要是通过向量叉积判断方向可得。 最关键是求出 CC 点的坐标 。 这个问题可以转化为两个圆的方程求解 两个圆的方程分别是:

  1. 圆A,圆心 (0,0)(0, 0) ,半径 r1r_1x2+y2=r12x^2 + y^2 = r_1^2
  2. 圆B,圆心 (d,0)(d, 0) ,半径 r2r_2 : (xd)2+y2=r22(x - d)^2 + y^2 = r_2^2

为解这两个方程,首先可以通过从一个方程中减去另一个来消去 y2y^2 ,得到 xx 的关系式。 从上面两个方程中减得: x2(xd)2=r12r22x^2 - (x-d)^2 = r_1^2 - r_2^2
展开后得到: x2(x22xd+d2)=r12r22x^2 - (x^2 - 2xd + d^2) = r_1^2 - r_2^2
这可以简化为 xx 的一个线性方程: 2xdd2=r12r222xd - d^2 = r_1^2 - r_2^2
从而有效的 xx 为: x=r12r22+d22dx = \frac{r_1^2 - r_2^2 + d^2}{2d}
之后我们可以将 xx 的值代回任一圆的方程中来求解 yy 。将 xx 代入圆A的方程: y2=r12x2y^2 = r_1^2 - x^2
最后有代码

float sdf_moon(vec2 P, float r1, float r2, float d ) 
{
    P.y = abs(P.y);
    float a = (r1*r1 - r2*r2 + d*d)/(2.0*d);
    float b = sqrt(max(r1*r1-a*a,0.0));

    vec2 PointC = vec2(a, b);
    vec2 PointB = vec2(d, 0.);
    vec2 PointD = vec2(0., 0.);
    vec2 VectorCB = PointB - PointC;
    vec2 VectorDC = PointC - PointD;
    vec2 VectorCP = P - PointC;
    vec2 VectorDP = P - PointD;

    float t1 = cross2(VectorCB, VectorCP);
    float t2 = cross2(VectorDC, VectorDP);

    if (t1  > 0.0 && t2 < 0.0) {
       return distance(P,  PointC);
    }
    
    return max(
        r2 - distance(P, PointB),
        length(P) - r1
    );
   
}

Heart

如何画一个心参考上图, 心外部的距离可以变成线段 ECEC 和圆 DD 的SDF。心内部的距离在向量 BCBC 左侧为圆的距离, 右侧为距离 BB 点或者线段 ECEC 的最短距离。最后有一下代码

float sd_heart( in vec2 P, float r )
{
    P.x = abs(P.x);
    vec2 PointB = vec2(0., r);
    vec2 PointA = vec2(r, 0.0);
    vec2 PointC = vec2(0.5*r, 0.5*r);
    vec2 PointD = vec2(0.25 * r, 0.75 *r );
    vec2 VectorBA = PointA - PointB;
    vec2 VectorBP = P - PointB;

    float t = cross2(VectorBA, VectorBP);
   if (t > 0.0) {
       return distance(P, PointD) -  0.25 * r * sqrt(2.);
    }

    float hh = clamp(dot(P, PointC) / dot(PointC, PointC), 0., 1.);
    return min(
      distance(P, PointB),
      length(P-hh*PointC)
    ) * sign(P.x-P.y);

}