【免费下载链接】thebookofshaders
Step-by-step guide through the abstract and complex universe of Fragment Shaders.
本文以 The Book of Shaders 第 11 章(11/README.md)为骨架,系统讲解片段着色器(Fragment Shader)中噪声(Noise)算法的完整演进路线:从 1D/2D 值噪声(Value Noise)的插值原理,到消除"块状感"的梯度噪声(Gradient Noise),再到 Perlin 改进曲线与 Simplex Noise 单形网格的实现细节。读完本文,你将能独立写出float noise(float x)/float noise(vec2 st)函数,并掌握如何用噪声驱动空间旋转、距离场和形状调制,生成木纹、泼溅、熔岩灯等有机风格的生成式纹理。
从随机到噪声:真实感纹理的突破口
上一章(第 10 章:生成式设计)中我们学会了用fract(sin(x)*100000.0)一类的技巧制造"伪随机":当乘法因子足够大时,正弦波的连续性被小数部分彻底打碎,输出近似电视雪花(白噪声)。但当我们观察路面、岩石、树木与云朵的表面(见 11 章纹理样例)时会发现:这些天然纹理虽然"不可预测",却不是那种刺眼的随机——它们具有某种连续的、有机的起伏感。
这正是 Ken Perlin 在 1980 年代初为电影《电子世界争霸战》(Tron)制作更真实纹理时面对的问题。他给出的答案是后来获得奥斯卡技术奖的Perlin 噪声算法:不直接使用随机值,而是在"随机值之间做平滑插值"。这个思路后来被统称为值噪声(Value Noise)。
从源码结构看,本书仓库在第 10 章提供了基础随机函数(如 10/1d-random.frag 中的float random(in float _x)),第 11 章的 1d-noise.frag 正是建立在这一基础之上。
1D Noise:值噪声的第一步
整数与小数分解:floor() 与 fract()
一维噪声的核心操作,是把一个连续的浮点数x拆成两部分:
float i = floor(x); // 整数部分:确定落在哪个"格子" float f = fract(x); // 小数部分:格子内的局部位置floor()给出每个整数的唯一归属,对i应用rand()后,每个整数格子都会得到一个确定性的随机值(同一个i永远返回同一个随机数);fract()保留格子内的连续位置,用于后续插值。
三种插值方式对比
有了"每格一个随机值"和"格子内的连续坐标"之后,剩下的问题就是如何让相邻格子间的值平滑过渡。文档给出了三个递进版本:
1. 无插值(仅随机阶梯):y = rand(i)——相邻整数之间是硬跳变,这就是第 10 章的"随机"本身。
2. 线性插值:
y = mix(rand(i), rand(i + 1.0), f);用小数部分f在rand(i)与rand(i + 1.0)之间做线性混合。此时曲线是连续的,但在每个整数格点处存在明显的折角(斜率突变)。
3. smoothstep 插值:
y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f));smoothstep(0.,1.,f)在两端导数归零,消除了格点处的折角,波峰与波谷之间的过渡变得圆润。这也是很多噪声实现的标准做法。
4. 自定义三次曲线:部分实现不用smoothstep(),而手写等价的立方 Hermite 曲线:
float u = f * f * (3.0 - 2.0 * f); // 自定义三次曲线 y = mix(rand(i), rand(i + 1.0), u); // 用它做插值f*f*(3.0-2.0*f)与smoothstep(0.,1.,f)在数学上完全等价(f(x) = 3x² - 2x³)。
完整实现:1d-noise.frag
仓库中的 11/1d-noise.frag 把上述思路封装成了一个可复用的函数:
float random (in float x) { return fract(sin(x)*1e4); } float noise (in float x) { float i = floor(x); float f = fract(x); // Cubic Hermite Curve float u = f * f * (3.0 - 2.0 * f); return mix(random(i), random(i + 1.0), u); }其main()中通过y = noise(st.x*3.+u_time)让一维噪声随时间横向滚动,再用第 5 章学过的plot()函数把曲线绘制出来。
练习:让噪声动起来
文档给出了这一阶段的核心练习,目标是理解"平滑随机"的表现力:
- 编写自己的
float noise(float x)函数; - 用噪声函数驱动一个形状的平移、旋转或缩放(动画);
- 用噪声让多个形状"共舞",做成动态构图;
- 用噪声构造"有机感"的形状;
- 有了自己的"生物"之后,再为它设计独特的运动方式,把它发展成角色。
2D Noise:四角插值的值噪声
从两点到四角
1D 噪声在一条线段的两个端点(rand(x)与rand(x+1.0))之间插值;2D 噪声则升级为在平面正方形网格的四个角之间插值:
rand(st)、rand(st)+vec2(1.,0.)、rand(st)+vec2(0.,1.)、rand(st)+vec2(1.,1.)
同理,3D 噪声需要在立方体的八个角之间插值。这种"在随机值之间做插值"的技术,正是**值噪声(Value Noise)**名称的由来(参考 11/3d-noise.frag 中基于 8 个角点的三重嵌套mix)。
与 1D 一样,2D 的插值同样不是线性的,而是采用三次曲线,使网格内任意位置的过渡都平滑连续。
逐行解析 2d-noise.frag
仓库中的 11/2d-noise.frag 是完整的教学实现,逐段拆解如下。
随机函数(第 10-14 行):用dot()把二维坐标投影成一维标量后再打碎:
float random (in vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); }噪声函数(第 18-38 行):
float noise (in vec2 st) { vec2 i = floor(st); // 格子的整数坐标 vec2 f = fract(st); // 格子内的分数坐标 // 取 2D 格子四个角的随机值 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)); // 三次 Hermite 曲线,与 smoothstep() 等价 vec2 u = f*f*(3.0-2.0*f); // u = smoothstep(0.,1.,f); // 对四个角的值做双向插值 return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y; }关键点:
- 第 23-26 行:用格子的整数坐标算出四个角的坐标并各取一个随机值;
- 第 31 行:用三次曲线平滑分数坐标(可替换为
smoothstep(0.,1.,f),效果一致); - 第 35-37 行:按
u.x、u.y对四个随机值做双线性插值,得到格内任意一点的连续噪声值。
主函数(第 40-51 行):vec2 pos = vec2(st*5.0)将屏幕空间放大 5 倍,使单个格子放大到肉眼可辨,从而观察插值效果;float n = noise(pos)后直接以灰度输出gl_FragColor = vec4(vec3(n), 1.0)。
扩展到 3D:八角插值
仓库中的 11/3d-noise.frag 展示了三维值噪声:用floor(p)/fract(p)得到整数与分数部分,对立方体的 8 个角((0,0,0)到(1,1,1))分别取随机值,再做三重嵌套mix。其main()中vec3 pos = vec3(st*5.0, u_time*0.5)把时间作为第三个维度,从而得到随时间连续演化的三维噪声场。
练习:缩放、鼠标、距离场与 Rothko
文档为 2D 噪声准备了以下探索练习:
- 改变第 45 行的缩放倍数(
st*5.0),并尝试用u_time或u_mouse驱动它; - 观察:放大到什么程度噪声重新变得像随机?什么程度下噪声几乎不可见?
- 把噪声函数接到鼠标坐标上;
- 如果把噪声的梯度当作距离场使用,能做出什么有趣的东西?
- 综合运用矩形、颜色与噪声,做一幅带有 Mark Rothko 绘画复杂性的构图。
值噪声与梯度噪声:消除"块状感"
前面的练习会暴露值噪声的一个明显缺点:由于每个格子内的插值曲线呈"鼓包"状,输出看起来偏"块状"(blocky)。为消除这一现象,1985 年 Ken Perlin 开发了另一种实现——梯度噪声(Gradient Noise)。
两者核心区别:
| 对比维度 | 值噪声(Value Noise) | 梯度噪声(Gradient Noise) |
|---|---|---|
| 随机对象 | 每个格角一个随机数值float | 每个格角一个随机方向vec2 |
| 插值内容 | 直接插值随机值 | 用方向与格内偏移做点积后再插值 |
| 视觉效果 | 相对"块状" | 更平滑、方向感更自然 |
在 11/2d-vnoise.frag(值噪声版)中,随机函数返回单个浮点:
float random2(vec2 st){ st = vec2( dot(st,vec2(127.1,311.7)), dot(st,vec2(269.5,183.3)) ); return -1.0 + 2.0 * fract( sin( dot( st.xy, vec2(12.9898,78.233) ) ) * 43758.5453123); }而在 11/2d-gnoise.frag(梯度噪声版)中,同样的random2()返回-1.0 ~ 1.0的vec2 方向向量,插值前先做点积:
float noise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); vec2 u = f*f*(3.0-2.0*f); return mix( mix( dot( random2(i + vec2(0.0,0.0) ), f - vec2(0.0,0.0) ), dot( random2(i + vec2(1.0,0.0) ), f - vec2(0.0,1.0) ), u.x), mix( dot( random2(i + vec2(0.0,1.0) ), f - vec2(0.0,1.0) ), dot( random2(i + vec2(1.0,1.0) ), f - vec2(1.0,1.0) ), u.x), u.y); }dot(random2(corner), f - corner)的含义是:用该格角的随机方向与"当前点相对该角的位置向量"做点积,得到该角对当前点的一个有向贡献,再在四个角之间做三次插值。正因为插值的是"方向影响"而非原始数值,梯度噪声消除了值噪声的块状感。下图可直观对比两者差异(左:值噪声,右:梯度噪声):
从源码结构看,11/2d-vnoise.frag 与 11/2d-gnoise.frag 主体结构几乎一致,唯一差异在于
random2()的返回值类型(floatvsvec2)以及是否进行点积运算,这正是值噪声与梯度噪声的分水岭。
像画家理解颜料的特性一样,理解各种噪声实现之间的差异,才能更好地选用它们。
生成式设计实战:把噪声变成纹理
文档给出了三种把噪声用于生成式设计的经典手法,仓库中均有完整可运行的着色器。
手法一:旋转空间造木纹
思路:用二维噪声值作为旋转角度,先旋转坐标空间,再在其中绘制直线。来自 11/wood.frag 的关键两行:
pos = rotate2d( noise(pos) ) * pos; // rotate the space pattern = lines(pos,.5); // draw lineswood.frag中lines()函数绘制条带(基于sin(pos.x*3.1415)的平滑阶梯),rotate2d()构造旋转矩阵;噪声为每个局部区域提供不同的旋转量,最终把直条带"拧"成木纹年轮般的涡旋纹理(成品见 11/wood-long.png)。
手法二:把噪声当作距离场:泼溅效果
思路:把噪声函数当作一个"标量场/距离场",用第 7 章(形状章节)学过的smoothstep()阈值技巧切出斑块。来自 11/splatter.frag:
color += smoothstep(.15,.2,noise(st*10.)); // 黑色泼溅 color -= smoothstep(.35,.4,noise(st*10.)); // 泼溅上的孔洞两个阈值区间(.15~.2与.35~.4)的带通组合,产生了"墨点内部再掏空"的层次感;取消注释st += noise(st*2.)*t;(配合t = abs(1.0-sin(u_time*.1))*5.)还能让整个墨迹场缓慢流动(成品见 11/splatter-long.png)。
手法三:用噪声调制形状:波浪圆环
思路:把噪声叠加进形状的半径或角度计算中,让几何边缘"呼吸"。仓库的 11/circleWave-noise.frag 在极坐标下绘制圆环,并把噪声注入半径与角度:
float m = abs(mod(a+u_time*2.,3.14*2.)-3.14)/3.6; m += noise(st+u_time*0.1)*.5; // 角度方向加噪声 f += sin(a*50.)*noise(st+u_time*.2)*.1; // 半径方向加噪声 return 1.-smoothstep(f,f+0.007,r);圆环边缘在噪声驱动下随时间波动,形成"呼吸感"。
练习:花岗岩、大理石与 Jackson Pollock
- 选三种你感兴趣的真实纹理(如花岗岩、大理石、岩浆、水),分别用噪声实现它们的算法化版本;
- 用噪声调制一个形状;
- 回到第 8 章矩阵章节,把其中"+"号平移的例子,改为用随机与噪声共同驱动运动;
- 生成一幅 Jackson Pollock 风格的泼溅作品。
Improved Noise:从三次曲线到五次曲线
Perlin 对原始非单形噪声的第一次改进,是把三次 Hermite 曲线(f(x) = 3x²-2x³,即smoothstep())替换为五次插值曲线:
// Cubic Hermite Curve. Same as SmoothStep() y = x*x*(3.0-2.0*x); // Quintic interpolation curve y = x*x*x*(x*(x*6.-15.)+10.);即f(x) = 6x⁵-15x⁴+10x³。这条曲线在两端更加"平坦"(一阶、二阶导数在端点为 0),使每个格子边界能与相邻格子优雅地缝合,得到更连续的过渡。
仓库中 Stefan Gustavson 的经典实现 11/2d-pnoise.frag 里,fade()函数就是这条五次曲线的直接体现:
vec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }该文件同时包含cnoise()(经典 Perlin 噪声)与pnoise()(带显式周期的周期性变体,通过Pi = mod(Pi, rep.xyxy)实现),3D 版本见 11/3d-pnoise.frag 中的vec3 fade(vec3 t)。
Simplex Noise:单形噪声的原理与实现
为什么需要 Simplex:2^N 角的计算开销
按值噪声的思路,N 维需要插值 2^N 个角点:2D 是 4 个(正方形角),3D 是 8 个,4D 是 16 个。Perlin 在 Siggraph 2001 提出的Simplex Noise解决了这一问题,其宣称的改进包括:
- 更低的计算复杂度与更少的乘法运算;
- 扩展到更高维度时计算成本增长更小;
- 消除方向性伪影(directional artifacts);
- 梯度定义良好且连续、计算廉价;
- 易于在硬件中实现。
单形网格:N+1 个角
Perlin 的关键洞察是:虽然正方形是"显而易见"的铺满二维空间的形状,但二维空间中最简单的形状其实是等边三角形。N 维的**单形(simplex)**是恰好有N+1 个角的形状——于是 2D 只需插值 3 个角(少算 1 个),3D 只需 4 个角(少算 4 个),4D 只需 5 个角(少算 11 个),计算量大幅下降。
网格斜切:simplex-grid.frag 逐行解析
如何构造单形网格?11/simplex-grid.frag 展示了构造过程:把正方形网格的每个格子沿对角线一分为二成两个等腰三角形,再斜切(skew)直至每个三角形成为等边三角形。
vec2 skew (vec2 st) { vec2 r = vec2(0.0); r.x = 1.1547*st.x; r.y = st.y+0.5*r.x; return r; }vec3 simplexGrid (vec2 st) { vec3 xyz = vec3(0.0); vec2 p = fract(skew(st)); if (p.x > p.y) { // "下"三角形 xyz.xy = 1.0-vec2(p.x,p.y-p.x); xyz.z = p.y; } else { // "上"三角形 xyz.yz = 1.0-vec2(p.x-p.y,p.y); xyz.x = p.x; } return fract(xyz); }要点:
skew()中的常量1.1547与0.5对应斜切变换系数(1.1547 ≈ 2/sqrt(3));- 第 22 行通过比较
p.x > p.y判断当前点位于斜切后的"下三角形"还是"上三角形"; - 主函数中依次取消注释第 44 行(
color.rg = fract(skew(st)))与第 47 行(color = simplexGrid(st)),即可亲眼看到网格从普通方格被斜切、再被细分为等边三角形的过程。
Ashima Arts 的 GLSL 实现:2d-snoise-clear.frag
仓库中的 11/2d-snoise-clear.frag 是 Ian McEwan(Ashima Arts)与 Stefan Gustavson 的二维 Simplex 噪声 GLSL 实现。虽然对教学而言略显"过度优化",但结构清晰、代码短小快速。核心步骤:
// 预计算斜切三角网格的常量 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 // 第一个角:把输入 v 投影到斜切坐标并取整 vec2 i = floor(v + dot(v, C.yy)); vec2 x0 = v - i + dot(i, C.xx); // 其余两个角:通过 x0.x > x0.y 判断所在三角形 vec2 i1 = vec2(0.0); i1 = (x0.x > x0.y)? vec2(1.0, 0.0):vec2(0.0, 1.0); vec2 x1 = x0.xy + C.xx - i1; vec2 x2 = x0.xy + C.zz;随后用mod289/permute做无纹理的置换(避免数组与纹理依赖,便于在 GPU 上执行),用max(0.5 - dot(x,x), 0.0)计算各角的影响权重(四次幂m*m; m*m形成平滑衰减),并以130.0 * dot(m, g)归一化输出。
三维版本见 11/3d-snoise.frag:其梯度采样采用"7×7 点阵映射到八面体"的策略(mod(p, 7*7)、h = 1.0-abs(x)-abs(y)),最后以42.0 * dot(...)归一化。两个文件都标注为 Ashima Arts / Stefan Gustavson 的 MIT 许可实现。
练习:熔岩灯与易经纹理
文档鼓励把 Simplex Noise 当作创作原料:
- 逐一观察每种噪声实现的外观,把它们想象成雕塑家的原石——眯起眼睛,在噪声中寻找形状与联想,然后动手用代码实现;
- 做一个能投射"流动"幻觉的着色器,如熔岩灯、墨滴、水流——仓库范例 11/lava-lamp.frag 用多层
snoise()的叠加(DF += snoise(pos+vel)*.25+.25)配合速度场与fract()生成流动墨滴; - 用 Simplex Noise 为已有作品添加质感——仓库范例 11/iching-03.frag 在易经六边形图案上叠加
snoise(vec3(st*75.,t*0.1))*0.03,让像素级纹理产生细微抖动。
观察与实践:把噪声变成创作材料
本章的价值不仅在于算法本身,更在于建立起"随机 → 平滑随机 → 可控生成"的创作链条。无论值噪声、梯度噪声还是 Simplex Noise,它们都是生成式设计的基础原材料。若想进一步精进,可参考以下仓库内资料:
- 完整讲解与源码:章节主文档 11/README.md,全部着色器位于 11/ 目录;
- 随机数基础:第 10 章生成式设计 及 10/1d-random.frag;
- 形状绘制基础:第 7 章形状(距离场技巧)、第 8 章矩阵(坐标变换);
- 词汇查询:glossary 词汇表;
- 在线编辑运行:仓库根目录的 edit.php 支持以
edit.php#11/2d-snoise-clear.frag的形式直接加载任意.frag示例。
值得补充的工具箱:LYGIA 的 generative functions 提供了一套可复用的 GLSL 生成函数集合,涵盖随机、噪声与各类生成式图案,设计上强调可复用性、性能与灵活性,可以方便地集成到各类项目和框架中。
最后,如同文末 Bob Ross 所说:"和树说话,和它做朋友。"掌握噪声需要同样多的观察力与动手能力。走出门去,观察自然里那些错综复杂的纹理——你的观察力需要和你的实现技巧一样(甚至更多地)被刻意练习。
本文内容基于 The Book of Shaders 第 11 章文档与仓库内对应 GLSL 源码整理而成;所涉算法细节以仓库实际代码为准。
【免费下载链接】thebookofshaders
Step-by-step guide through the abstract and complex universe of Fragment Shaders.
相关推荐
The Book of Shaders 精读:GLSL 噪声算法从 1D 值噪声到 Simplex Noise 的完整实现指南
The Book of Shaders 精读:GLSL 噪声算法从 1D 值噪声到 Simplex Noise 的完整实现指南 导读 在着色器(Fragment
The Book of Shaders 噪声指南:从值噪声、梯度噪声到 Simplex Noise 的 GLSL 实战
The Book of Shaders 噪声指南:从值噪声、梯度噪声到 Simplex Noise 的 GLSL 实战 本指南围绕 The Book of Sh
The Book of Shaders 深度解析:GLSL 噪声(Noise)算法——从值噪声到 Simpleks 噪声的完整实战指南
The Book of Shaders 深度解析:GLSL 噪声(Noise)算法——从值噪声到 Simpleks 噪声的完整实战指南 导读 本篇文章以《The
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考