0%

Value Noise

原文:
Value Noise

上一篇文章介绍了如何在着色器中生成随机数。这里我想对前面生成的随机噪声图进行插值,得到一个光滑、渐变的噪声图。因为我们需要事先生成好的噪声图来进行插值,所以建议你先阅读上一篇来实现噪声图。本篇讲的值噪声和泊林噪声有点区别,首先两者都是在上一篇生成的随机噪声的基础上进一步平滑得到的,区别在于,本篇将上一篇得到的噪声当做值来处理,而泊林噪声则将其当做方向来处理。

Show a Line

首先我们来实现一个简单的一维噪声可视化显示,这个是以上一篇中的噪声块为起点,然后改变这个色块的尺寸变量为标量,因为我们将处理的是一维数据。然后我们只通过世界坐标的x值来生成一维噪声。

1
2
3
Properties {
_CellSize ("Cell Size", Range(0, 1)) = 1
}
1
float _CellSize;
1
2
3
4
void surf (Input i, inout SurfaceOutputStandard o) {
float value = floor(i.worldPos.x / _CellSize);
o.Albedo = rand1dTo1d(value);
}

这样我们可以看到我们的噪声图沿着x方向分布。接下来我们将这个分布图改为曲线表示,这样我们可以清楚地看到噪声的变化情况。首先我们将像素点的y坐标减去该像素的噪声值,然后取绝对值。

1
2
3
4
5
6
void surf (Input i, inout SurfaceOutputStandard o) {
float value = floor(i.worldPos.x / _CellSize);
float noise = rand1dTo1d(value);
float dist = abs(noise - i.worldPos.y);
o.Albedo = dist;
}

然后我们使用这个差值,并且设定一个阈值来剔除远离噪声值得点,这样我们就得到一根细细的线,不过这个阈值不好选定,线的粗细也不好控制。这里有一个更好的方法,通过计算像素值之间的梯度值,我们可以精确的到宽度为1个像素的细线。需要用到的函数就是fwidth,这个函数会自动比较相邻像素之间的值,然后返回梯度值。这里我们对世界坐标点的y求梯度,其物理含义是像素点的单位长度。所以用像素点的单位长度来作为阈值,就可以控制线的粗细了。

1
2
3
4
5
6
7
8
void surf (Input i, inout SurfaceOutputStandard o) {
float value = floor(i.worldPos.x / _CellSize);
float noise = rand1dTo1d(value);
float dist = abs(noise - i.worldPos.y);
float pixelHeight = fwidth(i.worldPos.y);
float lineIntensity = smoothstep(0, pixelHeight, dist);
o.Albedo = lineIntensity;
}

Interpolate Cells in one Dimension

为了实现色块之间插值功能,首先我们需要在片段着色器中执行两次噪声采样,分别是当前色块,和上一个色块。我们可以用floorceil两个函数,来计算上下两个色块的值。这里我们还可以直接使用坐标的小数部分作为插值位置。

1
2
3
4
float value = i.worldPos.x / _CellSize;
float previousCellNoise = rand1dTo1d(floor(value));
float nextCellNoise = rand1dTo1d(ceil(value));
float noise = lerp(previousCellNoise, nextCellNoise, frac(value));

插值后得到一根连续的曲线,不过我希望曲线更光滑一点。因此,我们可以实现简单的缓动函数,后面我们还会深入的介绍缓动函数,不过这里使用简单的几种就够了。首先我们实现名为easeIn的缓入函数,这里我们执行平方操作,这样插值的边界值还是0-1,但是在接近0的时候变化更缓慢。然后我们将缓入函数应用到我们的插值中。

1
2
3
inline float easeIn(float interpolator){
return interpolator * interpolator;
}
1
2
3
float interpolator = frac(value);
interpolator = easeIn(interpolator);
float noise = lerp(previousCellNoise, nextCellNoise, interpolator);

使用缓入函数后,我们发现色块的开头位置更加平缓。下面我们再实现一个名为EaseOut的缓出函数,使得色块结尾位置更加平缓。在实现缓出函数时,我们利用了前面的缓入函数,不过将插值翻了个个,这样就是1附近的值变化更缓慢。然后我们还要将结果翻转一遍,这样可以保证缓入缓出同时应用的时候,连接部位是光滑的。

1
2
3
float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

最后一步是将两者结合,实现缓入缓出的效果。这里我们还是使用线性插值函数,当插值接近0时,我们倾向于使用缓入效果,当插值接近1时,我们倾向于使用缓出效果。

1
2
3
4
5
float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}
1
2
3
float interpolator = frac(value);
interpolator = easeInOut(interpolator);
float noise = lerp(previousCellNoise, nextCellNoise, interpolator);

有了缓入缓出函数,我们就可以对我们的一维噪声色块进行平滑处理了。

Interpolate Cells in two Dimensions

实现两个维度的插值,我们可以选择相邻的四个色块。然后沿着x轴进行平滑处理、再沿着y轴进行平滑处理。

