ShaderToy 快速入门

ShaderToy 快速入门

by acdzh · 2022-06-03 04:26:42

简介与坐标系

ShaderToy

文章整理自 https://www.bilibili.com/video/av209900301. 页面中有大量 webgl 元素, 建议使用桌面端浏览器打开.

简介

这是一个 ShaderToy 的默认程序

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = fragCoord/iResolution.xy;
 
  // Time varying pixel color
  vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));
 
  // Output to screen
  fragColor = vec4(col,1.0);
}

坐标系

mainImage 接受两个参数: fragColorfragCoord. fragColor 是画布的颜色, fragCoord 是画布上的坐标, 画布的坐标从左下角开始, 往右是 x 轴, 往上是 y 轴, 以像素为单位.

我们可以把坐标归一化: vec2 uv = fragCoord / iResolution.xy;, 这样的话, uv.xuv.y 就都是一个坐标在 [0, 1] 之间的值.

void mainImage (out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = fragCoord / iResolution.xy;
  fragColor = vec4(uv, 0, 1.);
}

一般来说, 我们需要把坐标原点移到画布的中心. 考虑到坐标的比例, 我们可以让 x 和 y 中的较小者的范围在 [-0.5, 0.5] 之间, 另一个的范围则根据比例来计算. 如下所示:

void mainImage (out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = (fragCoord - .5 * iResolution.xy ) / min(iResolution.x, iResolution.y);
  float b = length(uv) > .45 ? 1. : 0.;
  fragColor = vec4(uv + 0.5, b, 1.);
}

绘制坐标系

fwidth 函数可以获取像素的宽度.

void mainImage (out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = (2. * fragCoord - iResolution.xy ) / min(iResolution.x, iResolution.y);
  vec3 col = vec3(0.);
 
  if (abs(uv.x) <= fwidth(uv.x)) col.r = 1.;
  if (abs(uv.y) <= fwidth(uv.y)) col.g = 1.;
 
  fragColor.rgb = col;
}

这里之所以不使用 abs(uv.x) < 0.01, 是因为 0.01 或其他的数值并不能精确的对齐到像素上, 可能会造成直线忽然变细或消失.

借助 fract 可以绘制出格子, 每个格子的大小是 1.

void mainImage (out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = 2. * (2. * fragCoord - iResolution.xy ) / min(iResolution.x, iResolution.y);
  vec2 pixel = fwidth(uv);
  vec3 col = vec3(0.);
  vec2 cell = 1. - 2. * abs(fract(uv) - .5);
  
  if (abs(uv.x) <= pixel.x) col = vec3(0, 1, 0);
  else if (abs(uv.y) <= pixel.y) col = vec3(1, 0, 0);
  else if (cell.x <= 2. * pixel.x) col = vec3(1.);
  else if (cell.y <= 2. * pixel.y) col = vec3(1.);
 
  fragColor = vec4(col, 1.);
}

线段

我们有一个线段, 起点是 AA, 终点是 BB. 线段的宽度是 widthwidth. 对于每一个点, 假设其坐标是 PP, 如果我们需要再该点绘制一条线段, 需要满足下面的条件:

  1. PPAB\overrightarrow{AB} 的距离小于等于 width2\cfrac{width}{2}.
  2. 保证画出来的是线段不是直线.

对于 1, 我们可以用下面的方式判断:

AB×APABwidth2\cfrac{|\overrightarrow{AB} \times \overrightarrow{AP}|}{|\overrightarrow{AB}|} \leq \cfrac{width}{2}

对于 2, 只需要满足 ABAP0\overrightarrow{AB}\cdot\overrightarrow{AP} \leq 0ABBP0\overrightarrow{AB}\cdot\overrightarrow{BP} \geq 0 即可. 因此函数如下:

bool segment(in vec2 p, in vec2 a, in vec2 b, in float width) {
  vec3 ab = vec3(b - a, 0.);
  vec3 ap = vec3(p - a, 0.);
  vec3 bp = vec3(p - b, 0.);
  return dot(ab, ap) * dot(ab, bp) <= 0. && abs(cross(ab, ap).z) / length(ab) <= width / 2.;
}
 
// mainImage
col = mix(
  col,
  vec3(0., 0., 1.),
  segment(uv, vec2(-2.5, -.5), vec2(2.5, 1.5), .1)
);

其他函数

我们把前面的一些操作抽象一下, 整理如下:

#define PI 3.141592654
 
vec2 fixUv(in vec2 fragCoord) {
  return 2. * (2. * fragCoord - iResolution.xy ) / min(iResolution.x, iResolution.y);
}
 
