GLSL中的随机/噪声函数

230
由于GPU驱动程序供应商通常不会在GLSL中实现noiseX,因此我正在寻找一组“图形随机化瑞士军刀”实用函数集,最好是优化为在GPU着色器内使用。我更喜欢GLSL,但任何编程语言的代码都可以,我可以自己将其翻译成GLSL。
具体来说,我期望:
a) 伪随机函数- N维、均匀分布在[-1,1]或[0,1]上,从M维种子计算(理想情况下,种子可以是任何值,但我可以接受将种子限制在0..1以获得均匀的结果分布)。类似于:
float random  (T seed);
vec2  random2 (T seed);
vec3  random3 (T seed);
vec4  random4 (T seed);
// T being either float, vec2, vec3, vec4 - ideally.
b) 持续噪声,如Perlin Noise - 再次,是N维的,+-均匀分布,有一组受限制的值,并且看起来很好(一些配置外观的选项,如Perlin levels也可能会有用)。我期望的签名是:
float noise  (T coord, TT seed);
vec2  noise2 (T coord, TT seed);
// ...

我对随机数生成理论不是很了解,因此我更愿意选择一个现成的解决方案,但我也会欣赏像“这里有一个非常好的、高效的一维rand()函数,让我来告诉你如何在其基础上构建一个优秀的N维rand()函数......”这样的答案。


这里有一堆很棒的伪随机函数:https://www.shadertoy.com/view/4djSRW - sdfgeoff
一篇好的论文,包含了不同哈希函数的性能和质量测试结果,链接为http://www.jcgt.org/published/0009/03/02/。同时还有https://www.shadertoy.com/view/XlGcRh。 - LarsH
14个回答

322

对于非常简单的伪随机生成,我使用在某个地方从互联网上找到的这个一行代码:

float rand(vec2 co){
    return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
}

你还可以使用任何PRNG生成噪声纹理,然后按照正常方式上传它,并在着色器中采样值;如果需要,我稍后可以找到一些代码示例。

此外,请查看此文件,其中包含Stefan Gustavson编写的Perlin和Simplex噪声的GLSL实现。


32
vec2 co怎么使用?它是范围吗?种子? - Ross
15
注意使用此算法时低精度浮点片段着色器的问题(例如S3的ARM Mali):https://dev59.com/S2XWa4cB1Zd3GeqPO6Mg#15846469。https://github.com/ashima/webgl-noise项目似乎没有lowp问题。 - P.T.
5
这里描述的函数在这里有更详细的讨论:链接 - Loomchild
7
FYI:该函数的分布很糟糕。 - Tara
4
为什么在GLSL中使用co.xy而不是co?作为GLSL的新手,能否有人解释一下? - kelin
显示剩余8条评论

120

我突然想到你可以使用一个简单的整数哈希函数并将结果插入到浮点数的尾数中。据我所知,GLSL规范保证了32位无符号整数和IEEE二进制32位浮点表示,因此应该完全可移植。

我刚试过了一下,结果非常好:对于我尝试的每个输入,它看起来就像静态的,没有任何可见的模式。相比之下,流行的sin/fract代码片段在我的GPU上给出了相当明显的对角线。

其中一个缺点是它需要GLSL v3.30。虽然它似乎足够快,但我还没有经验性地量化其性能。AMD的着色器分析器声称vec2版本在HD5870上每个时钟周期为13.33像素。与sin/fract代码片段的16像素每个时钟周期相比,它肯定会慢一些。

这是我的实现。我留下了各种各样的想法来使您更容易从中推导出自己的函数。

/*
    static.frag
    by Spatial
    05 July 2013
*/

#version 330 core

uniform float time;
out vec4 fragment;



// A single iteration of Bob Jenkins' One-At-A-Time hashing algorithm.
uint hash( uint x ) {
    x += ( x << 10u );
    x ^= ( x >>  6u );
    x += ( x <<  3u );
    x ^= ( x >> 11u );
    x += ( x << 15u );
    return x;
}



// Compound versions of the hashing algorithm I whipped together.
uint hash( uvec2 v ) { return hash( v.x ^ hash(v.y)                         ); }
uint hash( uvec3 v ) { return hash( v.x ^ hash(v.y) ^ hash(v.z)             ); }
uint hash( uvec4 v ) { return hash( v.x ^ hash(v.y) ^ hash(v.z) ^ hash(v.w) ); }



