圆形的SDF模型

367 阅读4分钟

源码:github.com/buglas/shad…

在绘制二维SDF模型之前,我们需要先有一个距离场的概念。

1-距离场

SDF是Signed-distance-field的简写,译作有向距离场,它是一个记录模型中心点到模型表面的最小距离的函数。

以球体为例解释一下SDF的概念。

image-20220705180011674

球体的SDF函数是:

f(p)=|p-o|-r
  • p 空间内部任意一点
  • o 球心
  • r 球体的半径

通过这个函数,我们可以明显的看出:

  • f(p)>0 时,p在球外
  • f(p)=0 时,p在球上
  • f(p)<0 时,p在球内

接下来我们以此为例绘制一个二维的圆形。

2-用距离场绘制圆形

2-1-代码实现

// 坐标系缩放
#define ProjectionScale 1.
// 半径
#define r .7

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
  return ProjectionScale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 圆形的sdf模型
float sdfCircle(vec2 p) {
  return length(p) - r;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord);
  // 当前片元到圆形的有向距离
  float cd = sdfCircle(coord);
  // 当有向距离小于0时,绘制白色圆形
  vec3 color = cd < 0. ? vec3(1) : vec3(0);
  // 最终的颜色
  fragColor = vec4(color, 1.0);
}

效果如下:

08

在上面的代码中,默认圆形的圆心在零点。

sdfCircle(vec2 p)方法便是判断当前片元到圆形的有向距离。

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

知道了当前片元到圆形的有向距离后,便可以可以基于此距离来绘制圆形。

// 当前片元到圆形的有向距离
float cd = sdfCircle(coord);
// 当有向距离小于0时,绘制白色圆形
vec3 color = cd < 0. ? vec3(1) : vec3(0);

现在基于距离场绘制圆形的代码我们已经实现了,接下来我们可以通过一种艺术效果把距离场显示出来。

2-2-显示距离场

左轮眼

看过《火影忍者》小伙伴会知道,里面的佐助有一双轮回眼,可以看破生死,掌控轮回。

如果我们用一个个圆圈将距离场的辐射范围可视化,就可以画出一个酷似轮回眼的效果,看起来酷酷哒。

09

其整体代码如下:

// 坐标系缩放
#define ProjectionScale 1.
// 半径
#define r .2

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
  return ProjectionScale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 圆形的sdf模型
float sdfCircle(vec2 p) {
  return length(p) - r;
}

// 显示距离场
vec3 SdfHelper(float cd) {
  vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
  color *= 1. - exp(-3. * abs(cd));
  color *= .8 + .2 * sin(150. * cd);
  color = mix(color, vec3(.7, .7, 0), smoothstep(.01, 0., abs(cd)));
  return color;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord);
  // 当前片元到圆形的有向距离
  float cd = sdfCircle(coord);
  // 当有向距离小于0时,绘制白色圆形
  vec3 color = SdfHelper(cd);

  // 最终的颜色
  fragColor = vec4(color, 1.0);
}

重点看SdfHelper() 方法,此方法可以分成4步理解。

1.定义初始颜色

vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);

sign(x):

  • 当x<0时,返回-1
  • 当x=0时,返回0
  • 当x>0时,返回1

根据圆形的SDF模型,我们可以知道:

  • cd 从圆心处的值为-r
  • cd 在圆形边界上的值为0
  • cd 在圆形边外的值为+∞

因此sign(x)在圆内的值为-1,圆边上的值为0,圆外的值为1。

将sign(x)乘以一个颜色后,那圆内就会是黑色,圆外则是相应的颜色。

我们可以自己测试一下:

vec3 SdfHelper(float cd) {
  vec3 color = sign(cd) * vec3(0, 0.5, 1);
  return color;
}

效果如下:

13

当我们用1减去这个颜色后,圆内就是白色,圆外是这个蓝色的相反色。

vec3 SdfHelper(float cd) {
  vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
}

效果如下:

13

2.给圆形边界一个黑色过度。

vec3 SdfHelper(float cd) {
  vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
  color *= 1. - exp(-3. * abs(cd));
  return color;
}

效果如下:

10

exp(x) 方法返回的是自然指数e的x次方。

当cd=0时,exp(-3. * abs(cd))等于1;其它时候则会小于1。我们可以用函数图像显示一下:

vec3 SdfHelper(float cd) {
  return vec3(1. - exp(-3. * abs(cd)));
}

效果如下:

11

用1减去上面的颜色便可以得到一个相反色。

