[OpenGL] 體積雲實現探索

        

這個雲是可以調厚的,目前主要是採樣的總步長不足所以比較薄,但
當然會很卡,目前用的電腦沒有顯卡我就沒有嘗試截圖了

        

        

5.5更新:換了臺有顯卡的電腦截了個動圖

          首先,從雲的形狀開始。由於雲的形狀是不規則的,在圖形學中,爲了模擬這種不規則,我們通常使用噪聲來實現。

          我們使用兩種不同的噪聲來模擬雲的形狀——worley噪聲和perlin噪聲,圖中只使用了兩張紋理,變化量是兩張紋理混合的比例。

         根據地平線的體積雲論文的描述,實際上他們是使用一張perlin-worly噪聲來模擬雲塊狀的基本形狀(左一),不同頻率的worley噪聲模擬(右三)模擬體積雲蓬鬆的細節,不同的頻率對應着不同雲的細節。

        

Worley噪聲 

        我們可以把雲的形狀看作是"一朵朵"的,爲了表現這種塊狀、多孔的效果,我們可以考慮使用worley噪聲來表現。關於worley噪聲的具體描述,可以參閱這一論文 http://weber.itn.liu.se/~stegu/TNM084-2017/worley-originalpaper.pdf

        在討論worley噪聲本身之前,我們有必要了解一下Voronoi圖,它是像下面的這樣一張圖(摘自維基百科):

Voronoi-C.png
Voronoi

        可以看到,空間被劃分爲多個塊狀,而每個塊狀有一箇中心點,worley噪聲的論文中將其稱爲特徵點。這個圖的數學性質是:空間中的任一點,到其所在區域的特徵點的距離(相比起到其它區域特徵點的距離)最短。

        那麼,如果我們想要生成一張Voronoi圖的話,我們只需要考慮特徵點的位置就可以了。因爲在確定了特徵點的位置後,區域會被自然地確定。同時,論文中還定義了第二最短距離、第三最短距離……以及第n最短距離,利用這些性質,我們還能得到很多其它具有獨特外觀的圖案。

       一種可以生成Voronois圖的算法描述如下(3D):

 

       (1)將空間劃分爲立方體網格,通過floor操作得到每個點所落在的網格點位置。

        空間中的每個小立方體可能包含0個,一個或多個特徵點。如果每立方體平均密度爲,則單位立方體中出現點的概率爲。實踐中使用m = 4,並限制每個單元中點處在[1,9]之內。

 

          (2)  確定每個小立方體所包含的特徵點數目。

        特定立方體中特徵點數量的隨機數字需要是確定的,可以將立方體的三個整數座標作爲隨機種子,一個簡單的三元數(i,j,k)的hash函數像,但更好的方法是使用交換數組。

        我們使用種子隨機數生成器中的第一個值作爲特徵點預計算概率列表的索引,以找到立方體中的點數。

 

        (3) 計算m個特徵點的位置。

        同樣的,它們雖然是隨機的,但對於每個立方體而言是固定的,因此我們使用已經初始化好的隨機數生成器來計算每個特徵點的x,y,z位置,範圍爲[0,1]。

 

        (4) 找到距離最近的點,在當前立方體和鄰域進行查找。

       在生成這些點後,由於加入了新的點,我們需要更新某一點到所有特徵點的距離的函數的排序,保證這一序列的順序性。這樣,我們就得到了當前空間的立方體內,點的最近特徵點和一系列Fn的值。但是,相鄰立方體中可能存在更近的點,因此我們需要在邊界立方體也做一次查找。

        根據以上描述,可以實現一個CPU版本的worley 3D噪聲生成。其中給出了一個參考可用的hash函數,此外就是注意隨機數的生成方式,考慮模擬C庫中rand()的線性同餘方法,給定一個特定的初始隨機種子,每次求解新的隨機數時,需要以上一個隨機數爲輸入。如果輸入種子是一定的,那麼就會得到一個僞隨機的固定序列。

        通過CPU的方式生成後需要將結果烘焙爲3D紋理,然後在計算體積雲的時候直接從3D紋理中採樣,避免了每幀重複計算。這個過程很(wo)麻(bu)煩(hui), 於是我又參考了這一GPU的實現https://www.shadertoy.com/view/MslGD8,實際上這是一個利用GPU生成2D Voronoi圖的shader,它的實現相比而言更加簡單,通過一個vec3到vec3的hash映射直接得到了每個特徵點的隨機偏移,然後通過多個循環在鄰域立方體裏查找離當前點的最近距離,再將每個點到其最近特徵點的距離作爲灰度圖的顏色輸入就可以得到我們需要的所謂的worley噪聲。

        計算得到worley 3D噪聲之後,根據世界座標作爲採樣點繪製到了一個立方體上,並且做了反色運算(也就是x = 1 - x)。

       

 