// Construct a float with half-open range [0:1] using low 23 bits.
// All zeroes yields 0.0, all ones yields the next smallest representable value below 1.0.
float floatConstruct( uint m ) {
    const uint ieeeMantissa = 0x007FFFFFu; // binary32 mantissa bitmask
    const uint ieeeOne      = 0x3F800000u; // 1.0 in IEEE binary32

    m &= ieeeMantissa;                     // Keep only mantissa bits (fractional part)
    m |= ieeeOne;                          // Add fractional part to 1.0

    float  f = uintBitsToFloat( m );       // Range [1:2]
    return f - 1.0;                        // Range [0:1]
}



// Pseudo-random value in half-open range [0:1].
float random( float x ) { return floatConstruct(hash(floatBitsToUint(x))); }
float random( vec2  v ) { return floatConstruct(hash(floatBitsToUint(v))); }
float random( vec3  v ) { return floatConstruct(hash(floatBitsToUint(v))); }
float random( vec4  v ) { return floatConstruct(hash(floatBitsToUint(v))); }





void main()
{
    vec3  inputs = vec3( gl_FragCoord.xy, time ); // Spatial and temporal inputs
    float rand   = random( inputs );              // Random per-pixel value
    vec3  luma   = vec3( rand );                  // Expand to RGB

    fragment = vec4( luma, 1.0 );
}

截图如下:

random(vec3)在static.frag中的输出结果

我在图像编辑程序中检查了这张截图。有256种颜色,平均值为127,这意味着分布是均匀的,并涵盖了预期范围。



30
对于一个好的想法和实现我给予加一。但我会对这个说法提出质疑,因为有256种颜色且平均值为127,并不意味着分布一定是均匀的(在严格意义上)。它可能是均匀的,但我认为我们目前还不确定。例如,一个钟形曲线分布也可能具有相同的平均值和颜色数量,但并不是均匀的。 - LarsH
好吧,对于不需要均匀性的大多数应用程序来说,这已经足够了。 :-) - itmuckel
8
根据我对直方图的观察,它似乎非常均匀......我认为它足够好,适用于大多数需要均匀性的应用程序。 (唯一比其他值生成较少的值为0和255) - leviathanbadger
2
我该如何实现这个方法的 vec3 random(vec3 v) 重载? - Pedro Henrique
@PedroHenrique:vec3(random(v.x), random(v.y), random(v.z)) 怎么样? - Alexander Torstling
显示剩余2条评论

80
不是的,自2005年以来就不再使用了。只是人们坚持下载旧版本。您提供的链接使用的版本仅使用8位2D纹理。 由Ashima和我自己的Ian McEwan制作的新版本不使用纹理,但在具有大量纹理带宽的典型桌面平台上运行速度约为一半。在移动平台上,无纹理版本可能更快,因为纹理通常是一个重要的瓶颈。 我们积极维护的源代码库是:

https://github.com/ashima/webgl-noise

这里有一个包含无纹理和使用纹理版本的噪声集合(仅使用2D纹理):

http://www.itn.liu.se/~stegu/simplexnoise/GLSL-noise-vs-noise.zip

如果您有任何具体问题,请直接发送电子邮件给我(我的电子邮件地址可以在classicnoise*.glsl源代码中找到)。

