在绘制二维SDF模型之前,我们需要先有一个距离场的概念。
1-距离场
SDF是Signed-distance-field的简写,译作有向距离场,它是一个记录模型中心点到模型表面的最小距离的函数。
以球体为例解释一下SDF的概念。
球体的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);
}
效果如下:
在上面的代码中,默认圆形的圆心在零点。
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-显示距离场
看过《火影忍者》小伙伴会知道,里面的佐助有一双轮回眼,可以看破生死,掌控轮回。
如果我们用一个个圆圈将距离场的辐射范围可视化,就可以画出一个酷似轮回眼的效果,看起来酷酷哒。
其整体代码如下:
// 坐标系缩放
#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;
}
效果如下:
当我们用1减去这个颜色后,圆内就是白色,圆外是这个蓝色的相反色。
vec3 SdfHelper(float cd) {
vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
}
效果如下:
2.给圆形边界一个黑色过度。
vec3 SdfHelper(float cd) {
vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
color *= 1. - exp(-3. * abs(cd));
return color;
}
效果如下:
exp(x) 方法返回的是自然指数e的x次方。
当cd=0时,exp(-3. * abs(cd))等于1;其它时候则会小于1。我们可以用函数图像显示一下:
vec3 SdfHelper(float cd) {
return vec3(1. - exp(-3. * abs(cd)));
}
效果如下:
用1减去上面的颜色便可以得到一个相反色。
用初始color乘以上面的颜色,边可以得到越趋向圆边越黑的图像。
vec3 SdfHelper(float cd) {
vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
color *= 1. - exp(-3. * abs(cd));
return color;
}
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;
}
效果如下:
上面的sin函数很好理解,它可以基于有向距离绘制一圈圈深浅不一的波纹。
vec3 SdfHelper(float cd) {
return vec3(.8 + .2 * sin(150. * cd));
}
效果如下:
将这个黑白波纹乘以第2步的图像,便可以得到下面的效果:
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;
}
效果如下:
现在圆形的距离场已经显示出来了。
在以后工作中,我们必然还会遇到鼠标对于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);
}
效果如下:
重点看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的值如下图所示:
当a、b趋近相等时,abs(a - b)趋近0,那smoothstep(0.007, 0., abs(a - b)) 就会趋近1。
因此,利用mix方法便可以显示出一个圆形的边界。
扩展
我们可以使用iTime 动态改变圆形半径,从而给圆形一个缩放动画。
#define r .5+.4 *sin(iTime)
效果如下:
现在二维圆形的SDF就已经说完了,接下来咱们来说三维球体的SDF模型。