Shader 3d RayMarching5 3D SDF 原型

268 阅读2分钟

我最开始就是因为看不懂五边形的SDF才开始学习Shader技术,最开始的7,8篇文章都是做2D SDF的推导,到了3D之后,其实SDF推导大差不差, 3D SDF的变形能力更强了, IQ的这篇文章 iquilezles.org/articles/di… 有这个世界上关于3D SDF已知的一切。 本文将结合2D SDF帮助大家理解SDF的函数。 本文尝试解释三个最常用的,球,立方体,线

2407133138 www.shadertoy.com/view/lcfyRN

Sphere&Circle

float sdSphere( vec3 p, float s )
{
  return length(p)-s;
}

float sdCircle( vec2 p, float r )
{
    return length(p) - r;
}

球和园是最直接的SDF的函数了,p代表一个三维向量,即空间中的一个点。s 代表球体的半径。length(p)计算点 p 到原点的欧几里得距离(即向量 p 的长度)。 length(p) - s 计算点 p 到球体表面的距离。如果这个值为负,则说明点在球体内部;为正则说明点在球体外部;为零则说明点正好在球体表面。

Segment&Line

float sdLine( vec3 p, vec3 a, vec3 b, float r )
{
  vec3 pa = p - a, ba = b - a;
  float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
  return length( pa - ba*h ) - r;
}

float sdSegment( in vec2 p, in vec2 a, in vec2 b )
{
    vec2 pa = p-a, ba = b-a;
    float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
    return length( pa - ba*h );
}

线段和Line是所有推导的基础,从2D出发假定有线段Segment(A, B), 空间中所有点到线段的最近距离分为三种情况。

  • P1 在AB中间,距离为点到线的垂直线
  • P2 在AB之外,距离B较近 距离为P2到B的距离
  • P3 在AB之外,距离A较近,距离为P3到A的距离

image (9).gif 一种符合直觉的求法,便是把三个值\overrightarrow{PB}$$\overrightarrow{PA}$$\overrightarrow{PC}的长度都求出来。取最小的一个长度便可。\overrightarrow{PB}$$\overrightarrow{PA}长度非常好求。PC\overrightarrow{PC}的长度可以通过\overrightarrow{PA}$$\overrightarrow{PB}的夹角θ\theta,然后求sin(θ)×PAsin(\theta) \times ||\overrightarrow{PA}||θ\theta可以通过向量点积求得。 注意上图的hhhh参数,表示PPABAB上投影长度在ABAB长度的占比,如果这个值(0,1)(0, 1)区间,就说明我们需要求PC\overrightarrow{PC} 转化成为代码

float sd_segment(vec2 pct, vec2 pa, vec2 pb) {
    vec2 v1 = pct - pa;
    vec2 v2 = pb - pa;
    vec2 v3 = pct - pb;
    
    
    float hh = dot(v1, v2) / length(v2) / length(v2);
    
    if (0. < hh && hh < 1.0) {
        float theta = acos(dot(v1 ,v2) /length(v1)/length(v2));
        float d1 = sin(theta) * length(v1);
        return d1;
    } else {
        return min(length(v1), length(v3)) ;
    }
}

这里可以对代码进行优化, PC\overrightarrow{PC} 的长度其实PAAC\overrightarrow{PA} - \overrightarrow{AC}AC\overrightarrow{AC}可以有hhhh决定 ,我们已经知道hhhh的大小。并且当hh<0hh<0,距离就是Length(PA)Length(\overrightarrow{PA}),当hh>0hh>0,距离就是Length(PB)Length(\overrightarrow{PB}). 于是代码可以优化为

float sd_segment2(vec2 pct, vec2 pa, vec2 pb) {
    vec2 v1 = pct - pa;
    vec2 v2 = pb - pa;
    vec2 v3 = pct - pb;
    
    
    float hh = dot(v1, v2) / length(v2) / length(v2);

    if (hh < 0.0) {      
      return length(v1 - 0. * v2);
    } else if (0. < hh && hh < 1.0) {
        return length(v1 - hh * v2);
    } else {
        // v3 = v1 - v2 = pct -pb;
        return length(v1 - v2);
    }
}