4
是的,我所提到的实现,即@dep链接到的davidcornette.com上的代码确实使用了1D纹理:glBindTexture(GL_TEXTURE_1D, *texID);等。你所说的“你提供的链接”并不清楚,因为你引用了我的答案,但那个答案并没有链接到你的实现。我将更新我的答案以澄清我的指称,并反映你提供的新信息。将人们描述为“坚持”下载旧版本是一种扭曲的言论,这并不能展现出你的价值。 - LarsH
1
你可能想给David Cornette写信(他的联系信息在http://davidcornette.com/上)并要求他更改http://www.davidcornette.com/glsl/links.html上的链接,以链接到你的源代码库。我也会给他发电子邮件。 - LarsH
1
P.P.S. 你能澄清一下,哪个版本仅使用8位2D纹理吗?听起来这可能是某些平台的不错选择... - LarsH

45

黄金噪音

// Gold Noise ©2015 dcerisano@standard3d.com
// - based on the Golden Ratio
// - uniform normalized distribution
// - fastest static noise generator function (also runs at low precision)
// - use with indicated fractional seeding method. 

float PHI = 1.61803398874989484820459;  // Φ = Golden Ratio   

float gold_noise(in vec2 xy, in float seed){
       return fract(tan(distance(xy*PHI, xy)*seed)*xy.x);
}

立即在浏览器中查看金噪声!

enter image description here

这个函数相对于@appas在2017年9月9日回答中的函数改善了随机分布:

enter image description here

@appas的函数也是不完整的,因为没有提供种子(uv不是种子 - 对于每一帧都一样),并且不能在低精度芯片组上运行。黄金噪声默认以低精度运行(速度更快)。


1
谢谢您发布这篇文章。您是否考虑发布可运行的版本,例如在shadertoy.com上,以便人们可以在浏览器中尝试它? - LarsH
1
这是Gold Noise在野外的一个示例,一个抖动应用程序:https://www.shadertoy.com/view/XdGczW - Dominic Cerisano
16
我不认为这与其他噪声函数有任何区别。你有证明它具有特殊属性吗?仅仅使用一堆无理数并不能使其变得特殊。 - M.kazem Akhgary
8
@Dominic: “It has superior distribution to similar functions”: 这需要证明。 tan() 函数非常不稳定。当输入接近 pi/2 或接近零点处时,都很可能因为所有 fract(non-linear*big) 被基于较少的有效位数而在不同的硬件上产生不同的结果。小或大的输入值也会对其产生影响。此外,位动态可能因位置而异。 - Fabrice NEYRET
4
现在GLSL有整数,所以没有任何理由不使用“严肃的”基于int的哈希生成器,当需要质量分布(和动态)时,性能相似,除了非常低端的设备。 - Fabrice NEYRET
显示剩余8条评论

13

这里还有一个很好的实现(点击此处),由McEwan和@StefanGustavson描述,看起来像Perlin噪点,但“不需要任何设置,即不需要纹理或统一数组。只需将它添加到您的着色器源代码中,并在您想要的任何地方调用它”。

这非常方便,特别是考虑到Gustavson早期的实现,@dep提供的链接使用了一个1D纹理,在WebGL的着色器语言GLSL ES 中不支持


1
这是对OP的b)噪音类型请求的最佳答案!这是一个直接链接:https://github.com/ashima/webgl-noise。其中有2d、3d和4d版本,使用GLSL 120代码已经准备好了。 - user362515

13
在2010年首次发布这个问题之后,关于良好的随机函数和对其的硬件支持发生了很多变化。
从今天的角度来看,从这个算法中产生的随机数的均匀性非常差。而且,均匀性会受到输入值的大小的影响,当从中进行采样时,可见的伪影/图案将变得明显,例如在光线/路径追踪应用中。
这个任务有许多不同的功能(大多数是整数哈希函数),针对不同的输入和输出维度设计,其中大部分在2020年JCGT论文《GPU渲染的哈希函数》中进行评估。根据您的需求,您可以从该论文中提出的函数列表中选择一个函数,然后从附带的Shadertoy中获取。这篇论文中没有涵盖的一个函数,但在任何输入幅度值上都没有明显模式的函数,也是我想要强调的一个函数。
其他类别的算法使用低差异序列来生成伪随机数,例如使用Owen-Nayar混淆的Sobol序列。Eric Heitz在这个领域做了一些令人惊叹的研究,还有他的《一个在屏幕空间中将蒙特卡洛误差分布为蓝噪声的低差异采样器》论文。 另一个例子是(迄今为止最新的)JCGT论文《基于哈希的Owen混淆的实用性应用》,该论文将Owen混淆应用于不同的哈希函数(即Laine-Karras)。
另一种可能性是所谓的“同余生成器”,包括“线性同余生成器”(LCG)和“置换同余生成器”(PCG)。它们与基于哈希的算法不同,LCG和PCG具有一个“状态”和一个相关的函数,通过改变该状态来生成伪随机数。因此,它们不依赖于任何“输入”来计算哈希,而是仅仅通过移位/置换/改变状态来生成伪随机数,这个状态会在函数的多次调用中保持不变。 PCG的一个应用可以在Nvidia的“Mini Path-Tracer Tutorial”中找到。
还有其他类别的算法可以产生具有理想频谱的噪声模式,比如蓝噪声,这对眼睛来说特别“舒适”。
(我意识到一个好的StackOverflow回答应该提供算法的源代码而不是链接,因为链接可能会失效,但是现在有太多不同的算法了,我打算将这个回答作为当今已知好的算法的摘要。)

9

请使用这个:

