原文:
Value Noise
上一篇文章介绍了如何在着色器中生成随机数。这里我想对前面生成的随机噪声图进行插值,得到一个光滑、渐变的噪声图。因为我们需要事先生成好的噪声图来进行插值,所以建议你先阅读上一篇来实现噪声图。本篇讲的值噪声和泊林噪声有点区别,首先两者都是在上一篇生成的随机噪声的基础上进一步平滑得到的,区别在于,本篇将上一篇得到的噪声当做值来处理,而泊林噪声则将其当做方向来处理。
Show a Line
首先我们来实现一个简单的一维噪声可视化显示,这个是以上一篇中的噪声块为起点,然后改变这个色块的尺寸变量为标量,因为我们将处理的是一维数据。然后我们只通过世界坐标的x
值来生成一维噪声。
1 2 3
| Properties { _CellSize ("Cell Size", Range(0, 1)) = 1 }
|
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
为了实现色块之间插值功能,首先我们需要在片段着色器中执行两次噪声采样,分别是当前色块,和上一个色块。我们可以用floor
和ceil
两个函数,来计算上下两个色块的值。这里我们还可以直接使用坐标的小数部分作为插值位置。
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
分别计算四个色块的噪声值,然后分别计算x
、y
轴方向的缓入缓出插值,首先沿着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给两小钱。总之,各位大爷,走过路过不要错过,有钱的捧个钱场,没钱的捧个人场:-)!!!