现在代码量比较多,所以我们单独封装一个函数来执行者两个维度的插值逻辑。这里我们使用rand2dTo1d分别计算四个色块的噪声值,然后分别计算xy轴方向的缓入缓出插值,首先沿着x方向计算两两之间的插值,然后将结果沿着y方向计算最终的插值结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
float ValueNoise2d(float2 value){
float upperLeftCell = rand2dTo1d(float2(floor(value.x), ceil(value.y)));
float upperRightCell = rand2dTo1d(float2(ceil(value.x), ceil(value.y)));
float lowerLeftCell = rand2dTo1d(float2(floor(value.x), floor(value.y)));
float lowerRightCell = rand2dTo1d(float2(ceil(value.x), floor(value.y)));

float interpolatorX = easeInOut(frac(value.x));
float interpolatorY = easeInOut(frac(value.y));

float upperCells = lerp(upperLeftCell, upperRightCell, interpolatorX);
float lowerCells = lerp(lowerLeftCell, lowerRightCell, interpolatorX);

float noise = lerp(lowerCells, upperCells, interpolatorY);
return noise;
}
1
2
3
4
5
6
void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xy / _CellSize;
float noise = ValueNoise2d(value);

o.Albedo = noise;
}

Interpolate Cells in Three Dimensions and Loops

三个维度的插值方法和两个维度的方法基本一致。首先需要选择八个相邻的色块,然后沿着x轴进行两两插值,其结果再沿着y轴进行两两插值,其结果再沿着z轴两两插值,得到最终的插值结果。

但是为三维插值重写上面的方法,会产生很多行代码,不利于理解和管理。所以我们使用循环语句来避免这个问题。这里每个循环迭代两次,分别计算相邻两个需要插值的量。而最内层的循环将计算x方向相邻色块的值,然后存入临时数组中,待执行完后,计算x方向的插值。这里我们在循环语句前加[unroll],表示我们的循环在编译时会被展开。因为GPU执行循环语句比较慢,而展开后便不再是循环语句了。

1
2
3
4
5
6
7
8
9
10
11
float interpolatorX = easeInOut(frac(value.x));

int y = 0, z = 0;

float cellNoiseX[2];
[unroll]
for(int x=0;x<=1;x++){
float3 cell = floor(value) + float3(x, y, z);
cellNoiseX[x] = rand3dTo1d(cell);
}
float interpolatedX = lerp(cellNoiseX[0], cellNoiseX[1], interpolatorX);

然后在外层再套一个循环,这个循环也会执行两次,然后将上一个循环中的插值结果存入临时数组,待结束后进行插值。这个操作和前面的二维插值效果基本一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
float interpolatorX = easeInOut(frac(value.x));
float interpolatorY = easeInOut(frac(value.y));

int z = 0;

float cellNoiseY[2];
[unroll]
for(int y=0;y<=1;y++){
float cellNoiseX[2];
[unroll]
for(int x=0;x<=1;x++){
float3 cell = floor(value) + float3(x, y, z);
cellNoiseX[x] = rand3dTo1d(cell);
}
cellNoiseY[y] = lerp(cellNoiseX[0], cellNoiseX[1], interpolatorX);
}
float interpolatedXY = lerp(cellNoiseY[0], cellNoiseY[1], interpolatorY);

最后在外层再套一个循环,和上一层循环类似,会将上一层循环执行两遍,在一个临时数组中记录上一层的插值结果。然后在循环结束后,对临时数组中的结果进行插值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
float ValueNoise3d(float3 value){
float interpolatorX = easeInOut(frac(value.x));
float interpolatorY = easeInOut(frac(value.y));
float interpolatorZ = easeInOut(frac(value.z));

float cellNoiseZ[2];
[unroll]
for(int z=0;z<=1;z++){
float cellNoiseY[2];
[unroll]
for(int y=0;y<=1;y++){
float cellNoiseX[2];
[unroll]
for(int x=0;x<=1;x++){
float3 cell = floor(value) + float3(x, y, z);
cellNoiseX[x] = rand3dTo1d(cell);
}
cellNoiseY[y] = lerp(cellNoiseX[0], cellNoiseX[1], interpolatorX);
}
cellNoiseZ[z] = lerp(cellNoiseY[0], cellNoiseY[1], interpolatorY);
}
float noise = lerp(cellNoiseZ[0], cellNoiseZ[1], interpolatorZ);
return noise;
}
1
2
3
4
5
6
void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos.xyz / _CellSize;
float noise = ValueNoise3d(value);

o.Albedo = noise;
}

3d Output Values

