news 2026/10/15 20:20:56

The Book of Shaders 噪声算法实战指南:从 1D 值噪声到 Simplex Noise 的 GLSL 实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
The Book of Shaders 噪声算法实战指南:从 1D 值噪声到 Simplex Noise 的 GLSL 实现

【免费下载链接】thebookofshaders

Step-by-step guide through the abstract and complex universe of Fragment Shaders.

项目地址:https://gitcode.com/gh_mirrors/th/thebookofshaders
点击查看免费下载

本文以 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 lines

wood.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.

项目地址:https://gitcode.com/gh_mirrors/th/thebookofshaders
点击查看免费下载
上一篇:Open Generative AI:免费开源的AI视频生成工具,400+模型完整使用指南
下一篇:zanePerfor邮件预警系统:流量峰值监控与智能告警配置指南 🚀

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 20:19:11

激光智能焊接技术推动拼板制造迈向高效新时代

在船舶制造、压力容器、钢结构等工业领域,拼板焊接是核心工艺之一。传统拼板焊接依赖人工经验,存在效率低、质量波动大、劳动强度高等痛点。随着激光技术与智能算法的深度融合,镭烁光电以高精度焊缝跟踪系统与自适应焊接工艺为核心&#xff0…

作者头像 李华
网站建设 2026/10/15 20:19:09

Python实现Pacman搜索算法:BFS、DFS、UCS与A*完整解析

简介:本资源是UC Berkeley经典AI课程中Pacman搜索项目(Search Project)的完整Python实现与解析方案,面向人工智能初学者、高校AI课程学习者及算法实践者,聚焦基础搜索算法在游戏智能体中的建模与应用。压缩包共23个文件…

作者头像 李华
网站建设 2026/10/15 20:19:00

激光焊缝缺陷的智能检测技术及应用

在高端制造领域(如汽车、航空航天、新能源等),焊缝质量直接影响产品的结构强度和使用寿命。传统人工检测效率低、主观性强,而X射线、超声波等常规手段存在成本高、操作复杂等问题。随着激光焊接技术的普及,基于激光视觉…

作者头像 李华
网站建设 2026/10/15 20:18:58

融合图上下文与自注意力:会话推荐新范式解析

1. 从标题说起:这个问题为什么值得读拿到这篇论文标题的时候,我第一反应是"又来了个把注意力机制往会话推荐上套的工作"。但通读下来,这个"Graph Contextualized"的前缀确实不是噱头——它解决的是一个长期被忽视的痛点&…

作者头像 李华
网站建设 2026/10/15 20:18:04

ASN1C生成C代码实战:从ASN.1协议到BER/DER/PER编解码避坑指南

简介:面向需要将ASN.1规范转换为C代码的嵌入式或通信协议开发者,这份资源提供了基于ASN1C工具链的完整实例包。作者通过自定义raw_circle.asn文件,演示了如何用asn1c命令生成对应的C源码,并围绕RawCircle结构体实现BER/DER、PER及…

作者头像 李华