Unity Ray Marching體積云

中積云

淡積云

層積云
絮狀高積云

積雨云

雨層云

云層之上

以上圖中的柱子參考為1000m高,Ray Marching采樣數(shù)為64,云分辨率為960x540,屏幕分辨率1920x1080,天空被覆蓋較多半透明的云時(shí)耗時(shí)約1.5ms,云較少或者覆蓋較多濃度較高的云時(shí)耗時(shí)約1ms。

本次實(shí)踐主要參考的是SIGGRAPH 2015的The Real-time Volumetric Cloudscapes of Horizon - Zero Dawn以及Convincing Cloud Rendering

雖然網(wǎng)上也有不少體積云的案例,不過(guò)我還是打算考驗(yàn)下自己,動(dòng)手實(shí)踐一次。其中大部分實(shí)現(xiàn)方式是參考PPT和論文的,這篇博客做一個(gè)簡(jiǎn)單解釋。

形態(tài)/噪聲生成

云的基本形態(tài)是在一個(gè)128x128x128的3D紋理采樣得到的,具體是RGBA四個(gè)通道隔儲(chǔ)存不同頻率的Perlin Noise或者Worley Noise,并且要求tileable,否則生成的云會(huì)出現(xiàn)明顯的縫隙。這里網(wǎng)上沒(méi)有找到比較好的3D Tileable Perlin Noise和Worley Noise的代碼,因此我自己寫(xiě)了一個(gè)可以生成這樣紋理的腳本。腳本可以在我Git倉(cāng)庫(kù)中找到。

Perlin Noise的頂點(diǎn)隨機(jī)向量用極坐標(biāo)方法生成,要注意對(duì)生成的01半徑取三次方根(參見(jiàn)上篇圓盤(pán)均勻采樣)。為了讓紋理能夠無(wú)縫銜接,要讓所有邊界處的隨機(jī)向量的值能夠?qū)?yīng)起來(lái)。Worley Noise更簡(jiǎn)單一點(diǎn),采樣點(diǎn)對(duì)附近晶格內(nèi)部生成的隨機(jī)點(diǎn)取最小距離即可,在邊界處的采樣點(diǎn)要找到對(duì)應(yīng)的銜接處晶格取距離比較。



生成的紋理如圖所示,圖中為兩個(gè)Cube拼接起來(lái),可見(jiàn)紋理之間是無(wú)縫的。

除了基本形態(tài),我們要在此基礎(chǔ)上減去另一個(gè)細(xì)節(jié)紋理,細(xì)節(jié)紋理是32x32x32的3D無(wú)縫紋理,RGB通道分別為頻率不同的Worley Noise。然后我們就能看見(jiàn)云邊緣的類(lèi)似煙霧的細(xì)節(jié)效果。

另外,我們還要使用一張Weather貼圖,幾個(gè)不同通道分別代表云在此地表向上處的濃度,高度,厚度等細(xì)節(jié)。這里我暫時(shí)只用到了一個(gè)濃度Mask,中心高度固定在大氣層上下高度的平均值,厚度和濃度成正比。當(dāng)然這張紋理也需要是無(wú)縫的。


光照/渲染

主要參考比爾朗伯定律和光在云中的各向異性散射,以及一種會(huì)在邊緣處造成暗線(xiàn)的Powder Effect(具體參見(jiàn)論文)。因?yàn)樵剖遣欢ㄐ蔚模⑶覟榱梭w現(xiàn)出云的體積感,因此我們不能用常規(guī)的光柵化方式去渲染云,而要使用Ray Marching這種步進(jìn)的方法在云中采樣疊加數(shù)據(jù)最后得到進(jìn)入眼中的光線(xiàn)。

因此我們將云的渲染當(dāng)做后處理看待。根據(jù)我之前寫(xiě)的使用深度圖重構(gòu)世界空間坐標(biāo)博客中的方法,我們可以同樣將相機(jī)采樣的方向向量作為UV的方式傳入GPU,這里不再贅述。