highp float rand(vec2 co)
{
    highp float a = 12.9898;
    highp float b = 78.233;
    highp float c = 43758.5453;
    highp float dt= dot(co.xy ,vec2(a,b));
    highp float sn= mod(dt,3.14);
    return fract(sin(sn) * c);
}

不要使用这个:

float rand(vec2 co){
    return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}

您可以在对OpenGL ES 2.0的canonical one-liner GLSL rand()函数进行改进中找到解释。


我大致浏览了一下这篇文章,但仍然不确定,在 mod 中的 3.14 是否是 pi 的近似值? - Kaan E.
我不相信这个。我会重新表述为“在一个假设的糟糕系统上,它不能保证浮点运算的精度,并且足够愚蠢以至于没有包装sin参数(这真的存在吗?),它可以改进为对正弦内容进行模操作。”此外,当使用巨大的pi近似值时,我有点担心偏差,但较大的c值可能会挽救局面。 - Fabrice NEYRET
@FabriceNEYRET 在我的两台笔记本电脑上(一台配备英特尔UHD Graphics 620,另一台报告了Nvidia Quadro T1000),Muddy Cavern在32秒后开始在墙壁上显示网格图案。使用mod(n, 6.2831853),就没有这样的问题。这表明mod确实会产生影响,至少在某些GPU上是如此。文章的作者承认同时使用modhighp可能有些过度。我想需要进行一些研究才能弄清楚这个问题的根源。 - Ruud Helderman
1
@Ruud Helderman:没错,与此同时,在Shadertoy上有些人已经证实了这一点,特别是对于低端GPU。对于Nvidia来说,这更令人惊讶(而且他们通常甚至实现了超出GLSL规范的数学和IEEE)。哦天啊... - Fabrice NEYRET
@FabriceNEYRET 不要惊慌,这可能仍然是一个奇怪的巧合;例如(1)随着种子增长,精度降低,(2)种子跳跃恰好近似于2π的倍数,(3)我引入的2π模数略有不准确,破坏了规律性,给人以虚假的改进印象。 - Ruud Helderman
我已经尝试了这里发布的大多数示例,其中使用基于Perlin的方法时出现了奇怪的线条扭曲。我尝试了你发布的这个示例,它产生了平滑的噪声 :) ...有时候只需尝试一下就好了lol。 - Richard

7

哈希: 现在,WebGL2.0支持整数在(w)GLSL中使用。 -> 为了获得高质量的可移植哈希(与丑陋的浮点哈希成本相似),我们现在可以使用“严肃”的哈希技术。 IQ在https://www.shadertoy.com/view/XlXcW4(以及其他地方)实现了一些技术。

例如:

  const uint k = 1103515245U;  // GLIB C
//const uint k = 134775813U;   // Delphi and Turbo Pascal
//const uint k = 20170906U;    // Today's date (use three days ago's dateif you want a prime)
//const uint k = 1664525U;     // Numerical Recipes

vec3 hash( uvec3 x )
{
    x = ((x>>8U)^x.yzx)*k;
    x = ((x>>8U)^x.yzx)*k;
    x = ((x>>8U)^x.yzx)*k;

    return vec3(x)*(1.0/float(0xffffffffU));
}

2
我将Ken Perlin的Java实现之一翻译成了GLSL,并在ShaderToy上用于几个项目中。下面是我所做的GLSL解释:
int b(int N, int B) { return N>>B & 1; }
int T[] = int[](0x15,0x38,0x32,0x2c,0x0d,0x13,0x07,0x2a);
int A[] = int[](0,0,0);

int b(int i, int j, int k, int B) { return T[b(i,B)<<2 | b(j,B)<<1 | b(k,B)]; }

int shuffle(int i, int j, int k) {
    return b(i,j,k,0) + b(j,k,i,1) + b(k,i,j,2) + b(i,j,k,3) +
        b(j,k,i,4) + b(k,i,j,5) + b(i,j,k,6) + b(j,k,i,7) ;
}

