提示
教程例子都可以到下面网址进行运行,不需要另外安装软件环境:
官方提供在线编写shader工具:https://thebookofshaders.com/edit.php
glslsandbox网站:http://glslsandbox.com/
shadertoy网站:https://www.shadertoy.com/
Noise 噪声
我们感知得到浮动在皮肤上的空气,晒在脸上的阳光。世界如此生动而丰富。颜色,质地,声音。当我们走在路上,不免会注意到路面、石头、树木和云朵的表面的样子。
这些纹理的不可预测性可以叫做“random"(随机),但是它们看起来不像是我们之前玩的 random。{真实世界}是如此的丰富而复杂!我们如何才能才能用计算机模拟这些多样的纹理呢?
这就是 Ken Perlin 想要解答的问题。在20世纪80年代早期,他被委任为电影 “Tron”(电子世界争霸战)制作现实中的纹理。为了解决这个问题,他想出了一个优雅的算法,且获得了奥斯卡奖(名副其实)。
下面这个并不是经典的 Perlin noise 算法,但是这是一个理解如何生成 noise 的好的出发点。
float i = floor(x); // 整数(i 代表 integer)
float f = fract(x); // 小数(f 代表 fraction)
y = rand(i); //rand() 在之前的章节提过
// y = mix(rand(i), rand(i + 1.0), f);
// y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f));
默认生成一个随机数图形
- 问题是随机数之间没有过度,直接就从另一个随机数变化到了另一个随机数
-
解决方案:y = mix(rand(i), rand(i + 1.0), f); 将i和i+1之间进行过度,因为每段的百分比就是变量f存储的值,自然可以产生过度
- 但还是不够平滑,值与值之间的变化不存在平滑过渡。
-
解决方案:y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f)); 通过这种方式可以得到近似平滑的曲线(真正平滑还要考虑相邻曲线的曲率)
- 试试把f换成 float u = f * f * (3.0 - 2.0 * f ); 会发现也产生曲线,而且和第三幅图一样,因为smoothstep就是这个公式
2D Noise
现在我们知道了如何在一维使用 noise,是时候进入二维世界了。在 2D 中,除了在一条线的两点(fract(x) 和 fract(x)+1.0)中插值,我们将在一个平面上的方形的四角(fract(st), fract(st)+vec2(1.,0.), fract(st)+vec2(0.,1.) 和 fract(st)+vec2(1.,1.))中插值。
同样,如果我们想要在三维中使用 noise,就需要在一个立方体的8个角中插值。这个技术的重点在于插入随机值,所以我们叫它 value noise。
就像一维的那个例子,这个插值不是线性的,而是三次方的,它会平滑地在方形网格中插入点。
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
// 2D Random
float random (in vec2 st) {
return fract(sin(dot(st.xy,
vec2(12.9898,78.233)))
* 43758.5453123);
}
// 2D Noise based on Morgan McGuire @morgan3d
// https://www.shadertoy.com/view/4dS3Wd
float noise (in vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
// Four corners in 2D of a tile
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));
// Smooth Interpolation
// Cubic Hermine Curve. Same as SmoothStep()
vec2 u = f*f*(3.0-2.0*f);
// u = smoothstep(0.,1.,f);
// Mix 4 coorners percentages
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_resolution.xy;
// Scale the coordinate system to see
// some noise in action
vec2 pos = vec2(st*5.0);
// Use the noise function
float n = noise(pos);
gl_FragColor = vec4(vec2(n),.5, 1.0);
}
我们先把空间大小变成五倍(第 45 行)以便看清栅格间的插值。然后在 noise 函数中我们把空间分成更小的单元。我们把它的整数部分和非整数部分都储存在这个单元里。我们计算整数位置的顶点的坐标,并给每个顶点生成一个随机值(第 23 - 26 行)。最后,在第 35 行用我们之前储存的小数位置的值,在四个顶点的随机值之间插值。
- 每个被分割的格子,四个顶点以及四条公共边获得的随机值是相同的,可被近似平滑
生成式设计中的 noise 应用
Noise 算法的设计初衷是将难以言说的自然质感转化成数字图像。在目前我们看到的一维和二维的实践中,都是在random values(随机值)之间插值,所以它们才被叫做 Value Noise,但是还有很多很多获取 noise 的方法……
https://thebookofshaders.com/edit.php#11/2d-vnoise.frag
如你所见,value noise 看起来非常“块状”。为了消除这种块状的效果,在 1985 年 Ken Perlin 开发了另一种 noise 算法 Gradient Noise。Ken 解决了如何插入随机的 gradients(梯度、渐变)而不是一个固定值。这些梯度值来自于一个二维的随机函数,返回一个方向(
vec2
格式的向量),而不仅是一个值(float
格式)。点击下面的图片查看代码,看这个函数是如何运作的。
https://thebookofshaders.com/edit.php#11/2d-gnoise.frag
花一分钟来看看 Inigo Quilez 做的两个例子,注意 value noise 和 gradient noise的区别。
就像一个画家非常了解画上的颜料是如何晕染的,我们越了解 noise 是如何运作的,越能更好地使用 noise。比如,如果我们要用一个二维的 noise 来旋转空间中的直线,我们就可以制作下图的旋涡状效果,看起来就像木头表皮一样。同样地,你可以点击图片查看代码。
https://thebookofshaders.com/edit.php#11/wood.frag
mix( mix( random( i + vec2(0.0,0.0) ),
random( i + vec2(1.0,0.0) ), u.x),
mix( random( i + vec2(0.0,1.0) ),
random( i + vec2(1.0,1.0) ), u.x), u.y);
- 先对顶部和底部进行水平插值, 在对这两条线进行垂直插值, 就得到了连续的扭曲~~ 精神污染,脑壳疼
另一种用 noise 制作有趣的图案的方式是用 distance field(距离场)处理它,用用 第七章提到的招数。
https://thebookofshaders.com/edit.php#11/splatter.frag
color += smoothstep(.15,.2,noise(st*10.)); // 黑色的泼溅点
color -= smoothstep(.35,.4,noise(st*10.)); // 泼溅点上的洞
第三种方法是用 noise 函数来变换一个形状。这个也需要我们在第七章学到的技术。
https://thebookofshaders.com/edit.php#11/circleWave-noise.frag
Simplex Noise
对于 Ken Perlin 来说他的算法所取得的成功是远远不够的。他觉得可以更好。在 2001 年的 Siggraph(译者注:Siggraph是由美国计算机协会{计算机图形专业组}组织的计算机图形学顶级年度会议)上,他展示了 “simplex noise”,simplex noise 比之前的算法有如下优化:
- 它有着更低的计算复杂度和更少乘法计算。
- 它可以用更少的计算量达到更高的维度。
- 制造出的 noise 没有明显的人工痕迹。
- 有着定义得很精巧的连续的 gradients(梯度),可以大大降低计算成本。
- 特别易于硬件实现。
我们已经知道在二维中他是如何在四个点(正方形的四个角)之间插值的;所以没错你已经猜到了,对于三维(这里有个示例)和四维我们需要插入 8 个和 16 个点。对吧?也就是说对于 N 维你需要插入 2 的 n 次方个点(2^N)。但是 Ken 很聪明地意识到尽管很显然填充屏幕的形状应该是方形,在二维中最简单的形状却是等边三角形。所以他把正方形网格(我们才刚学了怎么用)替换成了单纯形等边三角形的网格。
这时 N 维的形状就只需要 N + 1 个点了。也就是说在二维中少了 1 个点,三维中少了 4 个,四维中则少了 11 个!巨大的提升!
在二维中插值过程和常规的 noise 差不多,通过在一组点之间插值。但是在这种情况下,改用单纯形网格,我们只需要给总共 3 个点插值。
这个单纯形网格是如何制作的?这是另一个聪明绝顶而十分优雅的做法。可以先把常规的四角网格分成两个等腰三角形,然后再把三角形歪斜成等边三角形。
另一个 Simplex Noise 的优化是把三次 Hermite 函数(Cubic Hermite Curve:f(x) = 3x2-2x3,和
smoothstep()
一样)替换成了四次 Hermite 函数( f(x) = 6x5-15x4+10x^3 )。这就使得函数曲线两端更“平”,所以每个格的边缘更加优雅地与另一个衔接。也就是说格子的过渡更加连续。你可以取消下面例子的第二个公式的注释,亲眼看看其中的变化(或者看这个例子)。
// 三次 Hermite 曲线。和 SmoothStep() 一样
y = x*x*(3.0-2.0*x);
// 四次 Hermite 曲线
y = x*x*x*(x*(x*6.-15.)+10.);
所有这些进展汇聚成了算法中的杰作 Simplex Noise。下面是这个算法在 GLSL 中的应用,作者是 Ian McEwan,以这篇论文发表,对于我们的教学而言太复杂了,但你可以点开看看,也许没有你想象得那么晦涩难懂。
https://thebookofshaders.com/edit.php#11/2d-snoise-clear.frag
-
做一个 shader 来表现流体的质感。比如像熔岩灯,墨水滴,水,等等。
https://thebookofshaders.com/edit.php#11/lava-lamp.frag
-
用 Simplex Noise 给你现在的作品添加更多的材质效果。
https://thebookofshaders.com/edit.php#11/iching-03.frag