这个GLSL rand()单行代码的起源是什么?

119

我在网络上看到一种用于着色器的伪随机数生成器,它被称为这里和那里

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

这个函数被称为"规范化函数"或者是"我在网上找到的一个一行代码"。

这个函数的起源是什么?常数的值是否像它们看起来那样随意选定,还是有某种艺术性的选择方法?有没有讨论这个函数的优点?

编辑:我找到的最早提到这个函数的参考资料是2008年2月的这个归档页面,原始页面已经从互联网上消失了。但是那里也没有更多的讨论。


这是一个噪声函数,用于创建程序生成的地形。类似于 https://en.wikipedia.org/wiki/Perlin_noise 这样的东西。 - Shai UI
1
好的,上面的函数并不类似于Perlin噪声。此外,Perlin噪声是基于随机数生成器的,因为整数位置处的梯度必须是随机生成的。 - Gab
5个回答

49
非常有趣的问题!
我在打字回答时尝试弄清楚这个问题 :) 首先,一个简单的方法来测试它:http://www.wolframalpha.com/input/?i=plot%28+mod%28+sin%28x*12.9898+%2B+y*78.233%29+*+43758.5453%2C1%29x%3D0..2%2C+y%3D0..2%29 然后让我们考虑我们要做什么:对于两个输入坐标x,y,我们返回一个“随机数”。现在这不是一个随机数。每次我们输入相同的x,y时它都是一样的。这是一个哈希函数!
函数要做的第一件事是从二维到一维。这本身并不有趣,但是数字被选择为通常不会重复。此外,我们在那里进行了浮点加法。可能会从y或x中得到更多位,但是数字可能刚好被选择为进行混合。
然后我们采样黑盒sin()函数。这将大大取决于实现!
最后,它通过乘法和取分数放大了sin()实现中的误差。
我认为在一般情况下,这不是一个好的哈希函数。 sin()是黑盒,在GPU上进行数值计算。通过使用几乎任何哈希函数并转换它,应该能够构造出一个更好的哈希函数。难点在于将CPU哈希中使用的典型整数操作转换为float(半数或32位)或定点操作,但应该是可能的。
再次强调,作为哈希函数的真正问题在于sin()是一个黑盒。

1
这并没有回答关于起源的问题,但我认为这个问题实际上是无法回答的。我会接受这个答案,因为它有说明性的图表。 - Grumdrig
你所说的黑盒子是指我们不知道它是否产生一致的结果吗?比如不同的GPU或者驱动程序可能会改变结果? - toraman
1
@toraman 一个'黑箱'是其内部操作无法被破解的进程。虽然方程中的每个其他操作都可以独立计算,但是找到正弦值取决于GPU的实现,因此结果是不可预测的(但对于任何一个实现而言将是一致的)。 - Barney

29
起源可能是一篇论文:"On generating random numbers, with help of y= [(a+x)sin(bx)] mod 1", W.J.J. Rey, 22nd European Meeting of Statisticians and the 7th Vilnius Conference on Probability Theory and Mathematical Statistics, August 1998
编辑:由于找不到这篇论文的副本,而且“TestU01”参考资料可能不太清楚,以下是在伪C中以TestU01描述的方案:
#define A1 ???
#define A2 ???
#define B1 pi*(sqrt(5.0)-1)/2
#define B2 ???

uint32_t n;   // position in the stream

double next() {
  double t = fract(A1     * sin(B1*n));
  double u = fract((A2+t) * sin(B2*t));
  n++;
  return u;
} 

唯一建议的常量值是B1。

注意,这是针对流的。将其转换为1D哈希表后,“n”变为整数网格。因此,我猜测有人看到了这一点,并将“t”转换为一个简单的函数f(x,y)。使用上述原始常量将产生:

float hash(vec2 co){
  float t = 12.9898*co.x + 78.233*co.y; 
  return fract((A2+t) * sin(t));  // any B2 is folded into 't' computation
}

