ShaderToy 快速入门

by acdzh · 2022年6月3日16:26 · 127 WORDS  ·  ~ 1 mins reading time · 0  Visitors | 

简介与坐标系

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
随便写写 © 2022 acdzh