网格噪声
在上一篇「噪声的艺术」中我们学习了 Value Noise, Gradient Noise 以及 Simplex Noise 这三种创建噪声的方法。
「the book of shaders」还介绍了另一类重要的基于网格的生成方法,能模拟自然界中细胞的纹理。 在深入学习之前,让我们首先来介绍一个非常好用的工具库。
GLSL-Canvas
「the book of shaders」页面上包含了很多使用 Canvas 绘制的例子,相比在 ShaderToy 上写然后在博客中使用 iframe 嵌入引用,要简便许多,因此我也在博客中采用了这种做法。
在 Jekyll 中使用十分简单,例如我想在所有包含了 WebGL tag 的博文中都引入 GLSL-Canvas:
{% if page.tags contains 'WebGL' %}
<script src="/assets/js/glsl-canvas.min.js"></script>
{% endif %}
使用方法十分简单,通过 [data-]
可以传递 vertex/fragment shader 源代码,并且可以传入其中用到的纹理。在运行时 GLSL-Canvas 会异步请求这些静态资源,初始化 WebGL 环境,传入预设的一些 uniform 变量例如 u_time
u_tex0
,随后开始绘制:
<canvas class="glslCanvas" data-fragment-url="/assets/shaders/moon.frag" width="300" height="300" data-textures="/assets/img/webgl/shaders/moon.jpg"></canvas>
使用例子中的 moon.frag
后效果如下:
在介绍网格之前,先来复习一下距离场的概念。
距离场
计算四个象限中,每个象限中的每个 fragment 到某个特征点(例如第一象限的 0.3,0.3)的距离,并通过 frag
保留距离的小数部分形成周期变换效果。
st = st *2.-1.;
d = length(abs(st)-.3);
gl_FragColor = vec4(vec3(fract(d*11.008)),1.0);
这种类似水波纹的“场”的效果如下:
距离场中的“距离”不仅仅限于到某一个固定点的距离,也可以是到某一组特征点集的最小距离。 例如我们定义一组特征点:
vec2 point[5];
point[0] = vec2(0.580,0.660);
point[1] = vec2(0.60,0.07);
point[2] = vec2(0.790,0.640);
point[3] = vec2(0.31,0.26);
point[4] = vec2(0.520,0.020);
计算每个 fragment 到这一组特征点距离的最小值:
float m_dist = 1.; // 保存最小距离
for (int i = 0; i < 5; i++) {
float dist = distance(st, point[i]);
m_dist = min(m_dist, dist);
}
color += m_dist;
// 使用 sin 制造波纹效果
color += step(.7,abs(sin(50.0*m_dist)))*.3;
效果如下:
上面的做法存在一个很明显的问题,当我们需要扩大这种随机效果,就需要增加特征点,也就增加了 for 循环的执行次数。 当特征点集数量变得越来越大时,每个 fragment 计算量都很大,GPU 性能必然不高。有没有办法减少运算量呢?
网格
之前我们在「绘制 Pattern」中已经学到了如何划分空间到一个个小的网格区域。我们可以为每个网格生成一个随机的特征点,对于某一个网格内的 fragment,只需要计算与他所在网格相邻的 8 个网格中特征点的最小距离,这就大大减少了运算量。这就是 Steven Worley 的论文中的主要思想。
生成随机特征点使用了之前学过的 random
方法,由于是确定性随机,每个网格内的特征点是固定的。
// 划分网格
vec2 i_st = floor(st);
vec2 f_st = fract(st);
float m_dist = 1.;
// 8 个方向
for (int y= -1; y <= 1; y++) {
for (int x= -1; x <= 1; x++) {
// 当前相邻的网格
vec2 neighbor = vec2(float(x),float(y));
// 相邻网格中的特征点
vec2 point = random2(i_st + neighbor);
// fragment 到特征点的距离
vec2 diff = neighbor + point - f_st;
float dist = length(diff);
// 保存最小值
m_dist = min(m_dist, dist);
}
}
color += m_dist;
效果如下,注意我们标注出了每个网格的边界,内部的特征点,并结合了 u_time
实时变换特征点的位置:
顺便复习一下直线的画法:
// 画出每个网格白色的特征点
color += 1.-step(.02, m_dist);
// 画出每个网格红色边框
color.r += step(.98, f_st.x) + step(.98, f_st.y);
Voronoi 算法
这个算法也可以从特征点而非像素点的角度理解。在那种情况下,算法可以表述为:每个特征点向外扩张生长,直到它碰到其它扩张的区域。这反映了自然界的生长规则。生命的形态是由内部扩张、生长的力量和限制性的外部力量共同决定的。模拟这种行为的算法以 Georgy Voronoi 命名。
在使用这个算法绘制具体图案时,除了保存最小距离,还可以额外保存当前 fragment 到最近的特征点的向量。使用这个向量我们可以进行着色。在下面的例子中,我们使用这个向量表示 rg 分量:
color.rg = m_point;
// 也可以得到一个灰度值
color += dot(m_point,vec2(.3,.6));
效果如下,当鼠标代表的特征点向一个已有特征点移动时,颜色也慢慢接近,正是“近朱者赤”。
注意 Steven Worley 的原始方法中,每个网格的特征点数是可变的,对大多数网格来说不止一个。在他的 C 语言实现中,这是用来提早退出来加速循环。GLSL 循环不允许动态的迭代次数,所以你可能更希望一个网格对应一个特征点。
优化 Voronoi
Stefan Gustavson 优化了 Steven Worley 的算法,除了使用到特征点的最小距离,还使用了第二小距离。主要优化点来自相邻网格的选择,对一个 2x2 的矩阵作遍历(而不是 3x3 的矩阵)。这显著地减少了工作量,但是会在网格边缘制造人工痕迹。
使用 2x2 网格 + 距离最小值效果如下:
其他版本具体实现可以参考 webgl-noise。包括使用 3x3 网格以及使用最小距离 F1 和第二小距离 F2。
比如使用 2x2 网格 + 距离最小值 F1 + 第二小距离 F2 效果如下:
顺便提一句,在 WebGL 中也可以使用 glslify 进行类似 Node.js 的模块管理。
#pragma glslify: noise = require('glsl-noise/simplex/3d')