在看过众多的体积云实现方案后,也想尝试自己建一个小demo跟大师们进行隔空对话,这里首先尝试在UE中对《地平线零》的体积云方案进行复刻,主要参考如下Siggraph15的PPT与GPU Pro 7的分享。
1. 准备工作
根据两篇参考文章,我们需要准备体积云实现的相关贴图资源:
1. 噪声贴图
1.1 低频Worley噪声贴图,分辨率为128x128x128
R通道存储的是使用Worley噪声调制过的PerlinFBM
GBA通道存储的则是低频(012阶)的Worley噪声,根据GPU Pro7中的说法,这三个通道存储的是Worley噪声,但是从给出的贴图来看,实际上存储的是多个不同Cell Num的Worley噪声的叠加,且根据后面的使用方法来看,这三个通道后面还会按照一定的方式进行混合,为了避免运行时混合,这里直接在离线计算的时候先完成混合
1.2 高频Worley噪声贴图,分辨率为32x32x32
RGB三通道存储的是高频(345阶)的Worley噪声,其实现算法跟低频噪声贴图中的GBA通道的结果完全一致
出于渲染效率与带宽消耗考虑,这里将PW Noise,低频WorleyFBM与高频WorleyFBM都放到一张贴图中(分别占据RGB三通道),此贴图分辨率为128x128x128。
Perlin噪声的实现算法代码给出如下:
float sum = 0.0;
float frequency = pow(2, firstOctave);
const float persistence = 0.6;
float amplitude = pow(persistence, firstOctave);
for(int i=firstOctave; i < firstOctave + accumOctaves;i++)
{
uvw += iTime * TimeScale;
sum += perlin(uvw * frequency, SliceNum, Coverage, NoiseTexRes, WhiteNoise, WhiteNoiseSampler) * amplitude;
frequency *= 2.0;
amplitude *= persistence;
}
return sum;
相关函数实现给出如下:
float perlin(float3 uvw, float SliceNum, float Coverage, int NoiseTexRes, Texture2D WhiteNoise, SamplerState WhiteNoiseSampler)
{
float3 pi = floor(uvw);
float3 pf = uvw - pi;
float u = fade(pf.x);
float v = fade(pf.y);
float w = fade(pf.z);
// SliceNum = 1.0f;
float3 posOffsetArr[8] =
{
float3(0, 0, 0),
float3(1.0, 0, 0),
float3(0, 1.0, 0),
float3(1.0, 1.0, 0),
float3(0, 0, 1.0 / SliceNum),
float3(1.0, 0, 1.0 / SliceNum),
float3(0, 1.0, 1.0 / SliceNum),
float3(1.0, 1.0, 1.0 / SliceNum),
};
float3 posNoOffsetArr[8] =
{
float3(0.0, 0.0, 0.0),
float3(1.0, 0.0, 0.0),
float3(0.0, 1.0, 0.0),
float3(1.0, 1.0, 0.0),
float3(0.0, 0.0, 1.0),
float3(1.0, 0.0, 1.0),
float3(0.0, 1.0, 1.0),
float3(1.0, 1.0, 1.0),
};
return COSInterpolation(
COSInterpolation(
COSInterpolation(
grad3D(rand(pi + posNoOffsetArr[0], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[0]),
grad3D(rand(pi + posNoOffsetArr[1], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[1]),
u ),
COSInterpolation(
grad3D(rand(pi + posNoOffsetArr[2], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[2]),
grad3D(rand(pi + posNoOffsetArr[3], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[3]),
u ),
v ),
COSInterpolation(
COSInterpolation(
grad3D(rand(pi + posNoOffsetArr[4], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[4]),
grad3D(rand(pi + posNoOffsetArr[5], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[5]),
u ),
COSInterpolation(
grad3D(rand(pi + posNoOffsetArr[6], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[6]),
grad3D(rand(pi + posNoOffsetArr[7], NoiseTexRes, WhiteNoise, WhiteNoiseSampler), pf - posOffsetArr[7]),
u ),
v ),
w ) + Coverage;
}
float fade(float t)
{
return t;
}
float COSInterpolation(float x,float y,float n)
{
return lerp(x, y, n);
// float r = n*3.1415926;
// float f = (1.0-cos(r))*0.5;
// return x*(1.0-f)+y*f;
}
float grad3D(float hash, float3 pos)
{
float angle = 6.283185 * hash + 4.0 * pos.z * hash;
return dot(float2(cos(angle), sin(angle)), pos.xy);
// int h = int(1e4*hash) & 15;
// float u = h<8 ? pos.x : pos.y,
// v = h<4 ? pos.y : h==12||h==14 ? pos.x : pos.z;
// return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
}
Perlin噪声(Revert)结果如下图所示:
Worley噪声实现算法的代码给出如下:
if(IsFBM > 0.001)
{
//Worley FBM
float fFinalWorley = 0;
float WorleyNoiseWeight[3] = {0.625, 0.25, 0.125};
for(int nChannel = 0; nChannel < 3; nChannel ++)
{
int nStartOctave = firstOctave + nChannel * OctaveSpan;
float frequency = pow(2, nStartOctave);
uvw += iTime * 0.04;
float WorleyNoise = clamp(worley(uvw * frequency, CellNum, NoiseTexRes, WhiteNoise, WhiteNoiseSampler) * saturateness, 0.0, 1.0);
if(MulCellNumAdd > 0.1f)
{
WorleyNoise += clamp(worley(uvw * frequency, CellNum * 2, NoiseTexRes, WhiteNoise, WhiteNoiseSampler) * saturateness, 0.0, 1.0);
WorleyNoise /= 2.0;
}
fFinalWorley += WorleyNoise * WorleyNoiseWeight[nChannel];
}
return fFinalWorley;
}
else
{
float frequency = pow(2, firstOctave);
uvw += iTime * 0.04;
float fCurWorleyNoise = clamp(worley(uvw* frequency, CellNum, NoiseTexRes, WhiteNoise, WhiteNoiseSampler) * saturateness, 0.0, 1.0);
if(MulCellNumAdd > 0.1f)
{
fCurWorleyNoise += clamp(worley(uvw* frequency, CellNum * 2, NoiseTexRes, WhiteNoise, WhiteNoiseSampler) * saturateness, 0.0, 1.0);
fCurWorleyNoise /= 2.0f;
}
return fCurWorleyNoise;
}
相应函数的代码给出如下:
float worley(float3 coord, int CellNum, int NoiseTexRes, Texture2D WhiteNoise, SamplerState WhiteNoiseSampler)
{
float3 cell = floor(coord * CellNum);
float dist = 10000.0;
// Search in the surrounding 5x5x5 cell block
for (int x = 0; x < 5; x++)
{
for (int y = 0; y < 5; y++)
{
for(int z = 0; z < 5; z++)
{
float3 cell_point = get_cell_point(cell + float3(x-2, y-2, z-2), CellNum, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
dist = min(dist, length(cell_point - coord));
}
}
}
return dist;
}
float3 get_cell_point(float3 cell, int CellNum, int NoiseTexRes, Texture2D WhiteNoise, SamplerState WhiteNoiseSampler)
{
float3 cell_base = cell / CellNum;
float noise_x = rand(cell.xyz, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
float noise_y = rand(cell.yzx, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
float noise_z = rand(cell.zxy, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
return cell_base + (0.5 + 1.5 * float3(noise_x, noise_y, noise_z)) / CellNum;
}
float rand(float3 pos, int NoiseTexRes, Texture2D WhiteNoise, SamplerState WhiteNoiseSampler)
{
float2 uv = pos.xy + pos.z;
return Texture2DSample(WhiteNoise, WhiteNoiseSampler, uv / NoiseTexRes).r;
}
输出贴图结果如下所示:
将三种贴图以{0.625, 0.25, 0.125}权重混合后的结果如下图所示:
GPU Pro 7中介绍,Perlin噪声与Worley噪声的结合是通过一个Remap完成的:
float Remap(float original_value , float original_min, float original_max , float new_min , float new_max)
{
return new_min + ((original_value - original_min) / (original_max - original_min)) * (new_max - new_min);
}
但实际上这里测试发现,使用Remap得到的结果跟论文以及PPT中的示例结果不太一致:
反而是将二者直接相加,得到的结果具有更高的相似性:
1.3 Curl噪声贴图,分辨率为128x128,包含RGB三个通道,但是在GPU Pro 7的文档给出的代码片段只使用了RG两个通道的数据,用于对采样位置进行扰动,因此这里只给出2D Curl噪声的生成:
相关代码给出如下:
//http://platforma-kooperativa.org/media/uploads/curl_noise_slides.pdf
float eps = 0.0001;
float x = uv.x;
float y = uv.y;
float fCoverage = 0.7f;
float TimeScale = 0.004f;
//Find rate of change in X direction
float n1 = potential(float3(x + eps, y, 0), firstOctave, accumOctaves, iTime, TimeScale, 1, fCoverage, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
float n2 = potential(float3(x - eps, y, 0), firstOctave, accumOctaves, iTime, TimeScale, 1, fCoverage, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
//Average to find approximate derivative
float a = (n1 - n2)/(2 * eps);
//Find rate of change in Y direction
float n3 = potential(float3(x, y + eps, 0), firstOctave, accumOctaves, iTime, TimeScale, 1, fCoverage, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
float n4 = potential(float3(x, y - eps, 0), firstOctave, accumOctaves, iTime, TimeScale, 1, fCoverage, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
//Average to find approximate derivative
float b = (n3 - n4)/(2 * eps);
//Curl
return float2(a, -b);
相关实现函数给出如下:
float potential(float3 uvw, int firstOctave, int accumOctaves, int iTime, float TimeScale, float SliceNum, float Coverage, int NoiseTexRes, Texture2D WhiteNoise, SamplerState WhiteNoiseSampler)
{
float noise = perlinFBM(uvw, firstOctave, accumOctaves, iTime, TimeScale, SliceNum, Coverage, NoiseTexRes, WhiteNoise, WhiteNoiseSampler);
return abs(1 - 2 * noise);
}
float perlinFBM(float3 uvw, int firstOctave, int accumOctaves, int iTime, float TimeScale, float SliceNum, float Coverage, int NoiseTexRes, Texture2D WhiteNoise, SamplerState WhiteNoiseSampler)
{
float sum = 0.0;
float frequency = pow(2, firstOctave);
const float persistence = 0.6;
float amplitude = pow(persistence, firstOctave);
for(int i=firstOctave; i < firstOctave + accumOctaves;i++)
{
uvw += iTime * TimeScale;
sum += perlin(uvw * frequency, SliceNum, Coverage, NoiseTexRes, WhiteNoise, WhiteNoiseSampler) * amplitude;
frequency *= 2.0;
amplitude *= persistence;
}
return sum;
}
结果贴图如下图所示:
2. Height LUT贴图,分辨率为1x128,RGB三通道,每个通道对应一种不同类型的低空云:stratus(层云),startocumulus(层积云)以及cumulus(积云)
这张贴图直接使用PS进行制作,结果如下图所示:
3. Weather Texture,虽然没有给出具体的分辨率,但是从使用逻辑推测,这张贴图分辨率不需要太高,这里直接使用128x128。另外,结合两篇文章,这张贴图包含三个通道,分别对应Coverage,Precipitation以及Cloud Type等三个参数,这三个参数都是运行时动态变化的,因此这个贴图应该也是运行时动态生成的,背后的具体实现算法没有给出,但是从结果上看跟Perlin噪声相似度非常高,因此这里尝试使用Perlin噪声来对其进行模拟。
这张贴图要具备如下几个特性:
- 能够随时间动态变化,在固定的参数(比如coverage以及octave等),添加上时间就能够实现这个特性,而时间的作用则是通过风力方向来表现
- 能够随天气的变化而变化,这里的天气变化无非就是雨天与晴天的变化,雨天可以通过云层密度达到一个阈值来进行模拟,而这个则可以通过一个随着时间缓慢变化的Perlin噪声贴图来进行模拟,其移动方向应该与风力方向保持一致
- 在没有云彩的地方,不应该出现下雨的情况,因此Precipitaion与CloudType参数应该具有一定的耦合性
- 要能实现覆盖率从极高到极低的自然过渡,
从上述描述来看,这三个通道的运动方向应该是一致的,后两个通道的运动速度可以与第一个通道做一些区分,以实现风力不动情况下的云层变动效果。
下面是按照这种做法输出的结果:
到此为止,《地平线零》体积云方案所涉及到的输入贴图就算准备妥当了,下一步就开始进入渲染实现阶段。