根據(jù)比爾朗伯定律,光在介質(zhì)中走得越深其衰減越大并呈指數(shù)衰減,如圖所示。



因?yàn)槭侵笖?shù)衰減,我們可以計(jì)算好每一步前進(jìn)積累的透明度衰減,并在每一步中累積相乘得到當(dāng)前步的透明度,這構(gòu)成了Ray Marching半透明材質(zhì)的最基本的光照模型。

根據(jù)Henyey-Greenstein scattering function,光在云這樣的介質(zhì)中的散射并不是像大氣散射一樣趨于各向同性,而是在不同方向上有較大的比例差距。


4.png

圖中其中占據(jù)最大比例的是0°附近的直射光。因此在面對(duì)太陽(yáng)觀察云的時(shí)候,我們可以清晰的看到云的“金邊”。


照片

渲染模擬

我們平常直接能夠觀察到的云的色彩來(lái)自30°到90°左右的散射。陽(yáng)光和天空光在云介質(zhì)中傳播,最終又有一部分從表面?zhèn)鞒觯M(jìn)入我們的眼中,因此,我們需要給表層附近的云添加環(huán)境光和類(lèi)似漫反射的陽(yáng)光散射光。在Ray Marching每一步中,我們都對(duì)朝向太陽(yáng)的方向再采樣幾次查詢(xún)?cè)摰晔欠衲鼙魂?yáng)光照射到,以正確模擬云層表面的光照。

再之后就是Powder Effect,主要效果是給邊緣處增加類(lèi)似“暗邊”的效果(非陽(yáng)光直射入眼)。采用了這個(gè)效果之后云的表面更有對(duì)比度,更能體現(xiàn)云的體積感。


照片

渲染模擬

最后計(jì)算累加得到的光照并不是直接求出的適用于像素的顏色值,因此我們需要進(jìn)行一次Tone Mapping。

float3 ToneMapping (float3 x)
{
    const float A = 0.15;
    const float B = 0.50;
    const float C = 0.10;
    const float D = 0.20;
    const float E = 0.02;
    const float F = 0.30;
    return ((x*(A*x + C * B) + D * E) / (x*(A*x + B) + D * F)) - E / F;
}

Temporal Upsampling

如果在Ray Marching時(shí)采用固定步長(zhǎng),會(huì)出現(xiàn)如圖所示的帶狀條紋。


對(duì)此我們需要在Ray Marching時(shí)采用隨機(jī)變動(dòng)步長(zhǎng)(隨機(jī)數(shù)來(lái)源于采樣一張?jiān)朦c(diǎn)圖)消除這種效果。但是這樣做會(huì)導(dǎo)致噪點(diǎn)的出現(xiàn),文中解決噪點(diǎn)采用的方法是時(shí)間性增采樣(Temporal Upsampling),即計(jì)算采樣點(diǎn)在上一幀中的屏幕UV位置并使用此UV對(duì)上一幀采樣,以達(dá)到升采樣效果消除噪點(diǎn)。

首先我選取的采樣點(diǎn)是當(dāng)前視線(xiàn)穿越大氣層過(guò)程的中點(diǎn),然后乘以從腳本傳來(lái)的上一幀的VP矩陣即可得到上一幀剪裁空間的坐標(biāo),稍加處理就能得到上一幀UV。

previousVP = cam.projectionMatrix * cam.worldToCameraMatrix;
cloudMaterial.SetMatrix("_LastFrameVPMatrix",previousVP);
float4 reprojectionPoint = float4((rayMarchingStart + rayMarchingEnd) / 2,1);
float4 lastFrameClipCoord = mul(_LastFrameVPMatrix, reprojectionPoint);
float2 lastFrameUV  = float2(lastFrameClipCoord.x / lastFrameClipCoord.w, lastFrameClipCoord.y / lastFrameClipCoord.w)* 0.5 + 0.5;

得到上一幀結(jié)果后我們可以采取多種方式混合兩幀,這里我直接采用了簡(jiǎn)單的類(lèi)似SrcAlpha OneMinusSrcAlpha的方法混合。

