UsubeniFantasy
10678
阅读量
173
收录文章
1521
建站天数

对 fragment shader 的一点点见解

发布于 2022-10-12 10:44

注意事项:

  • 本文仅涉及 WebGL1.0
  • 本文仅涉及 fragment shader
  • 本文重点参考对象:The Book of Shaders
  • 一些图片来源于 open.gl
  • 下面所有例子都可以贴到这个 Playground 直接运行

图形流水线

本质

从上面流水线的图也能看出来,fragment shader 的本质就是对每个(像素)点作处理。大家都知道 GPU 擅长并行的简单计算,目的就是要做好这件事。相反,CPU 擅长复杂计算,但处理器却很少。

The Book of Shaders 给出一个形象的比喻:

CPU

GPU

uniform

uniform 之所以叫 uniform 是因为它是统一的。由 CPU 传到 GPU,在处理每一个像素时,这个值都是统一的。相对的是 varying,这些值由 vertex shader 计算后传到 fragment shader,针对每个三角形,收到的值是不一致的。

输入输出

uniform vec2 u_resolution;

void main() {
	// `gl_FragCoord.xy/u_resolution` 将 xy 值映射为 0 到 1
	vec2 st = gl_FragCoord.xy/u_resolution;
	gl_FragColor = vec4(st.x,st.y,0.0,1.0);
}

gl_FragCoord 是内置输入,表示一个点的位置;gl_FragColor 是固定的输出值,表示这个点应该显示为何种颜色。

映射

关键输入是位置,关键输出是颜色,那么很明显,要使用 fragment shader 绘图的重点就是找到位置和颜色的关系,也可以说 GPU 要做的就是要计算每个点的颜色。再换一个说法就是你要发挥你的聪明才智,利用位置 x 和 y 的关系生成一个好看的图像。

所以为什么 0 到 1 的映射这么重要呢,因为 webgl 的 rgb 输出就是 0 到 1 的范围,这是构建位置和颜色关系最直接的方式。此外,像 smoothstepfract 这样的重要内置函数都返回范围为 0 到 1 的值。

在上面的例子中,首先将 gl_FragCoord 映射为 01,然后分别把 x 和 y 值放到 r 和 g,(左下角是 0,0)所以出来的图像左下角黑色,左上角红色,右下角绿色。

P.S. WebGL2 的输出方式不一样

常用函数

为了画出各种花里胡哨的图形,我们必须了解这些常用的函数:

角度和三角函数相关

指数相关

常用数学方法

  • abs() 绝对值
  • sign(x)  把 x 映射为 0、1、-1
  • floor() 向下取整
  • ceil() 向下取整
  • fract()  只取小数部分
  • mod(x, y)  对 x 取模
  • min(x, y) 返回两值中较小值
  • max(x, y) 返回两值中较大值
  • clamp(x, minVal, maxVal)  仅在最大最小值范围内变化,超出范围时输出边界值
  • mix(x, y, a) 从 x 渐变到 y,a 是变换的“进度”
  • step(edge, x)  一种突变,将 x 映射为 0 到 1,小于 edge 返回 0,否则返回 1
  • smoothstep(edge0, edge1, x) 在 edge0 和 edge1 的区间内,将 x(使用 Hermite interpolation)丝滑地映射为 0 到 1,需要注意的是改变 edge0 和 edge1 顺序的话效果是不一样的

GEOMETRIC FUNCTIONS

可以开始画了吗!

可以!在了解上面一堆常用函数之后,你已经可以画出出神入化的图像了!例如下面这样的简短的代码就能营造一种熔岩灯的感觉(看效果戳这里):

// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