vec3 grid(in vec2 uv) {
  vec2 pixel = fwidth(uv);
  vec2 fraction = 1. - 2. * abs(fract(uv) - .5);
  
  if (abs(uv.x) <= pixel.x) return vec3(0, 1, 0);
  else if (abs(uv.y) <= pixel.y) return vec3(1, 0, 0);
  else if (fraction.x <= 2. * pixel.x) return vec3(1.);
  else if (fraction.y <= 2. * pixel.y) return vec3(1.);
}
 
float segment(in vec2 p, in vec2 a, in vec2 b, in float width) {
  vec3 ab = vec3(b - a, 0.);
  vec3 ap = vec3(p - a, 0.);
  vec3 bp = vec3(p - b, 0.);
  return dot(ab, ap) * dot(ab, bp) <= 0. 
    && abs(cross(ab, ap).z) / length(ab) <= width / 2. 
      ? 1. 
      : 0.;
}
 
void mainImage (out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = fixUv(fragCoord);
  vec3 col = grid(uv);
 
  col = mix(
    col,
    vec3(0., 0., 1.),
    segment(uv, vec2(-2.5, -.5), vec2(2.5, 1.5), .1)
  );
 
  fragColor = vec4(col, 1.);
}

现在新增一个绘制函数:

float func1(in float x) {
  return sin(x * PI / 2.);
}
 
float funcPlot(in vec2 uv) {
  float f = 0.;
  for (float x = 0.; x <= iResolution.x; x += 1.) {
    float fx = fixUv(vec2(x, 0.)).x;
    float nfx = fixUv(vec2(x + 1., 0.)).x;
    f += segment(uv, vec2(fx, func1(fx)), vec2(nfx, func1(nfx)), 2. * fwidth(uv.x));
  }
  return clamp(f, 0., 1.);
}
 
// mainImage
col = mix(col, vec3(0., 0., 1.), funcPlot(uv));

这里为什么要遍历每一个 x, 而不是直接用 uv.x 来判断呢? 对于每一个点来说, 都需要判断它离函数曲线的最近距离, 而不是与曲线上这一点对应取值的点的距离.

smoothstep

修改一下上面绘制的函数:

float func(in float x) {
  return smoothstep(0., 1., x);
}

如果反一下:

float func(in float x) {
  return smoothstep(1., 0., x);
}

改造一下前面的函数:

vec3 grid(in vec2 uv) {
  vec2 pixel = fwidth(uv);
  vec2 fraction = 1. - 2. * abs(fract(uv) - .5);
  
  vec3 color = vec3(0.);
  color = vec3(smoothstep(2. * pixel.x, 1.9 * pixel.x, fraction.x));
  color += vec3(smoothstep(2. * pixel.y, 1.9 * pixel.y, fraction.y));
  color.rb *= smoothstep(1.9 * pixel.x, 2. * pixel.x, abs(uv.x));
  color.gb *= smoothstep(1.9 * pixel.y, 2. * pixel.y, abs(uv.y));
 
  return color;
}
 
float segment(in vec2 p, in vec2 a, in vec2 b, in float width) {
  vec3 ab = vec3(b - a, 0.);
  vec3 ap = vec3(p - a, 0.);
  vec3 bp = vec3(p - b, 0.);
  if (dot(ab, ap) * dot(ab, bp) > 0.) return 0.;
  float distance = abs(cross(ab, ap).z) / length(ab);
  return smoothstep(width, .95 * width, distance * 2.);
  return dot(ab, ap) * dot(ab, bp) <= 0. 
    && abs(cross(ab, ap).z) / length(ab) <= width / 2. 
      ? 1. 
      : 0.;
}

显然右侧锯齿会更少一些.

col = mix(
  col, vec3(0., .5, .5),
  smoothstep(1.01, .99, length(uv + .4))
);
col = mix(
  col, vec3(.5, .5, .0),
  length(uv - .4) <= 1. ? 1. : 0.
);

新的网格与函数绘制

网格

vec3 grid(in vec2 uv) {
  vec2 pixel = fwidth(uv);
  vec2 grid = floor(mod(uv, 2.));
  vec3 color = grid.x == grid.y ? vec3(.4) : vec3(.6);
 
  color = mix(
    color,
    vec3(0.),
    smoothstep(2. * pixel.x, pixel.x, abs(uv.x))
      + smoothstep(2. * pixel.y, pixel.y, abs(uv.y))
  );
 
  return color;
}
 
float func(in float x) {
  return smoothstep(0., 1., x) + smoothstep(2., 1., x) - 1.;
}
 
float funcPlot(in vec2 uv) {
  float y = func(uv.x);
  vec2 pixel = fwidth(uv);
  return smoothstep(y - 2. * pixel.y, y, uv.y)
    + smoothstep(y + 2. * pixel.x, y, uv.y)
    - 1.;
}

