引言 shader 编程是图形学编程中的重要环节,在 shader 代码中可以实现各种各样简单复杂的计算机图形学算法。本文通过列举一些简单的 shader 代码实例,看看能不能激发你学习的shader编程的兴趣。此外shader 的灵活性让我们除了实现各种传统的算法,还可以发挥想象力实现更多有趣的图形。
这不是一个从零开始的着色器教程 ,关于着色器网络上也有很多优秀的资料教程,读者可以提前搜索一下。不过这里还是带大家了解一下和本文相关的基础知识。
基础知识(熟悉可跳过)
着色器程序一般分为两种,一个是顶点着色器(vertex shader),一个是片元着色器(fragment shader)。本文主要涉及到的是片元着色器。
片元着色器编程的过程可以简单抽象为:**gl_FragColor = fragment_shader(gl_FragCoord, …other_parameters)**,其中fragment_shader就是我们需要编写的片元着色器代码。gl_FragColor则是输出的某个像素点颜色,gl_FragCoord是某个点的坐标,以及other_parameters表示各个来源的其他入参。
编写着色器程序用到的一门编程语言叫做glsl,语法和C系语言很像,且是强类型。
一些常用的基本类型:int整型,float浮点数,vec(2/3/4)分别表示二维、三维、四维向量。(本文涉及到最多的是vec2)。
uniform变量:
这种方式定义的变量,其值不是由着色器来决定的,而是由外部环境通过某种方式传入的。例如web中用js通过的webGL Api或者是桌面端c++通过openGL Api等等。所以这种变量在着色器中通常都是拿过来用的只读变量。
全局变量
gl_FragCoord:该变量由运行环境给出,表示当前坐标
gl_FragColor:该变量由运行环境给出,通常会被赋值为最终输出的颜色
分类实例 每一个实例都是由图片 + 代码 + 小段解释 + 在线demo组成。
Color Map
precision highp float ; uniform vec2 u_size; void main() { vec2 rg_size = gl_FragCoord .xy / u_size; gl_FragColor = vec4 ( 0.0 , rg_size.x, rg_size.y, 1.0 ); }
gl_FragCoord 表示的是当前片元着色器处理的片元(暂时理解为像素)的坐标,是一个vec4类型的变量(x, y, z, 1/w),目前你只需要知道gl_FragCoord.xy 表示取出第1和2两个分量,分别表示水平坐标x和纵坐标y(坐标原点位于左下角)。
这一段代码非常的简单,首先通过除以画布尺寸将坐标限制在[0, 1]的范围之内,然后将得到的结果的xy分量分配给保留的全局变量gl_FragColor 。也就是,无论你前面进行了多么复杂的计算过程,最终都是要通过这一个全局变量来将颜色输出,否则你的着色器程序是没有意义的。
关于颜色分布,我们可以看几个特殊点——四个角。左下角vec4(0.0, 0.0, 0.0, 1.0)黑色,左上角vec4(0.0, 0.0, 1.0, 1.0)蓝色,右下角vec4(0.0, 1.0, 0.0, 1.0)绿色,右上角vec4(0.0, 1.0, 1.0, 1.0)青色。
你可以尝试调换0.0、rg_size.x、rg_size.y三者的位置,看看会得到什么样不同的颜色分布。
Demo: https://observablehq.com/d/cd4c47d15af19c1f
三角函数曲线
precision highp float ; #define PI 3.14159265359 uniform vec2 u_size; uniform float u_dpr; float smoothstep_filter(float value, float target){ return smoothstep (target - 0.01 , target, value) - smoothstep (target, target + 0.01 , value); }void main() { float rate = 100.0 * u_dpr; vec2 st = gl_FragCoord .xy / vec2 (rate); vec3 color = vec3 (0.0 ); vec3 line_color = vec3 (38.0 , 204.0 , 213.0 ) / vec3 (255.0 ); float y = sin (st.x) + (u_size.y / rate) * 0.5 ; float percent = smoothstep_filter(st.y, y); color = mix (color, line_color, percent); gl_FragColor = vec4 (color, 1.0 ); }
平时学习一些图像绘制的时候总是绕不开的一个东西就是三角函数曲线,以正弦为例,y = sin(x),这是一个周期为2PI,振幅为1的正弦函数。不过想要在shader中绘制这样一根简单的曲线,似乎还不那么直观。
首先,我们定义了一个函数plot 用与判断当前坐标是否在目标值的范围内,返回一个[0, 1]之间平滑后的值。这里用到了一个shader内置的函数——smoothstep,用于生成平滑的插值。
然后在main 函数当中调用plot 函数得到颜色混合的百分比percent ,最后再调用一个shader内置函数mix ,该函数用于线性插值。将插值得到的颜色赋给gl_FragColor 。
同样你可以尝试不同的三角函数,例如余弦函数、正切函数等等。
Demo: https://observablehq.com/d/43753a27f77906b3
极坐标曲线
precision highp float ; #define PI 3.14159265359 uniform vec2 u_size; uniform float u_dpr; float step_filter(float value, float taget_value){ return step (taget_value, value) - step (taget_value + 0.2 , value); }vec3 color_1 = vec3 (45.0 , 89 , 198 ) / vec3 (255.0 );vec3 color_2 = vec3 (49 , 142 , 222 ) / vec3 (255.0 );vec3 color_3 = vec3 (38 , 205 , 213 ) / vec3 (255.0 );vec3 color_4 = vec3 (118 , 224 , 214 ) / vec3 (255.0 );void main() { vec2 st = gl_FragCoord .xy/u_size.xy; vec3 color = vec3 (0.0 ); vec2 pos = vec2 (0.5 ) - st; float distance = length (pos)*2.0 ; float alpha = atan (pos.y,pos.x); float f = tan (alpha * 5. ); color = mix (color, color_1, step_filter(distance , f)); gl_FragColor = vec4 (color, 1.0 ); }
绘制极坐标曲线的关键在于得到当前点到坐标原点的距离distance 以及目标距离值f ,然后通过step_filter 函数获取需要插值的值,最后用mix 函数进行插值,将得到的颜色赋值给gl_FragColor。
这里用到的极坐标表达式为 f = tan(alpha * 5),你还可以尝试其他的函数例如 f = 1.0,这将绘制一个正圆。
Demo: https://observablehq.com/d/0ed9b475e8bc72af
噪声
precision highp float ; #define PI 3.14159265359 uniform vec2 u_size; float random (in vec2 st) { return fract (sin (dot (st.xy, vec2 (12.9898 ,78.233 ))) * 43758.5453123 ); }float noise (in vec2 st) { vec2 i = floor (st); vec2 f = fract (st); float a = random(i); float b = random(i + vec2 (1.0 , 0.0 )); float c = random(i + vec2 (0.0 , 1.0 )); float d = random(i + vec2 (1.0 , 1.0 )); vec2 u = f*f*(3.0 -2.0 *f); return mix (a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y; }void main() { vec2 st = gl_FragCoord .xy/u_size.xy; vec2 pos = vec2 (st*400.0 ); float n = noise(pos); gl_FragColor = vec4 (vec3 (n), 1.0 ); }
这个例子是关于噪声的。我们首先定义了两个函数,一个random 函数,用于根据一个二维向量生成一个float随机值,然后定义了一个noise 函数,用于根据二维向量生成一个float噪声值。
然后,我们通过noise 函数来对放大后的实际坐标pos 进行干扰,将干扰得到的结果n ,赋值给gl_FragColor ,最终出来的效果如上图(笔者感觉很像小时候看的彩电没有型号的时候的画面)。
Demo: https://observablehq.com/d/7ff962a5ca69b910
分形
precision mediump float ;uniform vec2 u_size;uniform float u_time;float fixedTime = u_time / 2.0 ;float distanceToMandelbrot(in vec2 c) {#if 1 { float c2 = dot (c, c); if (256.0 * c2 * c2 - 96.0 * c2 + 32.0 * c.x - 3.0 < 0.0 ) return 0.0 ; if (16.0 * (c2 + 2.0 * c.x + 1.0 ) - 1.0 < 0.0 ) return 0.0 ; }#endif float di = 1.0 ; vec2 z = vec2 (0.0 ); float m2 = 0.0 ; vec2 dz = vec2 (0.0 ); for (int i = 0 ; i < 300 ; i++) { if (m2 > 1024.0 ) { di = 0.0 ; break ; } dz = 2.0 * vec2 (z.x * dz.x - z.y * dz.y, z.x * dz.y + z.y * dz.x) + vec2 (1.0 , 0.0 ); z = vec2 (z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; m2 = dot (z, z); } float d = 0.5 * sqrt (dot (z, z) / dot (dz, dz)) * log (dot (z, z)); if (di > 0.5 ) d = 0.0 ; return d; }void main() { vec2 p = (2.0 * gl_FragCoord .xy - u_size.xy) / u_size.y; float tz = 0.5 - 0.5 * cos (0.225 * fixedTime); float zoo = pow (0.5 , 13.0 * tz); vec2 c = vec2 (-0.05 , .6805 ) + p * zoo; float d = distanceToMandelbrot(c); d = clamp (pow (4.0 * d / zoo, 0.2 ), 0.0 , 1.0 ); vec3 col = vec3 (d); gl_FragColor = vec4 (col, 1.0 ); }
这个例子放在这里是觉得很有趣,且为了图形分类的完整性。有兴趣深入研究其算法的可以看看这篇https://www.iquilezles.org/www/articles/distancefractals/distancefractals.htm 。(这段代码我就不解释,因为笔者也没怎么看懂)
Demo: https://observablehq.com/d/2a14b96ee57a4800
图形pattern
precision mediump float ;uniform vec2 u_size;uniform float u_time;float fixedTime = u_time / 2.0 ;float width_1 = 2.0 ;float width_2 = 2.0 ;float f_x = 100.0 ;float f_y = 100.0 ;void main() { vec3 color = vec3 (1.0 ); if (mod (gl_FragCoord .x, f_x) <= width_1) { color = vec3 (0.0 ); } if (mod (gl_FragCoord .y, f_y) <= width_2) { color = vec3 (0.0 ); } gl_FragColor = vec4 (color, 1.0 ); }
pattern通常是由周期性重复的图案组成,这里的例子很简单,周期性最小重复单元可以看作是一个黑色的方框。
主要逻辑分为两个部分,一个是x方向,另一个是y方向。用到了条件if语句和内置求余函数mod 。该实例建议打开demo链接调节几个参数看看实际的效果会发生什么变化。
Demo: https://observablehq.com/d/5f64a6701696c782
时间系数
precision highp float ; uniform vec2 u_size; uniform float u_time; void main() { vec2 rg_size = gl_FragCoord .xy / u_size; gl_FragColor = vec4 ( abs (sin (u_time)), rg_size.x, rg_size.y, 1.0 ); }
截止到目前的例子都是静态的图形,在静态的基础上加上一个随时间变化的量u_time 即可作出简单的动画效果。
这里我们以最简单的Color Map 为例,将之前的0.0 替换为 abs(sin(u_time)) 即可得到一个简单的颜色渐变动画效果。如果是更加复杂的动画效果思路都是类似的——将时间系数引入到颜色计算的过程当中去。
Demo: https://observablehq.com/d/dbee4f41ec1c28bc
彩蛋——组合 看完了上面的“基础”,我们很容易想到将某些方式进行组合,也许会产生一些新的效果。下面就是笔者自己组合出的一些结果,有些挺出乎意料的。(代码都比较长,而且与前面的实例有很大部分重叠,就不放代码了,看代码直接去到链接。)
三角函数 + 时间系数
Demo: https://observablehq.com/d/8800b295dd539047
Pattern + 噪声
Demo: https://observablehq.com/d/a3dce179e339ee5a
极坐标曲线 + 噪声
Demo: https://observablehq.com/d/a5d33e8a3b5566c4
三角函数 + 噪声
Demo: https://observablehq.com/d/a36424c0af13a391
三角函数 + 过滤函数 +噪声
ps:这线段上的四种颜色你眼熟吗(狗头)
Demo: https://observablehq.com/d/adfc9b466b450ff9
More and more… 组合方式远不止笔者列举出的这些,更多可能性留给读者自己去探究。
参考
https://thebookofshaders.com www.shadertoy.com
总结 shader编程思想和语法相对稳定,学习了shader编程你既可以给webGL编写shader也可以给openGL编写shader,甚至是游戏引擎环境下编写shader等等。并且其对于像素的控制能力可以让你做出很多惊人的效果。
当然了,我们学习shader编程并非只是是为了写这些“花哨”的图形,而是让你对于渲染有了更强的控制能力。