《地平线零》体积云UE复刻

在看过众多的体积云实现方案后,也想尝试自己建一个小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)结果如下图所示:

Perlin噪声

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阶
1阶
2阶

将三种贴图以{0.625, 0.25, 0.125}权重混合后的结果如下图所示:

FBM

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中的示例结果不太一致:

Perlin Worley Remap

反而是将二者直接相加,得到的结果具有更高的相似性:

Perlin + Worley

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;
}

结果贴图如下图所示:

R
G

2. Height LUT贴图,分辨率为1x128,RGB三通道,每个通道对应一种不同类型的低空云:stratus(层云),startocumulus(层积云)以及cumulus(积云)

这张贴图直接使用PS进行制作,结果如下图所示:


R
G
B
RGB

3. Weather Texture,虽然没有给出具体的分辨率,但是从使用逻辑推测,这张贴图分辨率不需要太高,这里直接使用128x128。另外,结合两篇文章,这张贴图包含三个通道,分别对应Coverage,Precipitation以及Cloud Type等三个参数,这三个参数都是运行时动态变化的,因此这个贴图应该也是运行时动态生成的,背后的具体实现算法没有给出,但是从结果上看跟Perlin噪声相似度非常高,因此这里尝试使用Perlin噪声来对其进行模拟。

这张贴图要具备如下几个特性:

  1. 能够随时间动态变化,在固定的参数(比如coverage以及octave等),添加上时间就能够实现这个特性,而时间的作用则是通过风力方向来表现
  2. 能够随天气的变化而变化,这里的天气变化无非就是雨天与晴天的变化,雨天可以通过云层密度达到一个阈值来进行模拟,而这个则可以通过一个随着时间缓慢变化的Perlin噪声贴图来进行模拟,其移动方向应该与风力方向保持一致
  3. 在没有云彩的地方,不应该出现下雨的情况,因此Precipitaion与CloudType参数应该具有一定的耦合性
  4. 要能实现覆盖率从极高到极低的自然过渡,

从上述描述来看,这三个通道的运动方向应该是一致的,后两个通道的运动速度可以与第一个通道做一些区分,以实现风力不动情况下的云层变动效果。

下面是按照这种做法输出的结果:

Coverage
Precipitation
Cloud Type
RGB

到此为止,《地平线零》体积云方案所涉及到的输入贴图就算准备妥当了,下一步就开始进入渲染实现阶段。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容