上面的用线段来画函数有些扯淡, 所以这里改回了正常一些的画法.

二次抽样

#define AA 4
float funcPlot(in vec2 uv) {
  vec2 pixel = fwidth(uv);
 
  float count = 0.;
  for (int m = 0; m < AA; m++) {
    for (int n = 0; n < AA; n++) {
      vec2 offset = 2. * vec2(m, n) / float(AA) - 1.;
      vec2 _uv = uv + offset * pixel;
      float y = func(_uv.x);
      count += smoothstep(
        y - 2. * pixel.y,
        y + 2. * pixel.y,
        _uv.y 
      );
    }
  }
  if (count > float(AA * AA) / 2.) count = float(AA * AA) - count;
  count = count * 2. / float(AA * AA);
  return count;
}

2D SDF

vec2 fixUv(in vec2 c) {
  return 1. * (2. * c - iResolution.xy ) / min(iResolution.x, iResolution.y);
}
 
float sdfCircle(in vec2 p) {
  return length(p) - (.5 + .2 * sin(iTime));
}
 
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = fixUv(fragCoord);
  float d = sdfCircle(uv);
  vec3 color = 1. - sign(d) * vec3(.4, .5, .6);
  color *= 1. - exp(-3. * abs(d));
  color *= .8 + .2 * sin(150. * abs(d)); // contour line
  color = mix(color, vec3(1.), smoothstep(.005, .004, abs(d)));
 
  if (iMouse.z > 0.1) {
    vec2 m = fixUv(iMouse.xy);
    float currentDistance = abs(sdfCircle(m));
    color = mix(color, vec3(1., 1., 0.),smoothstep(.01, 0., abs(length(uv - m) - currentDistance)));
    color = mix(color, vec3(0., 0., 1.),smoothstep(.02, .01, length(uv - m)));
  }
 
  fragColor = vec4(color, 1);
}

3D SDF 与 Ray Marching

定义一个球的 sdf 函数.

float sdfSphere(in vec3 p) {
  vec3 o = vec3(0., 0., 2.);
  return length(p - o) - 1.5;
}

以及球上一点的法线函数.

vec3 normalSphere(in vec3 p) {
  return normalize(p - vec3(0., 0., 2.));
}

这里是一个特例, 对于更普通的图形, 法线函数如下 (https://iquilezles.org/articles/normalsSDF/):

vec3 normalSphere(in vec3 p) {
  const float h = .0001;
  const vec2 k = vec2(1., -1.);
  return normalize(
    k.xyy * sdfSphere( p + k.xyy * h) +
    k.yyx * sdfSphere( p + k.yyx *h) +
    k.yxy * sdfSphere( p + k.yxy * h) +
    k.xxx * sdfSphere( p + k.xxx * h)
  );
}

之后是 rayMatch 函数:

#define TMIN .1
#define TMAX 20.
#define MAX_STEPS 100000
#define PRECISION .001
 
float rayMarch(in vec3 ro, in vec3 rd) {
  float t = TMIN;
  for (int i = 0; i < MAX_STEPS && t <= TMAX; i++) {
    vec3 p = ro + t * rd;
    float d = sdfSphere(p);
    if (d < PRECISION) {
      break;
    }
    t += d;
  }
  return t;
}

渲染函数:

vec3 render(vec2 uv) {
  vec3 color = vec3(0.);
  vec3 ro = vec3(0., 0., -2.);
  vec3 rd = normalize(vec3(uv, 0.) - ro);
  float t = rayMarch(ro, rd);
 
  vec3 light = vec3(2. * cos(2. * iTime), 1., 2. * sin(2. * iTime) + 2.);
  float amp = .5;
 
  if (t < TMAX) {
    vec3 p = ro + t * rd;
    vec3 n = normalSphere(p);
    float dif = clamp(dot(normalize(light - p), n), 0., 1.);
 
    color = sqrt(amp * vec3(0.23) + dif * vec3(1.));
  } else {
    color = vec3(.21 * dot(normalize(light - ro), rd));
  }
  return color;
}

以及重采样:

#define AA 16
vec3 renderSub(vec2 uv) {
  vec2 pixel = fwidth(uv);
 
  vec3 color = vec3(0.);
  for (int m = 0; m < AA; m++) {
    for (int n = 0; n < AA; n++) {
      vec2 offset = 2. * vec2(m, n) / float(AA) - 1.;
      vec2 _uv = uv + offset * pixel;
      color += render(_uv);
    }
  }
  return color / float(AA * AA);
}

输出:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = fixUv(fragCoord);
  vec3 color = vec3(0.);
 
  color = render(uv);
  color = renderSub(uv);
 
  fragColor = vec4(color, 1);
}

History

VersionActionTime
1.0init2022-06-04 00:26:42