中EN
精修课概念页

着色器伪随机:可以重复的混乱

Shader Pseudorandom

更新于 2026-10-15 约 16 分钟GLSL伪随机fract程序化生成设计

代码里哪来的随机

上一课的图案全是规则:每一格都按同样的规矩画。这一课要往里加一点熵。随机是熵最极端的样子,可代码本身是完全确定的,同样的输入总会算出同样的结果。在这样的环境里,怎么造出「看起来很乱」的数?

把正弦波剁碎

从一个熟悉的函数出发:fract(sin(x))。sin 在 −1 到 1 之间起伏,fract 只留下小数部分,结果全落在 0 到 1 之间。

现在把 sin(x) 乘上一个越来越大的数。拖动滑块,一个零一个零地加上去:

y = fract(sin(x) × 10ⁿ)

直接改上面的表达式就能重画:x 是 0 到 1 的横坐标,可以用 sin、pow、smoothstep 等函数。

n = 0 时还看得出正弦的形状;加到 5 个零(乘以 100000),波形被剁成了碎渣,肉眼再也认不出来。

乘上的数越大,sin 的一点点变化在小数位上被放得越大,平滑的正弦就被撕成了毛刺。加到 100000.0 时,就得到了书里 rand() 的骨架:

float rand(float x) {
  return fract(sin(x) * 100000.0);
}

确定性:同一个输入,永远同一个输出

这个 rand 和编程语言里常见的 Math.random() 有一个根本区别:

着色器里的 rand(x)常见的 Math.random()
确定性是:rand(1.0) 永远返回同一个数否:每次调用都可能不同
典型用途程序纹理、给格子分配属性、可复现的生成游戏逻辑里的抽奖、非图形的蒙特卡洛

它够不够「随机」?

用随机数很难拿捏:它有时太乱,有时又不够随机。书里把 rand(x) 画成曲线后指出两个痕迹:

  • 在 ±1.5707(也就是 ±π/2)附近,能看出正弦波峰的影子。那里是 sin 的最大值和最小值,曲线在波峰处最平缓,乘上大数之后变化也最慢,结构就露了出来。
  • 数值的分布在中间比两端略密一些。

Pixelero 写过一篇文章,讨论怎么用塑形函数改变随机数的分布。下面把 600 个随机数分进 24 个桶里,柱子越高,说明落在这一段的数越多。拖动「分布」滑块,换几种塑形方式:

随机数落在哪里:数一数
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

0:原样;1:两个随机数相乘,偏向 0;2:开平方,偏向 1;3:两个随机数取平均,堆向中间。深色横线是完全均匀时的高度。

二维随机:先压成一个数

想对 st 的 x 和 y 都随机,就得先把 vec2 变成一个 float。常用的办法是和一个常数向量做点积:dot(st, vec2(12.9898, 78.233))。两个向量的朝向越接近,点积越大,不同的位置就得到了不同的数,再交给一维的老办法。

二维随机:一屏电视雪花
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

换一换两个常数,雪花就整体换成另一套。试试把常数拖得很小,看看雪花还乱不乱。

用格子驯服雪花

二维随机很像电视雪花,太碎了,很难直接拿来构图。办法是请出上一课的格子:

  1. 把空间放大,比如乘以 10;
  2. i = floor(st) 取出格子的整数坐标,f = fract(st) 留下格子里的局部坐标;
  3. 用 rand(i) 给整格分配一个随机值。因为函数是确定的,同一格里所有像素算出来的都是同一个值;
  4. 再用 f 在格子里画线、画形状。
i = floor(st)整格共用一个 rand(i)f = fract(st)在格子里照常画东西
整数部分 i 决定这一格的随机值(格子的深浅),小数部分 f 是格子里的坐标(黄点)。整数部分带来格与格之间的变化,小数部分保留格子内部的秩序。
每一格一个随机值
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

格内坐标为 0 时只显示 rand(i);拖到 1,红、绿通道换成格子里的坐标 f,每一格都变成一块小渐变。

一行 BASIC 的迷宫

有一句很有名的 BASIC 程序:10 PRINT CHR$(205.5+RND(1)); : GOTO 10。它每次随机打印 ╱ 或 ╲,满屏的斜线就拼成了一座迷宫。

着色器版本几乎一模一样:每一格掷一次硬币,正面画 /,反面画 \。这就是上一课的 Truchet 砖,只是朝向不再按规则挑,而是交给了随机:

10 PRINT 迷宫0.0s
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

每过两秒换一个随机种子,迷宫就整体重排一次。