perlin噪聲

        爲了表現雲的蓬鬆和層次,我們使用perlin噪聲來定義細節。

        perlin噪聲是一個梯度噪聲,相關的論文爲《An Image Synthesizer》。

         我們目前已經知道worley噪聲是通過晶格+隨機偏移得到特徵點位置生成的,最後我們根據輸入點到其最近的特徵點的距離得到該位置的噪聲顏色。perlin噪聲也是基於晶格生成的,不同的是它在每個晶格點存儲的是一個隨機梯度,我們根據輸入點與其附近所有晶格點梯度的點乘加權和作爲輸出的噪聲顏色。在此處就不展開詳細說明了。

        此處我試着自己做了一個GPU版本的perlin噪聲,效果如下:

        

       這種方法存在兩個問題,一個是速度無法滿足實時性要求(很卡,即使做了部分預處理),另一個是它和地平線發佈的體積雲論文中的perlin噪聲有一些肉眼可見的差異(下圖最左):

        

       論文中使用的實際上是perlin噪聲的分形版本,也就是將不同尺度的perlin噪聲疊加在一起得到的。

       爲了達到實時性要求,我同樣在shadertoy網站了找了一個比較近似的value噪聲,並手動做了分形處理。

雲的區域

       首先,我們需要定義天空中哪些地方有云,哪些地方無雲。這可以由一張2D的噪聲貼圖來定義,我們可以利用已有的兩個噪聲來得到這張貼圖,用世界座標x,z來採樣。

       

Ray-Maching積累濃度

       我們認爲0是無雲區,大於0爲有云區。但實際上噪聲中爲0的區域是很少的,爲了得到比較連續的無雲區域,我們可以設定一個閾值,低於該閾值的都映射到0,而高於該閾值的,都重新映射到[0,1]區間,用代碼描述則爲:

sampled_density = clamp((sampled_density - thickness) / (1 - thickness),0,1) ;

       之後,我們通過Ray-Matching + 體渲染的方式,可以得到這麼一個基本形狀(右:siggraph):

        

        此處我直接把這個形狀渲染在了一個立方體上, 並把起始採樣高度設置了一個定值。爲了得到圖中的效果,實際上我是在Ray-Matching的時候,如果第一步採樣就採樣到了有云區,則直接返回了一個比較暗的顏色,然後退出循環,作爲雲的底部顏色。

雲的形狀

        確定了有云和無雲區域後,我們再利用一張3D的Worley-Perlin噪聲去構造雲的形狀。

        具體的做法是,在Ray-Marching的每一步,我們用當前雲的世界座標去採樣3D Worley-Perlin 紋理,將得到的值與當前區域雲的密度相乘,得到最終的結果。

        同樣地,我們依然先把這個雲繪製在一個立方體上看一下效果(右:siggraph):

        

        之後,我們給這個雲添加一點細節,我們使用不同頻率的worley噪聲得到一些翻滾的細節,類似於花椰菜的外觀(右:siggraph):

         

​
float GetCloudDensity(vec3 pos)
{
    float thick = GetCloudThickness(pos);
    float base_density = GetCloudBaseShape(pos);
    float detail_density = GetCloudDetailShape(pos);
    return thick * base_density * detail_density;
}

