在上一篇中,我们了解了基本的距离场构图法,在这一篇,我们来进一步利用这个方法来绘制更多图形。
距离场构图法,最核心的思路是要定义一个形状的距离场,通俗来说,就是定义整个画布空间中每个像素点的距离值。
最简单的例子就是圆,定义圆的距离场,只需要定义像素点到圆心的距离,在上一篇中我们画圆,就是用了这个定义。
与圆比较类似,也很简单的距离场是矩形:
#version 300 es
precision highp float;
out vec4 FragColor;
uniform vec2 resolution;
void main() {
vec2 st = gl_FragCoord.xy / resolution;
vec2 center = vec2(0.5);
st = abs(st - center);
float d = max(st.x, st.y);
FragColor.rgb = smoothstep(d - 0.015, d, 0.2) * vec3(1.0);
FragColor.a = 1.0;
}
接下来我们来求稍微复杂一点的,任意直线的距离场。
点到直线的距离
要计算点到任意直线的距离。
最简单的办法是用向量来计算,我们用两点A、B来确定一条直线AB,那么任意一点P到直线AB的距离为:
因为如图所示:
向量与向量的叉积模的几何意义是平行四边形APP'B的面积,除以底边AB的长度,就是P到直线AB的距离。
所以我们可以实现直线的距离场:
float sdf_line(vec2 a, vec2 b, vec2 st) {
vec2 ap = st - a;
vec2 ab = b - a;
return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab);
}
并用以绘制直线:
void main() {
vec2 st = gl_FragCoord.xy / resolution;
float d = sdf_line(vec2(0), vec2(1.0), st);
float d2 = sdf_line(vec2(1.0, 0.0), vec2(0.0, 1.0), st);
FragColor.rgb = (stroke(d, 0.0, 0.03, 0.2) + stroke(d2, 0.0, 0.03, 0.2)) * vec3(1.0);
FragColor.a = 1.0;
}
点到线段的距离
前面我们计算的是点到直线的距离,稍微修改一下,考虑两个端点,就可以计算点到线段的距离。
点到线段的距离,当点P在向量的投影落在线段AB之间时(如下图左边),点P到线段AB的距离就等于P到AB所在直线的距离,而点P在向量的投影落在线段AB之外时(如下图右边),那么点P到线段AB的距离为线段AP和PB中较短的一条的长度。
所以我们得到如下的线段距离场计算函数:
float sdf_seg(vec2 a, vec2 b, vec2 st) {
vec2 ap = st - a;
vec2 ab = b - a;
vec2 bp = st - b;
float l = length(ab);
float proj = dot(ap, ab) / l;
if(proj >= 0.0 && proj <= l) {
return sdf_line(a, b, st);
}
return min(length(ap), length(bp));
}
注意这里面我们用向量和向量的点乘除以线段AB的长度来得到点P在AB上的投影,通过投影的长度和方向来判断点是否落在线段AB之间。
最终我们就可以用距离场绘制线段了:
采样与曲线绘制
如果要绘制一条连续曲线,我们可以取相邻的三个点A、B、C采样,计算P点到这三个点构成的两条线段AB和AC的距离,取距离短的作为P到曲线的距离。
float sdf_plot(vec2 a, vec2 b, vec2 c, vec2 st) {
float d1 = sdf_seg(a, b, st);
float d2 = sdf_seg(b, c, st);
return min(d1, d2);
}
我们定义一个宏,来对曲线方程进行采样:
#ifndef PLOT
#define PLOT(f, st, step) sdf_plot(vec2(st.x - step, f(st.x - step)), vec2(st.x, f(st.x)), vec2(st.x + step, f(st.x + step)), st)
#endif
有了采样方法,我们就可以绘制各种曲线了:
float stroke(float d, float d0, float w, float smth) {
float th = 0.5 * w;
smth = smth * w;
float start = d0 - th;
float end = d0 + th;
return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
}
float sdf_line(vec2 a, vec2 b, vec2 st) {
vec2 ap = st - a;
vec2 ab = b - a;
return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab);
}
float sdf_seg(vec2 a, vec2 b, vec2 st) {
vec2 ap = st - a;
vec2 ab = b - a;
vec2 bp = st - b;
float l = length(ab);
float proj = dot(ap, ab) / l;
if(proj >= 0.0 && proj <= l) {
return sdf_line(a, b, st);
}
return min(length(ap), length(bp));
}
float sdf_plot(vec2 a, vec2 b, vec2 c, vec2 st) {
float d1 = sdf_seg(a, b, st);
float d2 = sdf_seg(b, c, st);
return min(d1, d2);
}
#ifndef PLOT
#define PLOT(f, st, step) sdf_plot(vec2(st.x - step, f(st.x - step)), vec2(st.x, f(st.x)), vec2(st.x + step, f(st.x + step)), st)
#endif
float fx(in float x) {
return 0.0;
}
float fy(in float x) {
return 9999999.99 * x;
}
float f1(in float x) {
return floor(x);
}
float f2(in float x) {
return sin(2.0 * x) / x;
}
float f3(in float x) {
return sqrt(1.0 - x * x);
// return 0.0;
}
float f4(in float x) {
return -x - sin(x);
}
float f5(in float x) {
return log(x);
}
void main() {
vec2 st = gl_FragCoord.xy / resolution;
st = mix(vec2(-10, -10), vec2(10, 10), st);
float stp = 0.1;
float thick = 0.4;
float smth = 0.2;
// PLOT func, field, step
float px = PLOT(fx, st, stp);
float py = PLOT(fy, st, stp);
float p1 = PLOT(f1, st, stp);
float p2 = PLOT(f2, st, stp);
float p3 = PLOT(f3, st, stp);
float p4 = PLOT(f4, st, stp);
float p5 = PLOT(f5, st, stp);
vec3 cx = stroke(px, 0.0, 0.2, 0.2) * vec3(1.0, 1.0, 1.0);
vec3 cy = stroke(py, 0.0, 0.2, 0.2) * vec3(1.0, 1.0, 1.0);
vec3 c1 = stroke(p1, 0.0, thick, smth) * vec3(0, 1.0, 0);
vec3 c2 = stroke(p2, 0.0, thick, smth) * vec3(0, 1.0, 1.0);
vec3 c3 = stroke(p3, 0.0, thick, smth) * vec3(1.0, 1.0, 0);
vec3 c4 = stroke(p4, 0.0, thick, smth) * vec3(1.0, 0, 1.0);
vec3 c5 = stroke(p5, 0.0, thick, smth) * vec3(1.0, 0, 0);
FragColor.rgb = cx + cy + c1 + c2 + c3 + c4 + c5;
FragColor.a = 1.0;
}
注意,在上面代码里,我们通过st = mix(vec2(-10, -10), vec2(10, 10), st);来扩大坐标系的区间,将坐标系从(0,0),(1,1)扩大到了(-10,-10),(10,10),这也是一种常用的数学技巧,可以牢记。
在下一讲里面,我们将继续深入,利用距离场构图转换不同坐标系来构造出更加有趣的图形。