随机的大师:池田亮司

日本电子音乐人、视觉艺术家池田亮司 (Ryoji Ikeda) 把随机用到了极致。在他的作品里,随机不是刺眼的混乱,而像是一面镜子,映出技术文化的复杂。

书里受他启发出了几道练习:做几行随机的格子,相邻的行朝相反方向移动,只显示比较亮的格子;让每行的速度随时间起伏;用鼠标控制显示的门槛。下面是一个版本:

一排排流动的随机格子0.0s
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

鼠标的横坐标就是亮度门槛:越往右,能显示出来的格子越少。

随机的局限

纯随机很难做出自然的效果。看看雨落下的纹路,或者一张股价走势图,它们都相当随机,却一点也不像这一课开头的那团毛刺。

原因在于:随机值之间毫无关联,而大多数自然现象都「记得」前一刻的状态。下一课的噪声就是为此而来的:它是平滑的、看起来自然的混乱。

在游戏里怎么用

取舍与常见的坑

小结与练习

  • 着色器里的随机是确定性伪随机:fract(sin(x) * 大数),同一输入永远同一输出。
  • 二维随机先用 dot 把 vec2 压成一个数,常数不同,图案也不同。
  • floor 给整格分配同一个随机值,fract 保留格内坐标:前者带来变化,后者保留秩序。

练习:把上一课的砖墙和这一课结合:每块砖的颜色深浅随机,并且随机挑出几块砖做成「破损」的样子,比如缺一个角或者颜色明显更暗。

  1. The Book of Shaders: Random
  2. thebookofshaders.com/10

相关页面

图形学程序化图案Procedural Patterns片元着色器按像素执行:形状重复多少次,计算量几乎不变,因此特别适合平铺图案。核心手法是放大 st 再 fract 得到重复的 [0,1] 子空间,再在格子内画形、做矩阵变换,或按奇偶行错位、按格子旋转拼 Truchet。图形学噪声函数Noise FunctionsKen Perlin 为电影《电子世界争霸战》(Tron) 设计的噪声,解决「看起来随机却不像电视雪花」的程序纹理问题。入门路径是值噪声 (value noise):格子角点取伪随机,格内用平滑曲线插值;进阶有梯度噪声 (gradient noise)、五次插值改进版,以及用单纯形网格降复杂度的 Simplex Noise。游戏数学伪随机数生成Pseudorandom Number Generation游戏引擎到处要用随机数,但随机数生成器 (RNG) 并不真正随机:它只是给出一个复杂但完全确定、预先定义的序列,因此严格说应叫伪随机数生成器 (PRNG)。好坏取决于周期(多久重复)和能否通过各种公认的随机性测试。GEA 介绍了线性同余、梅森旋转、Marsaglia 的 Mother-of-All / Xorshift / KISS99,以及 PCG。图形学片元着色器与 GLSL 基础Fragment Shaders & GLSL Basics片元着色器(fragment shader)是在每个像素(片元)上并行执行的小程序:输入位置等信息,输出颜色。GLSL 是 OpenGL / OpenGL ES / WebGL 共用的着色语言;本书用全屏 billboard 上的像素着色练习,建立程序化图形的基础心智模型。图形学塑形函数Shaping Functions塑形函数把归一化到 [0,1] 的一维输入弯成可控制的曲线,是片元着色器里混合颜色、画边、做动画缓动的「基本功」。GLSL 的 pow/step/smoothstep/sin 等多为硬件加速;掌握一维再扩展到二维空间。图形学分形布朗运动Fractional Brownian Motion分形布朗运动 (fBm) / 分形噪声:把多「八度 (octave)」噪声叠加——频率按空隙度 (lacunarity) 步进升高,振幅按增益 (gain) 衰减——得到跨尺度自相似细节。变体包括湍流(绝对值尖谷)、脊状噪声、乘性/多重分形,以及用噪声扭曲采样坐标的域扭曲 (domain warping)。图形学细胞噪声Cellular NoiseSteven Worley(1996)《A Cellular Texture Basis Function》——书中注明约在 Perlin 原始噪声发表十六年之后、其 Simplex Noise(2001)五年之前——把程序纹理建立在「到最近特征点的距离」上,即细胞噪声 / Worley 噪声。GPU 上用格子 + 邻域搜索代替「对全体点求最小距离」;再多存最近特征点身份即可得到 Voronoi 图。后续有 2×2 邻域加速、精确边界与「voro-noise」等变体。

被这些页面引用2 篇

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原