ShaderToy 快速入门
by acdzh · 2022年6月3日16:26 · 127 WORDS · ~ 1 mins reading time · 0 Visitors |
简介与坐标系
文章整理自 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 colorvec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));// Output to screenfragColor = vec4(col,1.0);}
坐标系
mainImage
接受两个参数: fragColor
和 fragCoord
. fragColor
是画布的颜色, fragCoord
是画布上的坐标, 画布的坐标从左下角开始, 往右是 x 轴, 往上是 y 轴, 以像素为单位.
我们可以把坐标归一化: vec2 uv = fragCoord / iResolution.xy;
, 这样的话, uv.x
和 uv.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.);}
线段
我们有一个线段, 起点是 , 终点是 . 线段的宽度是 . 对于每一个点, 假设其坐标是 , 如果我们需要再该点绘制一条线段, 需要满足下面的条件:
- 到 的距离小于等于 .
- 保证画出来的是线段不是直线.
对于 1, 我们可以用下面的方式判断:
对于 2, 只需要满足 且 即可. 因此函数如下:
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.;}// mainImagecol = mix(col,vec3(0., 0., 1.),segment(uv, vec2(-2.5, -.5), vec2(2.5, 1.5), .1));
其他函数
我们把前面的一些操作抽象一下, 整理如下:
#define PI 3.141592654vec2 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.);}// mainImagecol = 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 4float 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 linecolor = 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 .001float 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 16vec3 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
Version | Action | Time |
---|---|---|
1.0 | init | 2022-06-04 00:26:42 |