4
非常有趣!我找到了一篇引用它的论文以及在Google Books上找到了该期刊本身,但似乎演讲或论文本身并未包含在该期刊中。链接如下:引用论文期刊链接 - Grumdrig
1
жӯӨеӨ–пјҢд»Һж ҮйўҳдёҠзңӢпјҢжҲ‘жӯЈеңЁиҜўй—®зҡ„еҮҪж•°еә”иҜҘиҝ”еӣһfract(sin(dot(co.xy,vec2(12.9898,78.233)))*(co.xy+vec2(43758.5453,SOMENUMBER))д»Ҙз¬ҰеҗҲи®әж–ҮдёӯжүҖи®Ёи®әзҡ„еҮҪж•°гҖӮ - Grumdrig
还有一件事,如果这确实是函数使用的起源,那么反复使用的魔数(选择ab)的起源问题仍然存在,但可能已经在您引用的论文中使用了。 - Grumdrig
我也找不到那篇论文了。(编辑:与上面链接的同一篇论文) - MB Reynolds
更新答案并提供更多信息。 - MB Reynolds

9
常数值是任意的,特别是它们非常大,离质数还有几个小数点。
一个高振幅正弦波乘以4000的模数是一个周期函数。就像一扇窗帘或者一块波纹金属,因为乘以了4000而变得非常小,并通过点积在一个角度上旋转。
由于这个函数是二维的,点积的作用是将周期函数相对于X和Y轴倾斜旋转。大约是13/79的比例。这是低效的,实际上你可以通过计算(13x + 79y)的正弦波来实现同样的效果,我认为这需要更少的数学计算。
如果你找到了X和Y中的函数周期,你可以对其进行采样,使其看起来像一个简单的正弦波。
这是一个放大后的图片graph 我不知道它的起源,但它与许多其他图案相似,如果你在规则的间隔中在图形中使用它,它会产生莫尔纹理,并且你可以看到它最终会再次循环。

但在GPU上,X和Y的范围从0到1,如果你改变你的图形,看起来更加随机。我知道这听起来像一个陈述,但实际上这是一个问题,因为我的数学教育在18岁结束了。 - Strings
我知道,我只是放大了一下,让你看到随机函数的形式,除了峰和谷很快地变化,除非你缩小到很小才能看到变化... 你可以想象,在峰和谷上取点将会给出从0到1高度的相当随机的数字,对于1到1 x和y值。 - bandybabboon
哦,我明白了,这对于任何使用正弦函数作为核心的随机数生成器似乎非常合理。 - Strings
2
这实际上是一个线性的之字形,sin函数应该添加一点变化,就像有人在你面前快速地将一副牌从1到10不停地抽来抽去,你应该尝试从牌中挑选出数字的模式,它们会是随机的数字,因为它抽得非常快,只有通过选择与牌旋转速度完全同步的牌才能得到模式。 - bandybabboon
只是提醒一下,使用(13x + 79y)并不会更快,因为dot(XY, AB)将会完全按照您所描述的方式执行,因为它是点积,即x,y dot 13, 79 = (13x + 79y) - Krupip

3
我不认为这是真正的起源,但OP的代码作为代码示例在Patricio Gonzalez Vivo和Jen Lowe的《着色器之书》(https://thebookofshaders.com/10/)中呈现。在他们的代码中,作者Patricio Gonzales Vivo被引用,即“// Author @patriciogv - 2015”。 由于OP的研究甚至可以追溯到更早的时间(到'08年),因此该来源可能至少解释了其流行程度,作者可能能够阐明他的来源。

2
也许这是一些非经常性的混沌映射,这可以解释很多事情,但也可能只是对大数进行的任意操作。
编辑:基本上,函数fract(sin(x) * 43758.5453)是一个简单的哈希函数,sin(x)提供了从-1到1的平滑正弦插值,因此sin(x) * 43758.5453将插值从-43758.5453到43758.5453。这是一个相当巨大的范围,因此即使在x上的小步骤也会提供大步骤的结果,并且在小数部分中变化非常大。 "fract"是为了获得-0.99 ...到0.999 ...的值。 现在,当我们有了像哈希函数这样的东西,我们应该创建用于从向量生成哈希的函数。最简单的方法是将“哈希”分别用于输入向量的x和y分量。但是,然后我们将获得一些对称值。因此,我们应该从向量中获取一些值,方法是找到一些随机向量并查找与该向量的“点”积,其中我们看到:fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453); 此外,根据选择的向量,它的长度应足够长,以便在计算“点”积后,sin函数经历几个周期。

但是4e5也应该可以工作,我不明白为什么有一个神奇的数字43758.5453。(另外,我会通过一些小数偏移x来避免rand(0)= 0。 - Fabrice NEYRET
1
我认为使用4e5,你将不会获得太多小数位的变化,它总是会给你相同的值。因此,必须满足两个条件,即足够大并且有足够好的小数部分变化。 - Roman
你的意思是什么,“总是会给你相同的值”?如果你的意思是它总是会取相同的数字,首先,它们仍然是混乱的;其次,浮点数存储为m2^p,而不是10^p,所以4e5仍然会打乱位。 - Fabrice NEYRET
我以为你是在写一个数的指数表示,4 * 10^5,所以 sin(x)*4e5 会给你一个不那么混乱的数字。我同意正弦波的分数位也会给你带来好的混沌效果。 - Roman
但是,这取决于x的范围,我的意思是如果函数应该对小值(-0.001,0.001)和大值(-1,1)具有鲁棒性。您可以尝试使用fract(sin(x / 1000.0)* 43758.5453);和fract(sin(x / 1000.0)* 4e5)之间的差异,其中x在范围[-1.,1.]内。在第二种变体中,图像将更加单调(至少我在着色器中看到了差异)。但是,总的来说,我同意您仍然可以使用4e5并获得足够好的结果。 - Roman

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