float K(int a, vec3 uvw, vec3 ijk)
{
    float s = float(A[0]+A[1]+A[2])/6.0;
    float x = uvw.x - float(A[0]) + s,
        y = uvw.y - float(A[1]) + s,
        z = uvw.z - float(A[2]) + s,
        t = 0.6 - x * x - y * y - z * z;
    int h = shuffle(int(ijk.x) + A[0], int(ijk.y) + A[1], int(ijk.z) + A[2]);
    A[a]++;
    if (t < 0.0)
        return 0.0;
    int b5 = h>>5 & 1, b4 = h>>4 & 1, b3 = h>>3 & 1, b2= h>>2 & 1, b = h & 3;
    float p = b==1?x:b==2?y:z, q = b==1?y:b==2?z:x, r = b==1?z:b==2?x:y;
    p = (b5==b3 ? -p : p); q = (b5==b4 ? -q : q); r = (b5!=(b4^b3) ? -r : r);
    t *= t;
    return 8.0 * t * t * (p + (b==0 ? q+r : b2==0 ? q : r));
}

float noise(float x, float y, float z)
{
    float s = (x + y + z) / 3.0;  
    vec3 ijk = vec3(int(floor(x+s)), int(floor(y+s)), int(floor(z+s)));
    s = float(ijk.x + ijk.y + ijk.z) / 6.0;
    vec3 uvw = vec3(x - float(ijk.x) + s, y - float(ijk.y) + s, z - float(ijk.z) + s);
    A[0] = A[1] = A[2] = 0;
    int hi = uvw.x >= uvw.z ? uvw.x >= uvw.y ? 0 : 1 : uvw.y >= uvw.z ? 1 : 2;
    int lo = uvw.x <  uvw.z ? uvw.x <  uvw.y ? 0 : 1 : uvw.y <  uvw.z ? 1 : 2;
    return K(hi, uvw, ijk) + K(3 - hi - lo, uvw, ijk) + K(lo, uvw, ijk) + K(0, uvw, ijk);
}

我从Ken Perlin的Noise Hardware第2章的附录B中翻译了这段文字,来源如下:

https://www.csee.umbc.edu/~olano/s2002c36/ch02.pdf

这是我在Shader Toy上创建的一个公共着色器,使用了发布的噪声函数:

https://www.shadertoy.com/view/3slXzM

在我的研究中,我发现了一些关于噪音的好资源,包括:

https://thebookofshaders.com/11/

https://mzucker.github.io/html/perlin-noise-math-faq.html

https://rmarcus.info/blog/2018/03/04/perlin-noise.html

http://flafla2.github.io/2014/08/09/perlinnoise.html

https://mrl.nyu.edu/~perlin/noise/

https://rmarcus.info/blog/assets/perlin/perlin_paper.pdf

https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch05.html

我强烈推荐《着色器之书》,因为它不仅提供了噪声的出色交互式解释,还包括其他着色器概念。
编辑:
可能可以通过使用GLSL中可用的一些硬件加速函数来优化翻译代码。如果我最终这样做了,我将更新此帖子。

另外,我相信Perlin / Simplex噪声仍然是伪随机的。据我回忆,有趣的事情在于,您可以在不同级别上分层和“缩放”噪声,使其看起来非常无缝。不要引用我说的话,但是这是值得思考的事情。 - Andrew Meservy
@Zibri 很抱歉,我对纯C或.sh命令不是非常熟悉。但看起来这个函数只是一个伪随机数生成器,而不是噪声函数。此外,请记住,glsl像素着色器直接在GPU上运行。您将无法访问任何可能在C中可用的额外库或CPU功能。 - Andrew Meservy
《着色器之书》对Simplex噪声的解释非常好,因为它通过扭曲网格和每个点的计算量减少了Perlin噪声的不必要计算,从而使其成为更高效的版本。绝对值得一读。 - Andrew Meservy
还请参阅分形布朗运动和沃罗诺伊斯章节。 - Andrew Meservy
Andrew Meservy:不需要任何库...我的噪声函数非常简单:2个64位整数是状态x(n)和x(n-1)。简单而快速的公式是x(n+1) = ROTR(x(n)+x(n-1),8)。如果你克隆我的git并运行它,你会看到它在运作。 - Zibri

2
一维Perlin的直线锯齿版本,本质上是随机的LFO锯齿形信号。
half  rn(float xx){         
    half x0=floor(xx);
    half x1=x0+1;
    half v0 = frac(sin (x0*.014686)*31718.927+x0);
    half v1 = frac(sin (x1*.014686)*31718.927+x1);          

    return (v0*(1-frac(xx))+v1*(frac(xx)))*2-1*sin(xx);
}

我还在Shadertoy网站的Inigo Quilez Perlin教程中发现了1-2-3-4d perlin噪声,以及voronoi等内容,他提供了完整快速的实现和代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接