return lerp(lastFrameCol, currentFrameCol, lerpFac);

這里L(fēng)erp越靠近上一幀,除噪效果越好,但是收斂越慢;越靠近當(dāng)前幀噪點(diǎn)越多,但是收斂快;這里用戶(hù)可以根據(jù)自己的需求調(diào)節(jié)。

顯然這種方式對(duì)相對(duì)玩家移動(dòng)較慢的物體非常有效,我使用這種方法之后確實(shí)能得到不錯(cuò)的效果。如果我們能采用其他方式混合,其實(shí)還可以再降低采樣率,例如地平線(xiàn)零之曙光的體積云就是采用每次更新4x4像素塊中的1/16的方式,極大地提高了渲染效率。

優(yōu)化

顯然我們?cè)赗ay Marching時(shí)并不是所有的光線(xiàn)都需要走完規(guī)定的采樣步數(shù),如果光線(xiàn)走到已經(jīng)完全不透光時(shí)就不需再往下走。


累積透光度為0提早退出采樣的像素

同樣,如果某條光線(xiàn)上完全沒(méi)有云我們能直接看到天空,那么我們也可以降低采樣數(shù)。我們需要得到上一幀對(duì)應(yīng)位置的透光度,如果非常接近1的話(huà)那么在本幀我們將大幅提高每一步的步長(zhǎng)。


能直接看到天空提早退出采樣的像素

如果采樣過(guò)程中連續(xù)好幾步都采樣到較低密度的話(huà),我們此時(shí)將采用更“便宜”的采樣并增加一定的步長(zhǎng),直到下一次采樣到超過(guò)我們?cè)O(shè)定的密度閾值再換回常規(guī)采樣。這里的“便宜“具體指的是我們不會(huì)在此去計(jì)算那幾個(gè)朝向太陽(yáng)的采樣。

另外,Temporal Upsampling也讓我能夠把記錄Ray marching分辨率降至原先的一半(1/4像素)。

采用這幾種方式之后,渲染效率大概提高到了原先沒(méi)有任何優(yōu)化的情況的20倍(1~1.5ms)左右。盡管圖形質(zhì)量有略微下降,但是至少我們能將體積云渲染放在一個(gè)實(shí)時(shí)的系統(tǒng)里。

待完成

這幾個(gè)有空再做。

首先是對(duì)體積云形態(tài)的進(jìn)一步調(diào)整,我們觀察顯示中云的照片是能夠發(fā)現(xiàn)云層側(cè)邊和底部一般有較多細(xì)節(jié),而頂部形狀較為規(guī)律,因此我們用Detail Texture蝕刻基本形時(shí)要增加一個(gè)和采樣點(diǎn)高度負(fù)相關(guān)的權(quán)重。另外,對(duì)于類(lèi)似積雨云的之類(lèi)的巨型云朵底部應(yīng)該是較為平坦的,這個(gè)要再重新寫(xiě)一下決定形態(tài)的公式。

現(xiàn)在的Shader已經(jīng)可以使用高度圖/厚度圖,之后待生成幾張有趣的圖實(shí)驗(yàn)一下。

嘗試采用地平線(xiàn)零之曙光PPT中提到的每次更新4x4像素塊中的一點(diǎn)的方法極大增高效率,這樣我們就可以有更多資源去消耗在更精確的光照模型上。

目前的混合是只在深度為1的情況下才混合(OnRenderImage階段),因此無(wú)法實(shí)現(xiàn)云中的朦朧物體的效果。目前構(gòu)思是用深度圖重構(gòu)出屏幕的世界空間坐標(biāo)后再對(duì)Ray Marching的起點(diǎn)和終點(diǎn)進(jìn)行調(diào)整,最后直接Alpha混合在屏幕上。這樣應(yīng)該不僅能正確顯示出云中物體的效果,還能在天空被物體遮擋的情況下直接跳過(guò)Ray Marching,提升一些性能。

日出/晚霞/夜晚效果
高層云/卷云等


Github: https://github.com/techizgit/UnityPlayground

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容