12

用初始color乘以上面的颜色,边可以得到越趋向圆边越黑的图像。

vec3 SdfHelper(float cd) {
  vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
  color *= 1. - exp(-3. * abs(cd));
  return color;
}

10

3.实用正弦函数绘制波纹。

vec3 SdfHelper(float cd) {
  vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
  color *= 1. - exp(-3. * abs(cd));
  color *= .8 + .2 * sin(150. * cd);
  return color;
}

效果如下:

14

上面的sin函数很好理解,它可以基于有向距离绘制一圈圈深浅不一的波纹。

vec3 SdfHelper(float cd) {
  return vec3(.8 + .2 * sin(150. * cd));
}

效果如下:

15

将这个黑白波纹乘以第2步的图像,便可以得到下面的效果:

14

4.绘制圆形描边,以凸显圆形的边界。

vec3 SdfHelper(float cd) {
  vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
  color *= 1. - exp(-3. * abs(cd));
  color *= .8 + .2 * sin(150. * cd);
  color = mix(color, vec3(.7, .7, 0), smoothstep(.01, 0., abs(cd)));
  return color;
}

效果如下:

16

现在圆形的距离场已经显示出来了。

在以后工作中,我们必然还会遇到鼠标对于SDF模型的选择,这就需要判断鼠标到SDF模型的距离了。

3-鼠标选择测试

整体代码如下:

// 坐标系缩放
#define ProjectionScale 1.
// #define r .5+.4 *sin(iTime)
// 半径
#define r .3

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
  return ProjectionScale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

// 圆形的sdf模型
float sdfCircle(vec2 p) {
  return length(p) - r;
}

// 显示距离场
vec3 SdfHelper(float cd) {
  vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
  color *= 1. - exp(-3. * abs(cd));
  color *= .8 + .2 * sin(150. * cd);
  color = mix(color, vec3(.7, .7, 0), smoothstep(.01, 0., abs(cd)));
  return color;
}

// 鼠标选择测试
void selectTest(out vec3 color, vec2 curCoord) {
  // iMouse.z > 0.对应鼠标按下事件
  if(iMouse.z > 0.) {
    // 鼠标的投影坐标位
    vec2 mouseCoord = ProjectionCoord(iMouse.xy);
    // 鼠标到圆形的有向距离
    float md = sdfCircle(mouseCoord);
    // 当前片元到鼠标的距离
    float a = length(curCoord - mouseCoord);
    //鼠标到圆形的有向距离的绝对值
    float b=abs(md);
    // 以有向距离为半径显示一个以鼠标位中心的圆形
    color = mix(color, vec3(.7, 1, .5), smoothstep(0.007, 0., abs(a - b)));
  }
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 投影坐标
  vec2 coord = ProjectionCoord(fragCoord);
  // 当前片元到圆形的有向距离
  float cd = sdfCircle(coord);
  // 当有向距离小于0时,绘制白色圆形
  vec3 color = SdfHelper(cd);
  // 鼠标选择测试
  selectTest(color, coord);

  // 最终的颜色
  fragColor = vec4(color, 1.0);
}

效果如下:

image-20220901162533193

重点看selectTest(color, coord)方法:

void selectTest(out vec3 color, vec2 curCoord) {
  // iMouse.z > 0.对应鼠标按下事件
  if(iMouse.z > 0.) {
    // 鼠标的投影坐标位
    vec2 mouseCoord = ProjectionCoord(iMouse.xy);
    // 鼠标到圆形的有向距离
    float md = sdfCircle(mouseCoord);
    // 当前片元到鼠标的距离
    float a = length(curCoord - mouseCoord);
    // 鼠标到圆形的有向距离的绝对值
    float b=abs(md);
    // 以有向距离为半径显示一个以鼠标位中心的圆形
    color = mix(color, vec3(.7, 1, .5), smoothstep(0.007, 0., abs(a - b)));
  }
}

上面代码中a、b的值如下图所示:

image-20220901164229563

当a、b趋近相等时,abs(a - b)趋近0,那smoothstep(0.007, 0., abs(a - b)) 就会趋近1。

因此,利用mix方法便可以显示出一个圆形的边界。

扩展

我们可以使用iTime 动态改变圆形半径,从而给圆形一个缩放动画。

#define r .5+.4 *sin(iTime)

效果如下:

2

现在二维圆形的SDF就已经说完了,接下来咱们来说三维球体的SDF模型。

参考链接:space.bilibili.com/10707223/ch…