1. 程式人生 > >移動端雲渲染的實現

移動端雲渲染的實現

先展示最終效果:

體積雲視訊

類似渲染雲這種自然現象的時候,必須首先了解噪聲這個概念。這個噪聲指的是描述自然界規律的一些隨機函式。例如大名鼎鼎的柏林噪聲。Perlin噪聲被大量用於雲朵、火焰和地形等自然環境的模擬,而Worley噪聲被提出用於模擬一些多孔結構,例如紙張、木紋等。不過其實Wroley噪聲也可以用在雲上面。

對於噪聲這裡不想深入討論,感興趣的可以去看論文或者馮女神的部落格https://blog.csdn.net/candycat1992/article/details/50346469。

另外,我們使用的噪聲其實是這個噪聲:https://www.shadertoy.com/view/4sfGzS

這是Iq大神弄的一個噪聲,效率和表現都算很好。

float noise(in float3 x)
{
    float3 p = floor(x);
    float3 f = frac(x);
    f = f * f*(3.0 - 2.0*f);
    float2 uv2 = (p.xy + float2(37.0, 17.0)*p.z) + f.xy;
    float2 rg = tex2Dlod(_NoiseTex, float4((uv2 + 0.5) / 256.0, 0, 0)).yx;
    return lerp(rg.x, rg.y, f.z);
}
 
float4 map(in float3 p, in float t)
{
    float3 pos = p;
//d就是當前座標距離頂部的差值
    pos.y += _cloudRange.z;
    pos /= _cloudRange.z;
    float d = -max(0.0, pos.y - _cloudRange.y / _cloudRange.z);
    float3 q = pos - _Wind.xyz * _Time.y;
    float f;
    f = 0.5000*noise(q); 
    q = q * 2.02;
    f += 0.2500*noise(q);
    q = q * 2.03;
    f += 0.1250*noise(q);
    q = q * 2.01;
    f += 0.0625*noise(q);
//算出的噪聲就是我們想要的噪聲,然後讓d去和噪聲相加,模擬當前雲的顏色值。
    d += _NoiseMultiplier * f;
    d = saturate(d);
    float4 res = (float4)d;
    res.xyz = lerp(_Bright, _Dark, res.x*res.x);
    return res;
}
這些還不夠,我們還要分析下基本的演算法,看下地平線早期的雲實現:

主要思想還是根據raymarching得到雲的外形,然後加上光照。

接下來先一步一步實現整個過程,但因為手機上要考慮效能,所以會對完整的雲進行大幅度閹割。

中間紅色的部分就是我們攝像機的FOV,那麼可以這麼計算出水平的fov的tan

//算出垂直fov的一半的弧度
float halfFov_vert_rad = Camera.main.fieldOfView * Mathf.Deg2Rad / 2.0f;
//根據tan可以算出當距離為1的時候攝像機的寬度,進行atan就可以得到水平的弧度,依然是一半
float halfFov_horiz_rad = Mathf.Atan(Mathf.Tan(halfFov_vert_rad) * Camera.main.aspect); 
//同樣,在shader裡也是進行如此的計算
void computeCamera(in float2 screenPos, out float3 ro, out float3 rd)
{
        //這是水平tan
    float tanFovH = _TanFov;
        //這是垂直tan
    float tanFovV = _TanFov * _ScreenParams.y / _ScreenParams.x;
    float3 forward = UNITY_MATRIX_V[2].xyz;
    float3 right = UNITY_MATRIX_V[0].xyz;
    float3 up = UNITY_MATRIX_V[1].xyz;
        //出發點就是攝像機的位置
        ro = _WorldSpaceCameraPos;
        //方向根據螢幕的y座標,往垂直方向偏移,根據x座標,往水平方向偏移,看如果y座標或者x座標滿值,剛好就是攝像機的視角邊緣線
    rd = normalize(forward + screenPos.y * tanFovV * up + screenPos.x * tanFovH * right);
}
float4 RayMarch(in float3 ro, in float3 rd, in float zbuf)
{
    float4 sum = (float4)0;
    float dt = 0.1;
    float t = dt;
        //這個是根據方向算出完全朝上的部分
    float upStep = dot(rd, float3(0, 1, 0));
    bool rayUp = upStep > 0;
        
    float angleMultiplier = 1;
 
    for (int i = 0; i < StepCount; i++)
    {
                //攝像機的深度圖-0.1
        float distToSurf = zbuf - t;
                //從ro出發的y增加上rd的y,比例是t
        float rayPosY = ro.y + t * rd.y;
        /* Calculate the cutoff planes for the top and bottom.
        Involves some hardcoding for our particular case. */
                
        float topCutoff = (_CloudVerticalRange.y + _CloudGranularity * max(1., _ParallaxQuotient) + .06*t + max(0, ro.y)) - rayPosY;
        float botCutoff = rayPosY - (_CloudVerticalRange.x - _CloudGranularity * max(1., _ParallaxQuotient) - t / .06 + min(0, ro.y));
 
        if (distToSurf <= 0.001 || (rayUp && topCutoff < 0) || (!rayUp && botCutoff < 0)) break;
 
        // Fade out the clouds near the max z distance
 
        float wt;
        if (zbuf < _ProjectionParams.z - 10)
            wt = (distToSurf >= dt) ? 1. : distToSurf / dt;
        else
            wt = distToSurf / zbuf;
 
 
        RaymarchStep(ro + t * rd, dt, wt, sum, t);
        t += max(dt, _CloudStepMultiplier*t*0.0011);
 
    }
    return saturate(sum);
}
實現以上所有的部分你可以得到一個普通的固定視角看上去還不錯的雲。類似於這樣:

然而,如果你移動你攝像機的位置,你會發現這個雲會出現各種異常。於是我們要一步一步解決問題。

首先是計算量的浪費問題,我們建立的雲層模型是水平的,有頂部和底部組成,攝像機出發的射線,其實是經過頂部和底部的部分才有效果,那麼其實我們可以直接從攝像機的頂部交點或者底部交點開始運算。如果位於攝像機裡面,那麼就是從攝像機位置開始運算。

float t = 0;
if (rd.y < 0)
{
    t = (_cloudTop - ro.y) / rd.y;
    t = max(0, t);
}
else
{
    t = (_cloudBottom - ro.y) / rd.y;
    t = max(0, t);    
}
這樣子就可以解決第一個問題。

由於正常情況下雲層一般較高,移動攝像機位置引起的變化量容易過大,導致一旦開始移動,取樣貼圖的座標也迅速移動,導致雲層瞬間變化。然而我們知道 ,攝像機的那點移動對於雲來說不算什麼,於是我增加了一個縮放係數,因為雲層基本在幾千米左右,我就把這個係數定位1000,然後根據和雲層的距離再做一個基本的非線性關係。

float times = 1000;
if (ro.y > _cloudTop)
{
    times *= pow(max(0, (2 * _cloudTop - ro.y) / _cloudTop), 1);
}
else
{
    times *= pow(max(0, _cloudBottom - ro.y) / _cloudBottom, 0.1);
    times = max(times, 1);
}
 
pos = ro / times + rd * (t - abs(_cloudOffset / rd.y)) + offset;
可以看到,我都會根據頂部還是底部做不同的分支,這其實是我偷懶,可以用直線和線段求教簡化這段程式碼,但現在就先這樣,能實現功能優先。最後還有一個_cloudOffset和offset,我們後面再講解。

接下來是邊緣過度問題,雲層的邊緣超過頂部和底部會直接不計算,那麼會導致一個難看的切邊,於是我要根據頂部和底部增加一個透明過度。另外,如果雲層很厚,我們將整個噪點分佈在全部雲層,會導致雲層和稀薄,於是需要一個迴圈處理,我通過一個簡單的線性周期函式來實現這個東西。

float offy = p.y - _cloudBottom;
float topOff = _cloudTop - p.y;
topOff = clamp(topOff, 0, _cloudEdge) / _cloudEdge;
offy = clamp(offy, 0, _cloudEdge) / _cloudEdge;
float offy1 = _cloudPadding * abs(frac(offy * _cloudLayers) - 0.5) - 1.5;
float den = clamp((_cloudCut - offy1 + _smooth * f) * topOff * offy, 0, 1);
return den
到這裡,理論上應該可以了,但實際操作中發現離雲很遠的時候,雲的噪點很厲害。想象一下在現實中,我們很遠看雲的時候,雲的那些細節會逐漸變成較大塊的純色,邊緣還是有細節。而我們根據位置進行那麼遠的取樣,必然會導致都是噪點。我思來想去,覺得如果一致能維持一個較近的渲染效果,根據距離做一丟丟的變化,那就好了。於是我增加了_cloudOffset這個變數,會根據攝像機的位置和雲的位置動態變化,保證雲的渲染效果一直以較近距離觀察為主。這樣雖然在人靠近雲的感受不太自然,但整體的感覺還可以接受。

最後,是光照。

這部分我我沒有太多想法,而是直接使用了iq大佬的基本思想,雲的邊緣會變數。通過往光線方向取樣,得到新的位置的雲的密度,如果密度變小,說明越靠近雲的邊緣,就更亮。

具體實現的時候依然根據雲的上層和下層做了區別,因為如果一個方向變數,那麼另一個方向反而會變暗,所以需要反向一下y軸。

void addSum(float den, float3 pos, float t, inout float4 sum, float3 sunColor, float3 ro, float3 rd)
{
    if (den > 0.01)
    {
        float a = 0.6;
        if (ro.y > _cloudTop)
        {
            rd = -rd;
            a = 0.45;
        }
            
        float dif = clamp((den - map(pos , ro, rd,  t, 0.3 * _WorldSpaceLightPos0.xyz)) / a, 0.0, 1.0);
        float3 lin = float3(0.65, 0.7, 0.75) * 1.4 + _LightColor0 * dif;
        float4 col = float4(lerp(float3(1, 0.95, 0.8), float3(0.25, 0.3, 0.35), den), den);
        col.xyz *= lin;
        col.a *= 0.4;
        col.rgb *= col.a;
        sum = sum + col * (1 - sum.a);
    }
}
至此,我們大概實現了這樣的雲:

視訊中還有一個穿越雲層的效果,具體可以看視訊內容。

最後說下這個雲的問題。

這個雲還遠遠沒有到可使用的級別,第一,作為後處理特效,最早也只能在非透明物體渲染完成後,也就是雲老是疊在非透明物體上。更合理的應該還是用CommandBuffer去做,可以控制渲染的時機。或者用一個面片去替代後處理,但是面片就無法降取樣了,效能無法達到手機上的標準。所以還是需要用CommandBuffer改造這個雲。

當然也可以自己設定深度快取,這樣子就可以實現雲和物體的交融,但是效能當然就更耗了。

即便沒有上述這些,這個手機場景demo在vivo x6手機上也只能跑30幀。那個穿越雲層的大咖只有10幀。所以,還是需要更好一點的手機才能使用這種方式製作的雲。

總而言之,還需要繼續優化。

最後是外掛地址:

https://www.assetstore.unity3d.com/en/?stay#!/content/133674
--------------------- 
作者:yxriyin 
來源:CSDN 
原文:https://blog.csdn.net/yxriyin/article/details/83892868 
版權宣告:本文為博主原創文章,轉載請附上博文連結!