float snoise(vec2 v) {
    const vec4 C = vec4(0.211324865405187,  // (3.0-sqrt(3.0))/6.0
                        0.366025403784439,  // 0.5*(sqrt(3.0)-1.0)
                        -0.577350269189626,  // -1.0 + 2.0 * C.x
                        0.024390243902439); // 1.0 / 41.0
    vec2 i  = floor(v + dot(v, C.yy) );
    vec2 x0 = v -   i + dot(i, C.xx);
    vec2 i1;
    i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    i = mod289(i); // Avoid truncation effects in permutation
    vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
        + i.x + vec3(0.0, i1.x, 1.0 ));

    vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
    m = m*m ;
    m = m*m ;
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
    vec3 g;
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.x *= u_resolution.x/u_resolution.y;
    vec3 color = vec3(0.0);
    vec2 pos = vec2(st*3.);

    float DF = 0.0;

    // Add a random position
    float a = 0.0;
    vec2 vel = vec2(u_time*.1);
    DF += snoise(pos+vel)*.25+.25;

    // Add a random position
    a = snoise(pos*vec2(cos(u_time*0.15),sin(u_time*0.1))*0.1)*3.1415;
    vel = vec2(cos(a),sin(a));
    DF += snoise(pos+vel)*.25+.25;

    color = vec3( smoothstep(.7,.75,fract(DF)) );

    gl_FragColor = vec4(1.0-color,1.0);
}

开玩笑的,离独自构思出这种图像还有很大一段距离 😂

0 到 1 有几种走法

在实际使用中,必定不可能只存在线性关系,所以利用各种函数调整 xy 和 0 到 1 的映射,是学习编写 fragment 不能绕过的一步。

The Book of Shaders 中,作者使用 plot 函数将映射关系可视化,同时将 x 值的映射赋给 gl_FragColor,这样可以比较直观地看到不同曲线的实际效果:

// Author: Inigo Quiles
// Title: Pcurve

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

//  Function from Iñigo Quiles
//  www.iquilezles.org/www/articles/functions/functions.htm
float pcurve( float x, float a, float b ){
    float k = pow(a+b,a+b) / (pow(a,a)*pow(b,b));
    return k * pow( x, a ) * pow( 1.0-x, b );
}

float plot(vec2 st, float pct){
  return  smoothstep( pct-0.02, pct, st.y) -
          smoothstep( pct, pct+0.02, st.y);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution;

    float y = pcurve(st.x,3.0,1.0);

    vec3 color = vec3(y);

    float pct = plot(st,y);
    color = (1.0-pct)*color+pct*vec3(0.0,1.0,0.0);

    gl_FragColor = vec4(color,1.0);
}

渲染结果可以直接点这里

方与圆

float rect (vec2 st,float width){
     // bottom-left
    vec2 bl = step(vec2(0.5-width/2.),st);
    float pct = bl.x * bl.y;

    // top-right
    vec2 tr = step(vec2(0.5-width/2.),1.0-st);
    pct *= tr.x * tr.y;
    return pct;
}

float circle(in vec2 _st, in float _radius){
    vec2 dist = _st-vec2(0.5);
	return 1.-smoothstep(_radius-(_radius*0.01),
                         _radius+(_radius*0.01),
                         dot(dist,dist)*4.0);
}

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 rectcolor = vec3(rect(st,.5));
    vec3 circlecolor = vec3(1) - vec3(circle(st,.5));

    gl_FragColor = vec4(circlecolor + rectcolor,1.0);
}

方圆是最容易理解的图形,上面的代码又几个需要注意的点:

  • 方形的代码有一点长,不过其实注意一下以乘法制造交集就好了
  • 例子中使用的点积画圆形,因为点积就是两个向量的投影关系,所以自己投到自己身上就是一种计算长度的方法
  • 当然你也可以使用 lengthdistance 甚至自己用两点间距离算法来画圆
  • 画圆形(或者说曲线)也不是不可以用 step,只是使用 smoothstep 会有抗锯齿效果
  • 程序员的好朋友抽象永远能帮你实现更高效简洁的代码,你可以轻易调个函数画出复杂的图形(即使暂时没看懂绘图的原理)

小星球

