噪声贴图

噪声贴图现在已经有许多种类,比如值噪声(value noise)、柏林噪声(perlin noise)、单形噪声(simplex noise)等,这些噪声在随机的基础上,明显遵守了一定的规则,展现出了某种特点。

image

Cpu这边,许多库都可以提供产生随机数的函数,其中有些是确定性随机,即每次输入的相同的值,输出的随机数都会是一样的;有些是非确定性随机,即每次输入相同的值,输出的随机数几乎一定是不同的。

一般来讲,生成噪声倾向于使用确定性随机,这样一来,每一次生成的结果就会是唯一且确定的,更加利于控制随机的效果。

GPU这边,可以通过哈希函数来获取随机。

这是一个经典的随机函数,输入二维变量,通过点乘、三角函数和取小数

1
2
3
4
5
float rand(vec2 st) {
    return fract(sin(dot(st.xy,
        vec2(12.9898,78.233)))*
        43758.5453123);
}

但是在稍大一些的范围时,该函数会给出重复的结果,这个ShaderToy列举了一些更好的hash函数

https://www.shadertoy.com/view/4djSRW

白噪声(White Noise)

如果把随机值直接作为结果返回颜色,就可以获得白噪声

1
2
3
4
5
half4 frag(v2f i) : SV_Target{
    float2 uv = i.uv;
    float random = rand(uv);
    return half4(random, random, random, 1);
}

writeNoise

添加一项缩放系数,对噪声进行初步控制

1
2
3
4
5
6
7
int _Scale

half4 frag(v2f i) : SV_Target{
    half2 uv = floor(i.uv * _Scale) / _Scale;
    float random = rand(uv);
    return half4(random, random, random, 1);
}

当_Scale为4时,结果如下:

texture

上面的代码是把uv[0,1]^2^转换为[0,4]^2^,然后再取整数部分,使得[n,n + 1)的值都为n

如此一来,相当于把整个uv分为4*4=16个区域,每一个区域中的uv值都是相同的,根据uv生成的噪声自然也是相同的。

值噪声(Value Noise)

上面使用了Scale的白噪声,每个区域内的值都是该区域左下那一点的值,区域内的值是相同的。

为了让让每个区域内的值不再相同,有些变化,值噪声在上面的基础上,通过插值,获得了有过渡的结果。

具体做法是根据一个uv的整数,获取其所在区域内的四个顶点的值,根据其小数部分,用那四个值进行双线性插值。

image

1
2
3
4
5
6
7
8
9
10
float valueNoise(float2 uv){
    float2 i = floor(uv);
    float2 f = frac(uv);
    float a = rand(i);
    float b = rand(i + float2(1, 0));
    float c = rand(i + float2(0, 1));
    float d = rand(i + float2(1, 1));
    float2 u = f
    return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y);
}

valueNoise

使用线性插值的结果,过于锐利,不够平滑,改为使用smoothstep平滑插值

1
float2 u = smoothstep(float2(0,0),float2(1,1),f);

valueNoise

smoothstep实际上是一个三次多项式函数,在许多生成噪声的代码里使用的是下面的式子,和smoothstep是等价的。

1
float2 u = f*f* (3.0 - 2.0 * f);

还有一些其他的多项式插值函数,后面在表。

梯度噪声

值噪声的结果不免有些过于方正,不能很好的模拟自然界的随机产物,为了消除这些“方正”的效果1985年Ken Perlin开发了一种噪声算法——梯度噪声。

Perlin Noise

Perlin 噪声是梯度噪声的一种类型,因为它是Ken Perlin提出的第一种梯度噪声,所以被以Perlin的名字命名。

不同于值噪声直接使用一个区域的四角的随机值进行插值,Perlin噪声会为一个区域的四角分别生成一个随机方向(2D的就是float2),然后计算输入点分别到四角的方向,然后一一对应计算他们的点积,用这计算得来的4个点积进行插值。

image

