网格噪声

在上一篇「噪声的艺术」中我们学习了 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')

参考资料