一些手机图片处理应用会有一个叫“小世界”的滤镜,其实就是把图片从笛卡尔坐标映射成极坐标。如果我们希望用 fragment shader 画出雪花、齿轮等图形,可以根据同样的道理把对应的“波形”映射到极坐标,代码如下:

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);

    vec2 pos = vec2(0.5)-st;

    float r = length(pos)*2.0;
    float a = atan(pos.y,pos.x);

    float f = cos(a*3.); // 3 瓣
    // f = abs(cos(a*3.)); // 6 瓣
    // f = abs(cos(a*2.5))*.5+.3; // 5 瓣
    // f = abs(cos(a*12.)*sin(a*3.))*.8+.1; // 雪花
    // f = smoothstep(-.5,1., cos(a*10.))*0.2+0.5; // 齿轮

    color = vec3( 1.-smoothstep(f,f+0.02,r) );

    gl_FragColor = vec4(color, 1.0);
}
  • a 通过反正切求出点与中心连线的角度,相当于原本的 X 轴,返回值为 -PI 到 PI
  • r 可以理解为原本的 Y 轴
  • f 则是 Y 轴的值,color 就是用 smoothstep 分割 f,小于 f 显黑,大于 f 显白
  • 所以顺眼的写法其实是 float y = cos(x*3.);
  • 通过将不同函数映射到极坐标可以产生各种有意思的形状

Pattern

这里的 Pattern 不是设计模式,而是指重复的图案。之前讲的都是单个图案,那么怎么批量生成一大堆排列整齐的图案呢?

答案是fract()

void main() {
	vec2 st = gl_FragCoord.xy/u_resolution;
    vec3 color = vec3(0.0);

    st *= 3.0;      // Scale up the space by 3
    st = fract(st); // Wrap around 1.0

    // Now we have 9 spaces that go from 0-1

    color = vec3(st,0.0);
    // color = vec3(circle(st,0.5));

	gl_FragColor = vec4(color,1.0);
}

人类的本质是复读机,而 fract() 可以算是 GLSL 的复读机了吧 😅,通过 fract() 可以让数值在 0 到 1 间的小数中不断循环。

  • 首先将 xy 值映射为 0 到 1
  • 再将其映射为 0 到 3
  • 经过 fract() 的处理,就会变成复读 3 次 0 到 1,效果就出来了

伪随机

对于生成图像,也不涉及金钱交易什么的,伪随机就够用了,至于真正的随机,用到硬件噪声等无法预测的输入,这里暂不讨论。

y = fract(sin(x)*1.0); 就是伪随机的基础(之一)。虽然从连续的图像上看,非常规律,但是要是以 x 为整数,取 y,作为一个正常人类未必能反应出来这就是一个普通的 sin,我们使用 JavaScript 输出一下整数作为参数输出的“随机值”:

function fract(num) {
  return Math.abs(num - Math.trunc(num))
}
function random(x) {
  return fract(Math.sin(x) * 1)
}
const nums = []
for (let i = 1; i < 20; i++) {
  nums.push(random(i))
}
// output=> [0.8414709848078965, 0.9092974268256817, 0.1411200080598672, 0.7568024953079282, 0.9589242746631385, 0.27941549819892586, 0.6569865987187891, 0.9893582466233818, 0.4121184852417566, 0.5440211108893698, 0.9999902065507035, 0.5365729180004349, 0.4201670368266409, 0.9906073556948704, 0.6502878401571168, 0.2879033166650653, 0.9613974918795568, 0.7509872467716762, 0.14987720966295234]

在这个基础上,sin(x) 的乘数越大,结果看起来就越随机:

现在我们知道可以用 y = fract(sin(x)*10000.0); 把 x 映射为伪随机的 0 到 1,那么接下来就很简单了吧?把像素位置映射为随机数,然后把随机数设置为输出颜色,便能看到随机图像。

float random (vec2 st) {
    return fract(sin(dot(st.xy,
                         vec2(12.9898,78.233)))*
        10000.0);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
	// st = st * 15.;
	// st = floor(st);
    float rnd = random( st );

    gl_FragColor = vec4(vec3(rnd),1.0);
}

上面的代码可以绘制出类似雪花屏的效果,需要注意的是 random 函数中的“x”是用 xy 值与一个向量求点积(投影长度)所得。如果想要马赛克也很简单,用之前的复读方法映射一下就完事啦(去掉两个注释可以看到效果)!

其他函数

矩阵相关

向量相关

材质相关

工具

参考

对 fragment shader 的一点点见解 | 寻我