雲與天空盒

       和體積霧之類的體素渲染物體不太一樣,天空和體積雲固定在場景中,不會隨着鏡頭推進而近大遠小地改變形態,而是像遠景一樣不會移動,但能旋轉鏡頭觀察。爲了達到這一效果,我們採用和天空盒類似的方式來處理體積雲的位置(如果此處有不清楚的地方,可以參考https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/06%20Cubemaps/

        在天空盒繪製時候,我們的基本思路是,使用一個單位立方體繪製,深度測試改爲GL_LEQUAL。在頂點着色器中做座標變換時,僅考慮相機矩陣的旋轉,而不考慮移動,這樣可以保證天空盒位置不隨相機變化而變化,並把立方體的世界座標傳給片元着色器。在片元着色器採樣時,將立方體世界座標歸一化(實際上就落在了一個單位球上),用它來採樣CubeMap。

        那麼我們就可以把雲彩繪製在天空盒的位置,將立方體的世界座標映射到一個圓弧或平面上:

      

 

        此時,我們有云和無雲的地方過渡比較突兀,雲直接在分界線被截斷了。爲了緩解這一現象,我們需要根據離眼睛距離做一個線性插值,讓密度緩慢減爲0。

雲的高度分佈

        在剛纔討論雲的天空盒分佈時,提到了可以讓雲在邊界做一個線性淡出的效果。實際上,如果我們把這一思路擴展一下,可以很好地定義雲在不同高度的分佈,比如我們還可以設定高於某一高度後,雲也會線性淡出;不同類型的雲分佈在不同的高度,也就是對特定高度的密度做一個重映射。

      《Horizon: Zero Dawn》中給出了一個密度-高度函數的貼圖,分別代表了層雲(左),積雲(中)和積雨雲(右)。

       

雲的光照

        我們的雲目前看起來只有單薄的白色,缺乏立體感。我們試着加一點光照來提升真實感。由於使用了體渲染,雲的光照需要在Ray-Marching的每一步都計算,並將結果累積起來。

       beer定律:光的衰減

        在現實生活中,雲的底部會有一點偏暗,光通過物體時,光線會不斷衰減,整個過程是一個指數衰減。對於每一步計算得到的光照,都有必要乘以這一衰減值,傳入的參數是Ray Marching開始到目前累積的濃度。其中,p值可以視爲積雨量(吸收水平),值越大,雲整體就越暗。

float beers = exp(-p * density);

        

        糖堆效應:光的內散射

         在雲的凸起摺痕處較多的地方會更亮,我們用糖堆效應來描述這一現象,它本身是一個概率意義上的函數,如下圖藍色線條所示,同樣的,我們也在每一步光照計算中都乘以這一系數。這一系數可以得到雲縫隙偏亮的效果。

float powder = (1.0f - exp(-density*2);

        

        Henyey-Greenstein相位函數

        我們使用hg相位函數來模擬光在雲中靠近雲時,雲邊發出閃耀的銀光的效果,它描述了光的前向散射。該公式與雲的濃度無關,而是取決於當前採樣位置與光源(一般是太陽)的距離,最終得到的效果爲越靠近光源越明亮。

        

        g越大,中心越亮,向外衰減也越快。 

vec3 lightDir = pos - lightPos;
vec3 viewDir = pos - cameraPos;
float cos_angle = dot(normalize(lightDir), normalize(viewDir));
float inG = 0.2;
float hg = ((1.0 - inG * inG) / pow((1.0 + inG * inG - 2.0 * inG * cos_angle), 3.0/2.0)) / 4.0 * 3.1415;

        陰影計算

        雲的底部會有偏暗的效果,爲了得到這種光在雲朵上的明暗效果,我們進行一些簡單的陰影計算,或者我們把其稱爲遮蔽會更加準確。此處可以做一些比較“粗暴"的模擬,我們設定一個比較高的高度(高於雲層),離這一高度越近,雲就越亮。具體的計算是給定一個顏色,除以歸一化後的距離,得到當前高度雲的基本顏色。

        之後,我們對雲的顏色做衰減處理,以當前位置爲基礎向上做Ray-Marching,得到累積後的濃度,然後使用beer定律得到衰減係數。

        處理    

        以上所有係數在每一步中相乘得到當前步採樣顏色,並將所有步得到的顏色累加起來。實際上由於每步的衰減,我們最終得到的顏色已經比較趨向于歸一化的結果了,但是我們還是有必要類似於HDR一樣,做最終的顏色做一些映射,讓其迴歸到一個比較合理的顏色區間。

        

加入體積光

        這個我只是稍微思考了一下大致思路,還沒有動手實現。此處想要模擬的是陽光穿過雲層絲絲縷縷的效果。思路是利用生成雲的區域的噪聲,利用透視投影的shadowMap,判斷當前位置是否處在雲的縫隙處(有陽光穿過)。

        此處可以省略生成shadowMap的過程,因爲雲的位置是不變的,我們直接從噪聲圖採樣即可,但是需要反解出從上往下透視觀察時,正好讓整個噪聲覆蓋視野範圍的透視投影矩陣,將當前位置利用這一矩陣做空間轉換後再進行對比。

後記 

       這個雲差不多折騰了兩週,有幸存了上週做的第一版效果,雖然有那麼一點雲的感覺,但是完全沒有體積雲的意思?

        

       之後我又推倒重做了一次,最後做出來依然沒有論文裏香香軟軟蓬蓬鬆鬆的效果,但好歹有了那麼一點體積的感覺。就這麼作爲最終交付結果吧(

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章