下面的代码不是正宗的Perlin噪声算法,在获取随机方向的部分使用了简化的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//为简化获得柏林梯度向量的随机函数
float2 randForPerlin(float2 x){
    float2 k = float2(0.3183099, 0.3678794);
    x = x * k + k.yx;
    return -1.0 + 2.0 * frac(16.0 * k * frac(x.x * x.y * (x.x + x.y)));
}

//柏林噪声 ——梯度向量简化版
float perlinNoise(float2 uv){
    float2 i = floor(uv);
    float2 f = frac(uv);
    float value0 = dot(randForPerlin(i + float2(0, 0)), f - float2(0, 0));
    float value1 = dot(randForPerlin(i + float2(1, 0)), f - float2(1, 0));
    float value2 = dot(randForPerlin(i + float2(0, 1)), f - float2(0, 1));
    float value3 = dot(randForPerlin(i + float2(1, 1)), f - float2(1, 1));

    float2 u = f * f *(3.0 - 2.0 * f);
    return lerp(lerp(value0, value1, u.x), lerp(value2, value3,u.x), u.y);
}

perlinNoise

Simplex Noise

Ken Perlin在2001年提出的噪声算法,比起之前的噪声,它不再使用四个角的值进行插值,而是改为从三个点,划分区域时,也不再划分为方格,而是划分为三角形。

这么做的好处是顶点少了后,需要的计算就少了非常多,这在2D上只是少了一个点,在3D上就少了4个!在更高维会少得更多。(确切说是由2^n^ 减少到了n^2^)

自然而然我们希望用等边三角形铺满纹理,因为生成得噪声应该是没有方向性的,这就需要正三角形。那如何获得等边三角形的区域?

方法是先将四角方格分为两个等腰直角三角形,然后再把三角形拉成等边三角形

image

通过比较x和y得大小,可以判断这个点是在上三角形还是在下三角形中。

由等腰三角形拉成等边三角形,我们先用一种容易理解的方法,我们让B点不动,把连带C点的整个空间往下拉,把C点拉到C^’^

image

用B(0,1)=>B’(0,1)和C(1,1)=>C’($\sqrt 3 /2,-1/2$)建立矩阵方程,求得转换矩阵为

sqrt(3)/2 0

-1/2 1

方格=》三角形

\[x'=\frac {\sqrt 3} 2x \space y'= -1/2x+y\]

需要注意,纹理上的uv值是(x’,y’),即平铺的三角形的平面中位置,我们需要逆向计算出该点原来的uv值,好把uv转换在[0,1]之间

总之就是输入的是(x’,y’),需要求的是(x,y),下面是代码:

1
2
3
4
5
6
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;
}

我们换种更合适的方式转换空间,让整个空间沿对角线方向挤压,每个点都在保持在对角线上移动,边长不再是1了:

imageimage

image

代码如下:

1
2
3
4
5
6
#define F 0.366025404
vec2 skew (vec2 st) {
    vec2 r = vec2(0.0);
    r=st+(st.x +st.y)*F;
    return r;
}

各顶点对输入点的权重

image

r^2^ 一般规定为0.5,因为单形的三角形的一个顶点到对面边的距离是$\frac {\sqrt 2} 2$,平方就是$\frac 1 2$

完整的噪声代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//单型噪声
#define F 0.366025404
#define G 0.211324865