通过上面的优化可以发现,三个判断条件的相似性。 通过引入clamp函数去掉if判断优化性能,通过dot(v2, v2)减少一次length的计算。 最后得到IQ大神最终结果

float sd_segment3(vec2 pct, vec2 pa, vec2 pb) {
    vec2 v1 = pct - pa;
    vec2 v2 = pb - pa;
    float h = clamp(dot(v1,v2)/dot(v2,v2), 0.0, 1.0);
    return length(v1-h*v2); 
}

3D 相对于2D,其实就是为线段整体增加了一个半径R, 可以想象将2D平面的一个线段,每个点都变成一个球最后得到的结果。

Square&Box


float sdSquare( in vec2 p, in vec2 b )
{
    vec2 d = abs(p)-b;
    return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}

float sdBox( vec3 p, vec3 b )
{
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

a.svg 长方形可以通过中心点、宽度和高度定义。点位置的分布主要有三种情况,分别是:

  1. P1 点在长方形内部:对于在长方形内部的点,其到最近边界的有符号距离是负值,其绝对值是点到最近边界的最小距离。
  2. P3, P2 点在长方形外部,水平或垂直对齐:如果点位于长方形的延长线上(即垂直或水平对齐),则其到长方形的最近距离将是其到最近边的直线距离。
  3. P4 点在长方形外部,不与任何边对齐:对于不与长方形边界平行或垂直的外部点,其到长方形的最近距离是点到长方形最近角的欧几里得距离。

设长方形中心为 C(xc,yc)C(x_c, y_c)为中心点C(0,0)C(0, 0)宽度为 ww,高度为 hh,任意点 P(x,y)P(x, y)。由于坐标轴具有对称性,只需要考虑第一象限的四种情况,对于其他象限的距离都可以通过绝对值转换到第一象限。

  1. 计算点 PP 水平和垂直方向上到长方形边界的距离 dx=xw/2dx = x - w/2dy=yh/2dy = y - h/2
  2. 根据dx$$dy正负情况可以判断如何计算距离 dd
    1. P1 点在长方形内部, 即**dx<0 and dy<0**d=max(dx,dy)d = max(dx, dy)
    2. P3, P2 点在长方形外部,水平或垂直对齐即**oneOf(dx,dy) < 0****, **d=max(dx,dy)d = max(dx, dy)
    3. P4 点在长方形外部,不与任何边对齐, 即**dx>0 and dy>0**d=dx2+dy2d = \sqrt{dx^2 + dy^2}

变成shader代码非常清晰

float sdf_rectangle1(vec2 pct, vec2 wh) {
  vec2 dxy = abs(pct) - wh;
  if (dxy.x > 0. && dxy.y >0.) {
    return length(dxy);
  } else {
    return max(dxy.x, dxy.y);
  }
}

但是要知道shader里面不喜欢if else. 考虑如何让两个condition变成一条语句。考虑情况2,3。 d=max(dx,0)2+max(dy,0)2d = \sqrt{max(dx, 0)^2 + max(dy, 0)^2}glsl中向量是支持max函数的,可以进一步简化max(dxy, vec2(0.)),但是上述公式在情况1就不适用了,结果为0, 所以进一步考虑到情况1。 max(min(dx,0),min(dy,0))max(min(dx, 0), min(dy,0)) 上面公式在情况2,3都为0, 所以最后只需要将两个公式相加,便得到最后的结果,shader代码如下

float sdf_rectangle2(vec2 pct, vec2 wh) {
  vec2 dxy = abs(pct) - wh;
  return length(max(dxy, 0.0)) + max(min(dxy.x, 0.0), min(dxy.y, 0.0));
}

而对于立方体来说,只需要针对于第三个z唯独多做一次max运算即可。