HLSL 随机数生成方法
以 CasualPRT 项目
SurfelSampleCS.compute中的实际用法为切入点,总结四种常用 GPU 随机数生成方法:sin-dot 哈希、Hammersley 低差异序列、PCG Hash、Dave Hoskins 系列 hash。整理日期:2026-10-03
0. 为什么 GPU 随机数不一样
CPU 上通常用带状态的 RNG(如 Mersenne Twister、Xoshiro),但 GPU 上成千上万个线程并行执行,维护全局 RNG 状态代价高。因此 GPU 随机数的主流做法是:
- 无状态哈希(stateless hash):把坐标/索引直接哈希成
[0,1)的浮点数,线程之间零耦合; - 少量状态的流式 RNG:每个线程持有一个
uint状态,逐步推进(PCG 是典型); - 低差异序列(quasi-random):根本不追求"随机",而是确定性、均匀分层的采样点分布。
另一个 GPU 特有的坑:浮点精度。所有基于 float 的哈希都依赖"大数乘法后取 frac 丢掉高位"来制造混沌,一旦编译器把变量优化成 half(移动端很常见),精度直接崩掉,输出会出现明显 banding。整数哈希(PCG)则完全没有这个问题,且跨驱动/跨平台位级一致。
1. sin-dot 哈希 —— rand(float2 uv)
来源:Stack Overflow: Random noise functions for glsl
float rand(float2 uv)
{
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}
逐层拆解
| 步骤 | 作用 |
|---|---|
dot(uv, float2(12.9898, 78.233)) |
把 2D 输入投影到固定方向,压缩成一个标量。两个系数是"搅拌"用的魔数,无特殊含义 |
sin(...) |
映射到 [-1, 1]。sin 是高频振荡函数,输入微小变化 → 输出剧烈变化,起到打乱作用 |
* 43758.5453 |
放大到很大的数值范围。GPU 浮点精度有限,大数乘法让低位精度大量丢失,进一步打散 bit |
frac(...) |
只留小数部分,得到 [0, 1) |
本质:利用浮点精度损失的混沌效应做廉价哈希。相同输入永远得到相同输出(确定性),不同输入的输出看起来均匀分布。
项目中的原始用法(SurfelSampleCS.compute)
float2 xy = float2(id.x, id.y) / float2(32, 16); // 线程 ID 归一化
xy += float2(1, 1) * _randSeed; // 逐帧偏移,换一组随机数
float u = rand(xy * 1.0); // 乘不同系数得到"独立"的两路随机
float v = rand(xy * 2.0);
float3 dir = UniformSphereSample(u, v); // u, v → 球面均匀方向
优缺点
- ✅ 一行搞定,零整数运算,写起来最快
- ✅ 教科书级流行,Shadertoy/教程里随处可见
- ❌ 质量差:大网格上有可见的相关性/条纹图案
- ❌ 精度敏感:
half/min16float下 frac 之前精度全丢,出现 banding;不同驱动的 sin 实现结果还可能不一致 - ❌ sin 本身是较贵的超越函数
适用:原型、demo、对质量无要求的场合。
2. Hammersley 低差异序列 —— UE4 MonteCarlo.usf
来源:Unreal Engine 4, Engine/Shaders/MonteCarlo.usf(现已迁移为 Private/MonteCarlo.ush)
关键认知:UE 的 MonteCarlo.usf 里根本没有 sin-hash 这类函数。UE 采样分布函数(UniformSampleSphere 等)只接受现成的随机数 E,而 E 由 Hammersley 序列生成。
// ref: Unreal Engine 4, Common.ush
// 32 位比特反转:Hammersley 序列的核心操作
// E2 的本质是 Van der Corput 序列(radical inverse),让相邻 Index 的采样点天然错开
uint ReverseBits32(uint bits)
{
bits = (bits << 16) | (bits >> 16);
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
return bits;
}
// ref: Unreal Engine 4, MonteCarlo.usf
float2 Hammersley(uint Index, uint NumSamples, uint2 Random)
{
float E1 = frac((float)Index / NumSamples + float(Random.x & 0xffff) / (1 << 16));
float E2 = float(ReverseBits32(Index) ^ Random.y) * 2.3283064365386963e-10;
return float2(E1, E2);
}
原理
- E1:均匀格点
Index/NumSamples+ 种子抖动(Random.x低 16 位归一化到 [0,1)); - E2:位反转即 Van der Corput 序列(radical inverse base-2)——Index=1,2,3,4… 映射到 0.5, 0.25, 0.75, 0.125…,相邻索引的点天然错开;异或种子再做 scrambling,逐帧换一套点分布;
2.3283064365386963e-10就是1/2^32。
与白噪声的本质区别:白噪声每个点是独立随机的,而低差异序列是确定性、分层均匀的——512 个点在 [0,1)² 上的覆盖比 512 个白噪声点均匀得多,蒙特卡洛积分收敛更快。
UE 的采样管线
// UE: E = Hammersley(i, N, Random) → dir = UniformSampleSphere(E).xyz
uint surfelIndex = id.x * 16 + id.y; // 序列索引兼缓冲区下标
float2 E = Hammersley(surfelIndex, 512, uint2(asuint(_randSeed * 157.0), asuint(_randSeed * 911.0)));
float3 dir = UniformSphereSample(E.x, E.y);
NumSamples = 512 = 32×16 线程总数,每线程取序列中一个点,球面覆盖天然分层均匀。
注意:NumSamples 要等于真实总采样数。Index > NumSamples 时 E1 的格点会回绕,破坏分层性。
优缺点
- ✅ 收敛快,同等采样数下方差显著低于白噪声
- ✅ 确定性 + 种子可扰动,烘焙类任务(PRT、SH、IBL 预积分)首选
- ✅ 只需位运算和一次 frac,很便宜
- ❌ 不是白噪声——做 TAA/Denoiser 输入时时间域上会"周期感",需要换序列或抖动
- ❌ E1 的抖动上限只有 16 bit 精度(UE 的实现取舍)
适用:离线/预计算积分、重要性采样、任何"固定 N 个样本摊平球面/半球/圆盘"的场景。
3. PCG Hash —— 整数哈希的性价比之王
来源:Melissa O'Neill, PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation;GPU 社区通用版本出自 Nathan Reed (reedbeta) 的博客 与 Jarzynski & Olano 论文 Hash Functions for GPU Rendering (JCGT 2020)。
Blender EEVEE、AMD Capsaicin、Unity 衍生项目里都是同一个实现:
// ref: reedbeta.com "Hash Functions for GPU Rendering" / pcg-random.org
uint pcg_hash(uint input)
{
uint state = input * 747796405u + 2891336453u;
uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
return (word >> 22u) ^ word;
}
变体一:每线程带状态的流式 RNG
PCG 本来是带状态的生成器,每线程持有一个 uint 即可:
uint pcg_random(inout uint rngState)
{
rngState = rngState * 747796405u + 2891336453u;
uint word = ((rngState >> ((rngState >> 28u) + 4u)) ^ rngState) * 277803737u;
return (word >> 22u) ^ word;
}
// 用法:uint rng = surfelIndex * 2654435769u + asuint(_randSeed);
// float r0 = pcg_random(rng) * (1.0 / 4294967296.0);
// float r1 = pcg_random(rng) * (1.0 / 4294967296.0); // 连续取不重复
变体二:多通道无状态哈希 pcg3d
需要 2~4 个"独立"随机数时,别再像 sin-hash 那样乘系数凑——直接用向量化版本(Jarzynski & Olano):
// ref: Jarzynski & Olano, "Hash Functions for GPU Rendering" — pcg3d
uint3 pcg3d(uint3 v)
{
v = v * 1664525u + 1013904223u;
v.x += v.y * v.z;
v.y += v.z * v.x;
v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z;
v.y += v.z * v.x;
v.z += v.x * v.y;
return v;
}
// 用法:uint3 r = pcg3d(uint3(surfelIndex, asuint(_randSeed), 0));
// float3 rand01 = (float3)r * (1.0 / 4294967296.0);
uint → float 的正确姿势
float rand01 = float(pcg_hash(seed)) * (1.0 / 4294967296.0); // 2^-32,[0,1)
优缺点
- ✅ 统计质量顶级:通过 TestU01 BigCrush;Jarzynski 论文全维度评测中质量/速度综合最优
- ✅ 纯整数运算:无精度坑、
half优化不影响它、跨驱动位级一致(回放/比对调试时极其重要) - ✅ 极快:两条乘加 + 移位异或,延迟低于一次 sin
- ✅ 状态仅 4 字节,流式取任意多随机数
- ❌ 代码不如 sin-hash 直观,需要理解"线性同乘 + 输出置换"的结构
适用:实时渲染里的通用随机(TAA 抖动、光线追踪、Path Tracer、SSAO 噪声),想要"就一个靠谱默认值"时选它。
4. hash21 / hash12 —— Dave Hoskins 系列
来源:Dave Hoskins, Hash without Sine(2013 博文 + Shadertoy 系列)。Hoskins 的动机很直接:把 sin 从哈希里去掉,规避移动端 sin 精度问题并提速。
经典版 hash21(Shadertoy 社区流传最广的形式)
// ref: Dave Hoskins
float hash21(float2 p)
{
p = frac(p * float2(123.34, 345.45));
p += dot(p, p + 34.345);
return frac(p.x * p.y);
}
思路:先 frac 截断输入、再让 p 与自身做 dot 产生"自反馈"乘积 p.x * p.y,最后一次 frac 输出。没有 sin,全靠乘法链的精度丢失制造混沌。
新版 "Hash without Sine"(0.1031 常量系列,质量更好)
// 1 out, 2 in
float hash12(float2 p)
{
float3 p3 = frac(float3(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.x + p3.y) * p3.z);
}
// 2 out, 2 in(直接返回 float2,不用乘系数凑独立随机数)
float2 hash22(float2 p)
{
float3 p3 = frac(float3(p.xyx) * float3(.1031, .1030, .0973));
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.xx + p3.yz) * p3.zy);
}
同系列还有 hash11(1→1)、hash13(3→1)、hash33(3→3),常量家族固定为 0.1031 / 0.1030 / 0.0973 / 33.33。
用法对照(替换项目里的 sin-hash)
float u = hash12(xy + _randSeed);
float v = hash12(xy + _randSeed + 17.17);
// 或一次出两路:
float2 uv = hash22(xy + _randSeed);
优缺点
- ✅ 无 sin:移动端安全,比 sin-hash 快
- ✅
hash22一类直接返回多维输出,通道间独立性比"乘 2.0 再 hash 一遍"好得多 - ✅ 图案与相关性明显少于 sin-hash
- ❌ 仍是 float 运算链:输入坐标很大时 frac 精度衰减(世界坐标建议先减去 tile 原点/取模),且跨驱动不保证位级一致
- ❌ 质量仍不及整数哈希 PCG
适用:Shadertoy 风格的程序化噪声、Value Noise 的晶格哈希、低维度小坐标域的日常着色。
5. 横向对比
| sin-hash | Hammersley (UE) | PCG Hash | hash21/12 (Hoskins) | |
|---|---|---|---|---|
| 类型 | 白噪声哈希(浮点) | 低差异序列(拟随机) | 整数哈希 / 流式 RNG | 白噪声哈希(浮点) |
| 分布质量 | 差(有条纹、相关性) | 分层均匀,收敛最快 | 优秀(BigCrush 级) | 中等偏上 |
| 多维输出 | 乘系数凑,通道相关 | 天然 2D | pcg3d/4d 向量化,独立性好 | hash22 直接多维 |
| 精度风险 | 高(half 崩、sin 实现差异) | 低(核心是整数位运算) | 无(纯整数,跨驱动一致) | 中(大坐标 frac 衰减) |
| 速度 | 慢(sin) | 极快(移位掩码 + 1 次 frac) | 极快(乘加 + 移位) | 快(无 sin) |
| 确定性/可扰动 | 种子偏移 | 种子 scrambling | 种子/状态任意 | 种子偏移 |
| 典型用途 | 原型、demo | 烘焙、重要性采样、MC 积分 | 实时渲染通用默认 | 噪声函数、着色小技巧 |
6. 选型建议
- 烘焙/预计算(SH、PRT、IBL 预积分) → Hammersley。固定样本数摊匀球面/半球,收敛快是硬指标。本项目的 Surfel 采样就属于这类。
- 实时渲染的通用随机(TAA 抖动、路径追踪、AO 噪声) → PCG。质量与速度的最佳平衡,纯整数无跨平台坑;要多个随机数用
pcg3d,要连续流式取样用inout uint状态版。 - 噪声函数(Value/Simplex 晶格哈希)、小坐标域 → Hoskins
hash12/hash22。快、够用、无 sin。 - 原型验证/一次性效果 → sin-hash,能跑就行,别带进生产。
- 绝对不要:把 sin-hash 写进会被编译成
half的移动端管线;用"乘 2.0 再 hash"的方式伪造独立多维随机数(用hash22/pcg3d代替)。
7. 参考
- Stack Overflow: Random noise functions for GLSL
- Unreal Engine 4 源码:
Engine/Shaders/MonteCarlo.usf(新版Engine/Shaders/Private/MonteCarlo.ush) - Melissa O'Neill: PCG 随机数家族
- Nathan Reed: Hash Functions for GPU Rendering
- Jarzynski & Olano, Hash Functions for GPU Rendering, JCGT 2020(论文 + Apfelkuchen/hash-functions 代码库)
- Dave Hoskins: Hash without Sine(2013;Shadertoy 系列实现)
- Reed Beta: Quick And Easy GPU Random Numbers In D3D11(Wang hash / 扰动思想出处)