float simplexNoise(float2 p){
    //计算当前p在方格中的uv
    float2 skewPos = p + (p.x + p.y) * F;
    float2 skewi = floor(skewPos);
    float2 skewf = frac(skewPos);
    //判断是上面的还是下面的三角形
    float2 p1 = (skewf.x < skewf.y) ? float2(0.0, 1.0) : float2(1.0, 0.0);
    //计算输入点到单形(三角形)三点的距离
    float2 d1 = p - (skewi - (skewi.x + skewi.y) * G);
    float2 d2 = d1 - p1 + G;
    float2 d3 = d1 - 1 + 2 * G;
    //计算权重
    float3 h = max(0.5 - float3(dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0);
    //计算各点的权重值
    float3 n = h * h * h * h * float3(dot(d1, rand2L(skewi)), dot(d2, rand2L(skewi + p1)), dot(d3, rand2L(skewi + 1)));
    //将各点的值加起来,并且至于转化为[-1,1]
    return dot(float3(70, 70, 70), n);
}

simplexNoise

Cellular Noise

WorleyNoise

网格噪声是另一种不用于梯度噪声的噪声算法,现在基本用Worley Noise的名称——作者Steven Worley的名字。

基本思想是随机选择一些位置,称为特征点;

然后对于任意一个输入点,根据距离函数,取到这些特征点的距离中最近的那个距离作为输出。

实现上的优化,先把空间划分成网格,在每个网格中随机选一点作为特征点,之后计算最短距离时,只需要计算所在的网格和周围8个网格的特征点(优化方法使用4个网格就足够了)的距离就好了,不必遍历全部特征值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
float worleyNoise(float2 uv){
    float2 i = floor(uv);
    float2 f = frac(uv);

    float m_dist = 1;
    //四个特征点
    float2 originPoint = float2(0.5, 0.5) - rand2(i) * 0.5 * _Jitter;
    float2 upPoint = float2(0.5, 1.5) - rand2(i + float2(0, 1)) * 0.5 * _Jitter;
    float2 rightPoint = float2(1.5, 0.5) - rand2(i + float2(1, 0)) * 0.5 * _Jitter;
    float2 urPoint = float2(1.5, 1.5) - rand2(i + float2(1, 1)) * 0.5 * _Jitter;

    float4 dis = float4(distance(f, originPoint), distance(f, upPoint), distance(f, rightPoint), distance(f, urPoint));
    m_dist = min(min(dis.x, dis.y), min(dis.z, dis.w));
    return m_dist;
}

worleyNoise

在计算得到最近的特征点距离时,同时保存下最近特征点的坐标,通过把坐标转化为一个随机值,输出这个随机值而不是输出距离值

worleyNoise_SingleColor

分型噪声(fbm)

分型布朗运动,通过将不同频率和振幅的噪声函数进行组合,最常用的方式每一级频率×2,振幅÷2,最后相加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Properties
const int octaves = 1;
float lacunarity = 2.0;
float gain = 0.5;
//
// Initial values
float amplitude = 0.5;
float frequency = 1.;
//
// Loop of octaves
for (int i = 0; i < octaves; i++) {
	y += amplitude * noise(frequency*x);
	frequency *= lacunarity;
	amplitude *= gain;
}

该处理可以对任何种的噪声使用,下面分别是柏林分型噪声和worley分型噪声

perlin_fbmworley_fbm

湍流(turbulence)

用于fbm的技术,对于类似值域在[-1,1]的噪声,不使用线性变换到[0,1],而是使用绝对值,从而获得非常尖锐的噪声值为0的范围,用来刻画尖锐的山谷

1
2
3
4
5
for (int i = 0; i < OCTAVES; i++) {
    value += amplitude * abs(snoise(st));
    st *= 2.;
    amplitude *= .5;
}

下面是柏林分型湍流

perlinNoise_Turbulence

山脊(ridge)

在湍流的基础上,把最低值翻上来,再做一些处理,作为山脊

1
2
3
n = abs(n);     // create creases
n = offset - n; // invert so creases are at top
n = n * n;      // sharpen creases

同样是perlin噪声的演变:

perlinNoise_RidgeperlinNoise_Ridge2

还有一些其他的处理方法,比如把每一次迭代的分量乘在一起,而不是叠加:
perlinNoise_RidgeMulti

域翘曲(Domain Warping)

https://iquilezles.org/articles/warp/

通过多次嵌套fbm函数,来扭曲每一次输入的值,生成的噪声

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
float fbm(float2 uv)
{
    float Co = 1.0f;
    float result = 0.0f;
    for (int i = 0; i < FBMNum; i++)
    {
        result += perlinNoise(uv * Co) / Co;
        Co *= 2;
    }
    return result;
}

float pattern(float2 p)
{
    float2 q = float2(fbm(p + float2(0.0, 0.0)),
        fbm(p + float2(5.2, 1.3)));
    float2 r = float2(fbm(p + 4.0 * q + float2(1.7, 9.2)),
        fbm(p + 4.0 * q + float2(8.3, 2.8)));

    return fbm(p + 4.0 * r);
}

两次嵌套和三次嵌套的perlin fbm噪声的结果:
perlinNoise_DomainWarpingperlinNoise_DomainWarping1

平铺无缝

当在一个面上平铺纹理时,需要多个贴图上下左右首尾相接,但是我们之前生成的噪声纹理并不能无缝的连接在一起:
image

要让这些纹理能够自然的无缝平铺,需要一些额外的处理。

周期化输入

一个简单的方法是,我们通过uv来获得随机噪声,那么只需要让输入的uv按照像素的大小体现周期性就好了。

以perlin噪声为例,在生成随机梯度向量这一步,把输入的四角的坐标,除余操作获得以分割数为周期的梯度向量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
float2 randSeamless(float2 x)
{
    x %= SplitNum;
    float2 k = float2(0.3183099, 0.3678794);
    x = x * k + k.yx;
    return -1.0 + 2.0 * frac(16.0 * k * frac(x.x * x.y * (x.x + x.y)));
}

float perlinNoiseSeamless(float2 uv) {
    float2 i = floor(uv);
    float2 f = frac(uv);
    float value0 = dot(randSeamless(i + float2(0, 0)), f - float2(0, 0));
    float value1 = dot(randSeamless(i + float2(1, 0)), f - float2(1, 0));
    float value2 = dot(randSeamless(i + float2(0, 1)), f - float2(0, 1));
    float value3 = dot(randSeamless(i + float2(1, 1)), f - float2(1, 1));

    float2 u = f * f * (3.0 - 2.0 * f);
    return lerp(lerp(value0, value1, u.x), lerp(value2, value3, u.x), u.y);
}

imageimage

使用这个方法的缺点是,缩放、或者说是分格数量(代码中的SplitNum),必须是整数,不然不能做到无缝。

高维采样

另一种方法是,通过更高维度的噪声贴图采样获取四周无缝噪声。

例如在2d纹理上,可以采样一个环,从而获得一个左右循环的的一维噪声;在3d纹理上,可以采样一个圆筒,获得一个左右无缝的2d纹理。

想要获取2d的完全无缝的纹理,则需要在4d空间的纹理中进行采样,3d的则需要在6d纹理中采样,即n维噪声需要在2n维噪声中采样。

还是以2dperlin为例,首先需要实现4d perlin噪声。

4d perlin噪声在一个超立方体中有16个顶点,即需要生成16个梯度向量、分别和距离向量点乘,然后插值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 4 out, 4 in...
float4 hash44(float4 p4)
{
    p4 = frac(p4  * float4(0.1031, 0.1030, 0.0973, 0.1099));
    p4 += dot(p4, p4.wzxy+33.33);
    return frac((p4.xxyz+p4.yzzw)*p4.zywx);
}

float perlinNoise4D(float4 uv)
{
    float4 i = floor(uv);
    float4 f = frac(uv);
    float4 u = f * f * (3.0 - 2.0 * f);

    //点000->0001 0000
    float value0000 = dot(hash44(i + float4(0, 0, 0, 0)), f - float4(0, 0, 0, 0));
    float value0001 = dot(hash44(i + float4(0, 0, 0, 1)), f - float4(0, 0, 0, 1));
    float v000 = lerp(value0000, value0001, u.w);
    //点100
    float value1000 = dot(hash44(i + float4(1, 0, 0, 0)), f - float4(1, 0, 0, 0));
    float value1001 = dot(hash44(i + float4(1, 0, 0, 1)), f - float4(1, 0, 0, 1));
    float v100 = lerp(value1000, value1001, u.w);
    //010
    float value0100 = dot(hash44(i + float4(0, 1, 0, 0)), f - float4(0, 1, 0, 0));
    float value0101 = dot(hash44(i + float4(0, 1, 0, 1)), f - float4(0, 1, 0, 1));
    float v010 = lerp(value0100, value0101, u.w);
    //001
    float value0010 = dot(hash44(i + float4(0, 0, 1, 0)), f - float4(0, 0, 1, 0));
    float value0011 = dot(hash44(i + float4(0, 0, 1, 1)), f - float4(0, 0, 1, 1));
    float v001 = lerp(value0010, value0011, u.w);
    //110
    float value1100 = dot(hash44(i + float4(1, 1, 0, 0)), f - float4(1, 1, 0, 0));
    float value1101 = dot(hash44(i + float4(1, 1, 0, 1)), f - float4(1, 1, 0, 1));
    float v110 = lerp(value1100, value1101, u.w);
    //011
    float value0110 = dot(hash44(i + float4(0, 1, 1, 0)), f - float4(0, 1, 1, 0));
    float value0111 = dot(hash44(i + float4(0, 1, 1, 1)), f - float4(0, 1, 1, 1));
    float v011 = lerp(value0110, value0111, u.w);
    //101
    float value1010 = dot(hash44(i + float4(1, 0, 1, 0)), f - float4(1, 0, 1, 0));
    float value1011 = dot(hash44(i + float4(1, 0, 1, 1)), f - float4(1, 0, 1, 1));
    float v101 = lerp(value1010, value1011, u.w);
    //111
    float value1110 = dot(hash44(i + float4(1, 1, 1, 0)), f - float4(1, 1, 1, 0));
    float value1111 = dot(hash44(i + float4(1, 1, 1, 1)), f - float4(1, 1, 1, 1));
    float v111 = lerp(value1110, value1111, u.w);
    //压缩至4 00 01 10 11
    float v0 = lerp(lerp(v000, v001, u.z), lerp(v010, v011, u.z), u.y);
    float v1 = lerp(lerp(v100, v101, u.z), lerp(v110, v111, u.z), u.y);
    return lerp(v0, v1, u.x);
}

然后2维的uv在4d中采样。

具体来说,是把2d的uv转化乘4d的uvwx,然后用4d的uvwx获取4d的噪声值作为2d噪声在uv处的值。

2d转换为4d uv的方法是每一个轴向,转化为两个三角函数值:

注意不再对uv进行缩放(注释掉的那行代码),而是对转化的4d的uvwz进行缩放(SplitNum分格)

这里使用了TimeOffset作为随机种子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
float SeamlessNoise(float2 uv) {
    float s = uv.x;
    float t = uv.y;

    float nx = TimeOffset + cos(s * 2.0f * PI) * SplitNum / (2.0f * PI);
    float ny = TimeOffset + cos(t * 2.0f * PI) * SplitNum / (2.0f * PI);
    float nz = TimeOffset + sin(s * 2.0f * PI) * SplitNum / (2.0f * PI);
    float nw = TimeOffset + sin(t * 2.0f * PI) * SplitNum / (2.0f * PI);

    return perlinNoise4D(float4(nx, ny, nz, nw));
}

[numthreads(8, 8, 1)]
void Seamless(uint3 id : SV_DispatchThreadID)
{
    int index = id.x + TextureSize * id.y;
    float2 uv = id.xy / TextureSize;
    //uv = uv * SplitNum;

    Result[index] = SeamlessNoise(uv) * 0.5 + 0.5;
}

录制_2022_08_14_18_45_14_686

缩放不必是整数,对所有噪声都有效,但是计算量更大

参考:

https://catlikecoding.com/unity/tutorials/pseudorandom-noise/

https://thebookofshaders.com/10/?lan=ch

https://blog.csdn.net/candycat1992/article/details/50346469

https://zhuanlan.zhihu.com/p/275695109