以上圖中的柱子參考為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ì)中的散射并不是像大氣散射一樣趨于各向同性,而是在不同方向上有較大的比例差距。
圖中其中占據(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í)就不需再往下走。
同樣,如果某條光線(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,提升一些性能。
日出/晚霞/夜晚效果
高層云/卷云等