【零基础】充分理解WebGL(三)

avatar
掘金前首席打杂官

接上篇:juejin.cn/post/710086…

在上一篇中,我们了解了基本的距离场构图法,在这一篇,我们来进一步利用这个方法来绘制更多图形。

距离场构图法,最核心的思路是要定义一个形状的距离场,通俗来说,就是定义整个画布空间中每个像素点的距离值。

最简单的例子就是圆,定义圆的距离场,只需要定义像素点到圆心的距离,在上一篇中我们画圆,就是用了这个定义。

code.juejin.cn/pen/7100853…

与圆比较类似,也很简单的距离场是矩形:

#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;
}

code.juejin.cn/pen/7103401…

接下来我们来求稍微复杂一点的,任意直线的距离场。

点到直线的距离

要计算点到任意直线的距离。

最简单的办法是用向量来计算,我们用两点A、B来确定一条直线AB,那么任意一点P到直线AB的距离为:

d=PA×ABABd = \dfrac{|\overrightarrow{PA} \times \overrightarrow{AB}|}{|\overrightarrow{AB}|}

因为如图所示:

image.png

向量PA\overrightarrow{PA}与向量AB\overrightarrow{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;
}

code.juejin.cn/pen/7103412…

点到线段的距离

前面我们计算的是点到直线的距离,稍微修改一下,考虑两个端点,就可以计算点到线段的距离。

点到线段的距离,当点P在向量AB\overrightarrow{AB}的投影落在线段AB之间时(如下图左边),点P到线段AB的距离就等于P到AB所在直线的距离,而点P在向量AB\overrightarrow{AB}的投影落在线段AB之外时(如下图右边),那么点P到线段AB的距离为线段AP和PB中较短的一条的长度。

image.png

所以我们得到如下的线段距离场计算函数:


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));
}

注意这里面我们用向量AP\overrightarrow{AP}和向量AB\overrightarrow{AB}的点乘除以线段AB的长度来得到点P在AB上的投影,通过投影的长度和方向来判断点是否落在线段AB之间。

最终我们就可以用距离场绘制线段了:

code.juejin.cn/pen/7103420…

采样与曲线绘制

如果要绘制一条连续曲线,我们可以取相邻的三个点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;
}

code.juejin.cn/pen/7103428…

注意,在上面代码里,我们通过st = mix(vec2(-10, -10), vec2(10, 10), st);来扩大坐标系的区间,将坐标系从(0,0),(1,1)扩大到了(-10,-10),(10,10),这也是一种常用的数学技巧,可以牢记。

在下一讲里面,我们将继续深入,利用距离场构图转换不同坐标系来构造出更加有趣的图形。