上面实现了一维噪声值得平滑处理,把它改成三维噪声的平滑处理非常简单。只需要把随机函数从一维改为三维。然后将所有与噪声值相关的数据类型都改为三维。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
float3 ValueNoise3d(float3 value){
float interpolatorX = easeInOut(frac(value.x));
float interpolatorY = easeInOut(frac(value.y));
float interpolatorZ = easeInOut(frac(value.z));

float3 cellNoiseZ[2];
[unroll]
for(int z=0;z<=1;z++){
float3 cellNoiseY[2];
[unroll]
for(int y=0;y<=1;y++){
float3 cellNoiseX[2];
[unroll]
for(int x=0;x<=1;x++){
float3 cell = floor(value) + float3(x, y, z);
cellNoiseX[x] = rand3dTo3d(cell);
}
cellNoiseY[y] = lerp(cellNoiseX[0], cellNoiseX[1], interpolatorX);
}
cellNoiseZ[z] = lerp(cellNoiseY[0], cellNoiseY[1], interpolatorY);
}
float3 noise = lerp(cellNoiseZ[0], cellNoiseZ[1], interpolatorZ);
return noise;
}
1
2
3
4
5
6
void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos.xyz / _CellSize;
float3 noise = ValueNoise3d(value);

o.Albedo = noise;
}

Source

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
Shader "Tutorial/025_value_noise/1d" {
Properties {
_CellSize ("Cell Size", Range(0, 1)) = 1
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

float _CellSize;

struct Input {
float3 worldPos;
};

float easeIn(float interpolator){
return interpolator * interpolator;
}

float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}

void surf (Input i, inout SurfaceOutputStandard o) {
float value = i.worldPos.x / _CellSize;
float previousCellNoise = rand1dTo1d(floor(value));
float nextCellNoise = rand1dTo1d(ceil(value));
float interpolator = frac(value);
interpolator = easeInOut(interpolator);
float noise = lerp(previousCellNoise, nextCellNoise, interpolator);

float dist = abs(noise - i.worldPos.y);
float pixelHeight = fwidth(i.worldPos.y);
float lineIntensity = smoothstep(0, pixelHeight, dist);
o.Albedo = lineIntensity;
}
ENDCG
}
FallBack "Standard"
}
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
52
53
54
55
56
57
58
59
60
Shader "Tutorial/025_value_noise/2d" {
Properties {
_CellSize ("Cell Size", Range(0, 1)) = 1
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

float _CellSize;

struct Input {
float3 worldPos;
};

float easeIn(float interpolator){
return interpolator * interpolator;
}

float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}

float ValueNoise2d(float2 value){
float upperLeftCell = rand2dTo1d(float2(floor(value.x), ceil(value.y)));
float upperRightCell = rand2dTo1d(float2(ceil(value.x), ceil(value.y)));
float lowerLeftCell = rand2dTo1d(float2(floor(value.x), floor(value.y)));
float lowerRightCell = rand2dTo1d(float2(ceil(value.x), floor(value.y)));

float interpolatorX = easeInOut(frac(value.x));
float interpolatorY = easeInOut(frac(value.y));

float upperCells = lerp(upperLeftCell, upperRightCell, interpolatorX);
float lowerCells = lerp(lowerLeftCell, lowerRightCell, interpolatorX);

float noise = lerp(lowerCells, upperCells, interpolatorY);
return noise;
}

void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xy / _CellSize;
float noise = ValueNoise2d(value);

o.Albedo = noise;
}
ENDCG
}
FallBack "Standard"
}
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Shader "Tutorial/025_value_noise/3d" {
Properties {
_CellSize ("Cell Size", Range(0, 1)) = 1
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

float _CellSize;

struct Input {
float3 worldPos;
};

float easeIn(float interpolator){
return interpolator * interpolator;
}

float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}

float3 ValueNoise3d(float3 value){
float interpolatorX = easeInOut(frac(value.x));
float interpolatorY = easeInOut(frac(value.y));
float interpolatorZ = easeInOut(frac(value.z));

float3 cellNoiseZ[2];
[unroll]
for(int z=0;z<=1;z++){
float3 cellNoiseY[2];
[unroll]
for(int y=0;y<=1;y++){
float3 cellNoiseX[2];
[unroll]
for(int x=0;x<=1;x++){
float3 cell = floor(value) + float3(x, y, z);
cellNoiseX[x] = rand3dTo3d(cell);
}
cellNoiseY[y] = lerp(cellNoiseX[0], cellNoiseX[1], interpolatorX);
}
cellNoiseZ[z] = lerp(cellNoiseY[0], cellNoiseY[1], interpolatorY);
}
float3 noise = lerp(cellNoiseZ[0], cellNoiseZ[1], interpolatorZ);
return noise;
}

void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos.xyz / _CellSize;
float3 noise = ValueNoise3d(value);

o.Albedo = noise;
}
ENDCG
}
FallBack "Standard"
}

本篇主要讲了使用线性插值来实现噪声图的平滑处理,希望能对你有所帮助。

你可以在以下链接找到源码:

相关文章

希望你能喜欢这个教程哦!如果你想支持我,可以关注我的推特,或者通过ko-fi、或patreon给两小钱。总之,各位大爷,走过路过不要错过,有钱的捧个钱场,没钱的捧个人场:-)!!!