在遊戲裡模擬天空的顏色,太迷人了!

遊資網發表於2020-09-07
幾個世紀以來,天空一直是許多藝術家迷戀的主題,他們試圖儘可能準確地描繪其顏色。同樣的,在遊戲開發中,天空渲染對於遊戲開發者也一直是一個迷人的主題,本文理論部分譯自scratchapixel,介紹了天空渲染的理論基礎,並在此基礎上了建立相應的數學模型,最後在ShaderToy上通過Pixel Shader來實現相應的演算法。

關鍵字

模擬天空顏色、大氣散射、瑞利散射、米氏散射、天空顏色、模擬天空、散射係數、吸收係數、衰減係數、相函式、光學深度、體積渲染、單次散射、多次散射、大氣、陽光

本章我們將學習大氣散射,推薦先閱讀體積渲染和次表面散射的內容(詳見www.scratchapixel.com),它們和本主題包含類似的概念,大氣散射可以看作是體積渲染的擴充套件。

1、大氣散射的理論基礎

1.1 介紹

幾個世紀以來,天空一直是許多藝術家迷戀的主題,他們試圖儘可能準確地描繪其顏色。在19世紀到20世紀之前,許多物理學家和數學家痴迷於弄清楚是什麼導致天空在日落和日出時呈現橙色,白天呈現藍色。從歷史上看,大氣散射的發現歸功於瑞利(又名John Strutt,獲得諾貝爾獎的英國物理學家),其主要工作是在19世紀產生的(瑞利接替了劍橋大學的麥克斯韋,後者因其在電磁方面的貢獻而聞名,計算機圖形很大一部分的研究與麥克斯韋的工作有關,瑞利還和普朗克研究了黑體)。大氣散射也會對物體的外觀產生影響,這種效應被稱為空氣透視(繪畫中多稱為大氣透視),被萊昂納多達芬奇首次觀察、研究和應用在他的畫作中(事實上,這種技術可以在達芬奇作品之前的繪畫作品中找到)。隨著物體和觀察者之間的距離增加,物體的顏色被大氣的顏色所取代(對於天空來說,它通常在白天呈現藍色,日出和日落時呈現紅橙色)。這是一個重要的視覺線索,因為人們通過比較它們相對於彼此有多藍來判斷場景中物體的距離。在(數字)繪畫中,空氣透視可以極大地幫助增加影像的深度(大腦可以更容易地處理比另一個更遠的物體),如下為達芬奇畫作複製品(注意後面的岩石的顏色受到大氣的影響)。

在遊戲裡模擬天空的顏色,太迷人了!

在計算機圖形學史上,Nishita在1993年發表了一篇開創性的論文”Display of the Earth Taking into account Atmospheric Scattering”,他在這篇論文中描述了一種模擬天空顏色的演算法,有趣的是他的這篇論文和他在1996年寫的關於同一主題的另一篇論文"Display Method of the Sky Color Taking into Account Multiple Scattering"不完全是關於大氣模擬,更多是關於從外太空看到的地球的真實渲染(包括渲染海洋表面、雲層和大陸)。

在遊戲裡模擬天空的顏色,太迷人了!

Figure 1: Top, a real photograph of the Earth from outer space. Bottom, a simulation using Nishita's model. These images have been copied from the paper "Display of the Earth taking into account Atmospheric Scattering" written by Nishita in 1993 (c) Siggraph.

這提醒了我們在這些年裡,計算機圖形技術的發展是由製造業推動的,用於精確模擬其產品或提供3D虛擬訓練環境,而不是娛樂行業(電影和遊戲)。Nishita描述的模擬天空的技術從他的時代以來沒有太大變化,在該領域的大部分研究都集中在GPU上實現Nishita的演算法,而模擬質量沒有任何明顯改善(更多關注的是速度和實時模擬)。在1999年發表於Siggraph的一篇論文”A Practical Analytic Model for Daylight”中,Preetham等人描述了另一種天空模型,正如其標題所示,這是一個分析模型,它提供了對天空顏色的精確模擬,但它比Nishita提出的技術有更多限制(例如,該模型僅適用於位於地面上的觀察者)。值得一提的是Jensen等人在2011年Siggraph上發表的一篇模擬夜空的論文”A Physically-Based Nightsky Model”,本章將給出Nishita模型的實現。

在後面小節中,我們將描述各種現象,它們組合在一起呈現天空的顏色,然後我們將展示Nishita的演算法如何模擬這些現象,一旦我們在程式中實現了該模型,我們可以輕鬆擴充套件該技術以模擬空氣透視,通過改變模型的引數,我們還可以建立外星天空。

1.2 大氣模型

大氣層是行星周圍的一層氣體,由於行星的引力,大氣層保持在適當的位置,大氣層的主要關注點是它的厚度和成分(組成大氣層的不同成分)。地球大氣層厚度約為100km(由卡門線劃定,卡門線是公認的外太空與地球大氣層的分界線),它由氧氣、氮氣、氬氣(瑞利發現的氣體)、二氧化碳和水蒸氣組成。然而,在模擬中,我們將使用60km的厚度(我們只考慮在地球大氣的前兩層,對流層和平流層中的散射)。如果你閱讀有關體積渲染的內容(建議首先看這部分內容再來看本篇內容),你將知道光子在與粒子碰撞時會散射。當光在某個方向上穿過一個體積(大氣)時,光子在與粒子碰撞時會往其他方向上偏轉,這種現象稱為散射。光被粒子散射的頻率取決於粒子的性質(主要是它們的大小)和它們的密度(單位體積中有多少粒子/分子)。我們現在已經知道了地球大氣層的分子大小,我們也知道它們的濃度/密度。我們將使用這些科學測量資料來進行大氣散射模擬。地球周圍大氣層的另一個重要方面是這些顆粒的密度隨高度的增加而減小。換句話說,比如在海平面上每單位立方體空氣中的分子比海洋上方2公里多。這是一個重要的事實,因為散射量取決於我們剛剛提到的分子密度。當光在大氣層中從上層傳播到地面時,我們需要沿著光線按固定間隔對大氣密度進行取樣,並根據這些樣本位置處的大氣密度計算散射量(我們將使用數值積分,下一步會詳細解釋)。大多數論文(包括Nishita的論文)都假設大氣密度隨高度呈指數下降。換句話說,它可以用以下形式的簡單方程建模:

在遊戲裡模擬天空的顏色,太迷人了!

其中density(0)是海平面的空氣密度,h是我們測量大氣密度時的高度(海拔高度),H是大氣密度均勻時的大氣厚度(在科學論文中,H被稱為大氣標高,大氣密度每經過一段距離H其值就減少為原來的值的1/e,H取決於溫度)。一些論文聲稱這個模型是一個很好的近似,但是其他一些人聲稱它不準確,並且更喜歡將天空密度模擬為一系列同心層,每個同心層由它們的厚度和密度決定。(論文出處未知)

將天空密度建模為一系列層(在Nishita的論文中稱為球殼)從計算的角度來看具有優勢。由於我們將沿著光線朝著觀察者的路徑執行數值積分,在密度高的大氣中採集的樣本比在密度低的情況下采集的樣本更重要,因此沿著光線執行“固定步長”積分將導致收斂性差。如今計算機速度如此之快以至於我們可以通過固定步長來強制執行此計算,但在Nishita那個時候,優化演算法是必要的。因此,Nishita提出了一個模型,其中大氣層用一系列球形殼體表示,在低海拔使用較小間隔,在高海拔使用較長間隔。但請注意,在論文中發表的圖表中,層的厚度似乎非常像按指數形式變化的,這並不令人驚訝,因為每個殼的半徑在Nishita的模型中由空氣分子的密度分佈精確確定。

在遊戲裡模擬天空的顏色,太迷人了!

大氣層由小顆粒(空氣分子)組成,小顆粒也會在低空與較大的顆粒混合,稱為氣溶膠。這些顆粒可以是由風揚起的灰塵或沙子,也可以是因為空氣汙染而存在的任何顆粒。它們肯定會對大氣的外觀產生影響,特別是因為它們不像空氣分子那樣散射光線。空氣分子對光的散射稱為瑞利散射,氣溶膠對光的散射稱為米氏散射。

在遊戲裡模擬天空的顏色,太迷人了!

Figure 2: aerosols present in the atmosphere are responsible for haze.

簡而言之,瑞利散射(空氣分子散射光)是造成天空藍色(以及日出和日落時的紅橙色)的原因,而米氏散射(氣溶膠散射光)通常是造成汙染城市上方白灰色霧霾的原因(陰霾掩蓋了天空的清晰度,見圖2)。如果沒有提到使用者需要指定行星和天空的半徑,模型將是不完整的,這些數字將用於計算沿觀察者和光線方向上樣本位置的高度。對於地球,我們將使用Re = 6360 km(e代表地球)和Ra = 6420 km(a代表大氣層)。該模型中與距離相關的所有距離和引數應在同一單位系統中表示(km、m等)。

在遊戲裡模擬天空的顏色,太迷人了!

Figure 3: a simple graphical representation of the atmospheric model we will be using in this lesson. It is defined by the radius of the planet (Re) and the radius of the atmosphere (Ra). The atmosphere is made of aerosols (mainly present at low altitude) and air molecules. Density of particles decreases exponentially with altitude. This diagram is not to scale.

對於大氣模型,這可能是棘手的,因為行星的大小可能很大。對於這樣的尺寸,公里通常是更合適的選擇。然而,散射係數非常小,並且它們更容易用米或毫米(對於光波長甚至用奈米)來表示。選擇一個好的單位也是因為浮點數精度的限制(也就是說,如果數字太大或太小,計算機對這些數字的浮點表示可能會變得不準確,從而導致計算錯誤)。模型的輸入引數可以用不同的單位表示,並由程式內部轉換。

最後,我們通過指定天空的主要照明來源是太陽這一點來完成我們對模型的描述,太陽離地球太遠,我們可以假設所有到達大氣層的光線都是相互平行的,我們將進一步展示這種假設如何簡化模型的實現。

在遊戲裡模擬天空的顏色,太迷人了!

Figure 4: the sun is so far away from the Earth, that each light ray reaching the Earth's atmosphere can be considered as being parallel to each other.

1.3 瑞利散射

瑞利在19世紀晚期發現了空氣分子對光的散射。他特別指出,這種現象具有強烈的波長依賴性,更準確地說,空氣分子散射的藍光比綠光和紅光更多(ps:更容易散射藍光)。這種形式的散射和他提出的計算由分子(例如我們在大氣中找到的分子)組成的體積的散射係數的方程,僅適用於尺寸遠小於構成可見光的波長的顆粒(粒子應至少比散射波長小十分之一)。可見光的波長在380到780 nm之間變化,440,550和680分別被認為是藍色,綠色和紅色光的波長峰值,我們將在本文的其餘部分使用這些值。瑞利散射方程提供了計算已知分子密度的體積的散射係數。在關於大氣散射的文獻中,散射係數由希臘字母β(beta)表示。

在遊戲裡模擬天空的顏色,太迷人了!

上標S用於標識散射,下標R用於標識瑞利(用於將這些係數與米氏散射係數區分開)。在這個等式中,h是高度,λ是波長,N是海平面的分子密度,n是空氣的折射率,是大氣標高。在我們的模擬中,我們將使用=8km(如果需要,請參見網上的大氣標高以獲得更精確的測量資料)。正如你所看到的,具有短波長的光(例如藍光)的β值比長波長的光(紅色)更高,這解釋了白天天空呈現藍色的原因。當來自太陽的光穿過大氣層時,相比綠色和紅色光,藍色光更多地向觀察者散射。為什麼在日出和日落時會出現紅橙色?在此種情況下(日出、日落時),相比於太陽高於頭部(天頂位置)時,來自太陽的光必須在大氣層中行進更長部分以到達觀察者的位置。那距離相當長,大部分藍光在到達觀察者的位置之前已經散開,而一些不像藍光一樣散射的紅光仍然存在。因此,日出和日落時的天空呈現紅橙色(有時可以是紫色或略帶綠色)。我們可以使用一個N,n的測量值來計算β,但是我們將使用在海平面的散射係數的預計算值(波長440、550和680nm,對應的散射係數分別是: 在遊戲裡模擬天空的顏色,太迷人了!在遊戲裡模擬天空的顏色,太迷人了!在遊戲裡模擬天空的顏色,太迷人了! ,注意藍光的散射係數大於綠光和紅光的散射係數)。通過應用方程的指數部分(右項),我們可以調整任何給定高度h的這些係數。

在遊戲裡模擬天空的顏色,太迷人了!

Nishita的論文和我們在本篇中提到的其他論文的主要問題之一是他們要麼沒有給出N,n等的值來生成論文中顯示的影像。在使用測量資料時,論文的作者通常不會引用它們的來源。在海平面上尋找分子密度的科學資料並不容易,如果你有任何關於此主題的有用連結或資訊,你可以向我們提供,我們希望收到你的來信。我們在本篇中使用的海平面散射係數可以在兩篇論文中找到:"Efficient Rendering of Atmospheric Phenomena", by Riley et al. and "Precomputed Atmospheric Scattering" by Bruneton et al.

我們不會詳細介紹空氣密度,但可以在網際網路上找到有關此問題的有趣資訊。在本篇中,我們真正需要的是海平面的平均散射係數,它們可以很好地重建天空的顏色,但是學習如何計算這些係數將會讓任何想要更好地控制大氣模型的好奇讀者感興趣。

如果你閱讀有關體積渲染的內容,你將會知道用於渲染參與介質的物理模型是散射係數,吸收係數和相函式,它描述光在與粒子碰撞時散射的程度和方向。到目前為止,在本篇中,我們只給出了地球大氣層的散射係數值(並解釋瞭如何計算)。對於大氣散射,通常認為吸收係數可忽略不計,換句話說,我們將假設大氣不吸收光。還記得在體積渲染的內容中,我們在渲染參與介質時使用的物理模型中需要的衰減係數是吸收係數和散射係數的總和,因此(因為吸收係數為零)衰減係數可以表示為:

在遊戲裡模擬天空的顏色,太迷人了!

瑞利相函式看起來像這樣:

在遊戲裡模擬天空的顏色,太迷人了!

其中μ是光與視線方向之間角度的餘弦(見圖8)。

1.4 米氏散射

米氏散射在某種程度上類似於瑞利散射,但適用於尺寸大於散射波長的粒子(比如我們在地球大氣層的低海拔地區發現的氣溶膠)。如果我們將瑞利方程應用於氣溶膠,不會得到令人信服的影像。渲染散射係數的米氏方程如下所示:

在遊戲裡模擬天空的顏色,太迷人了!


下標M標識米氏散射,注意,對於米氏散射存在特定的大氣標高,其通常設定為1.2km。與瑞利散射相反,我們不需要方程來計算米氏散射係數。我們將使用海平面測量值來代替。對於米氏散射,我們將使用: 在遊戲裡模擬天空的顏色,太迷人了! 。氣溶膠的密度也隨著海拔高度呈指數下降,同瑞利散射類似,我們將通過在方程(由大氣標高調製)的右邊包含一個指數項來模擬這種效應。米氏相函式方程是:

在遊戲裡模擬天空的顏色,太迷人了!


米氏相函式包括控制介質各向異性的項g(瑞利相函式不包括),氣溶膠具有很強的前向指向性,一些論文使用g=0.76(我們也將使用這個值作為我們的預設值)。

1.5 光學深度的概念

在實現演算法之前,我們將把所有零散部分放在一起。首先,我們應該注意到,天空只不過是圍繞實心球體的球形體積。因為它只是一個體積,為了渲染天空,我們可以使用光線步進演算法,這是最常用的渲染參與介質的技術。我們在關於體積渲染的課程中詳細研究了該演算法。當我們渲染一個體積時,觀察者(或相機)可以在體積內部或外部。

當相機在體積內時,我們需要找到觀察光線離開體積的點,當它在體積外時,我們需要找到觀察光線進入和離開體積的點。因為天空是一個球體,我們將使用光線與球體相交測試的方式來計算這些點。我們通過使用地面作為參考來測試相機位於大氣層內部或外部。如果此高度大於大氣厚度,則攝像機位於大氣層之外,並且觀察光線可能在兩個地方與大氣層相交。

在遊戲裡模擬天空的顏色,太迷人了!


Figure 6: the camera can either be inside the atmosphere or outside. When it is inside, we are only interested in the intersection point between the camera ray and the atmosphere. When the camera is outside it can intersect the atmosphere in two points.

現在我們假設攝像機在地面上(或高出地面一米),但該演算法適用於任意攝像機位置。讓我們假設我們有一個觀察光線(對應於幀中的一個畫素),我們知道這條光線與大氣層上邊緣相交的位置。我們此時需要解決的問題是找出有多少光線在沿著觀察者的方向上傳播。正如我們在下圖中看到的那樣,我們的相機或觀察者不太可能直視太陽(那樣會傷害你的眼睛)。

在遊戲裡模擬天空的顏色,太迷人了!


Figure 7: single scattering is responsible for the sky color. A viewer is rarely directly looking at the sun (which is dangerous). However when we are looking away from the sun the atmosphere has a color which is the result of blue light from the sunlight being scattered in the direction of the observer's eyes.

如果你朝天空的方向看,你不應該看到任何東西,因為你正在看空曠的空間(天體之間的空間)。然而,事實是你會看到藍色,這是由來自太陽的光進入大氣並被觀察方向上的空氣分子偏轉引起的。換句話說,沿著觀察方向沒有來自太陽的直射光(除非觀察方向直接指向太陽),但是來自太陽的光被大氣散射,一些光線最終沿著你的眼睛方向行進。這種現象稱為單次散射(在體積渲染內容中對此進行了解釋)。

在遊戲裡模擬天空的顏色,太迷人了!


Figure 8: to compute the sky color we first trace a ray from Pc to Pa and then integrate the amount of light coming from the sun (with direction L) that is reflected back along that ray (V).

到目前為止我們知道什麼?我們知道,為了渲染幀中一個畫素對應的天空顏色,我們將首先從點Pc(相機位置)向感興趣的方向上投射觀察光線(V)。我們將計算此光線與大氣相交的點Pa。最後,我們需要計算由於單次散射而沿著該光線傳播的光量。該值可以使用我們在體積渲染內容中研究的體積渲染方程來計算:

在遊戲裡模擬天空的顏色,太迷人了!


其中T是點Pc和X(沿觀察方向的樣本位置)之間的透射率。L是體積中位置X處的光量。它所說的是,在Pc到達觀察者的光總量等於沿著觀察方向V散射的所有太陽光。該量可以通過將沿V的各個位置(X)的光相加來獲得,這種技術稱為數值積分。我們沿著光線採集的樣本越多,結果越好,但計算所需的時間越長。等式中的透射率項(T)解釋了在每個樣本位置(X)處沿觀察者方向V散射的光從X行進到Pc時也會衰減。

回想一下體積渲染的內容,當光線從體積中的一個點移動到另一個點時因吸收和散射,光線會衰減。我們稱Pb點的光量為Lb,從Pb到達Pa點的光量為La。存在參與介質的情況下,La(從Pb接收的光)將低於Lb。透射率表示La與Lb之比,因此T在0到1的範圍內。透射率的方程看起來像這樣:

在遊戲裡模擬天空的顏色,太迷人了!


簡單來說,這個等式意味著我們需要測量沿著路徑光線Pa-Pb在不同樣本位置處的大氣的衰減βe,將這些值加起來,將它們乘以長度段ds(即距離Pa-Pb除以使用的樣本數量) ,取該值的負值並將其提供給指數函式。

在遊戲裡模擬天空的顏色,太迷人了!


Figure 9: as light (Lb) travels from Pb to Pa, it gets attenuated because of out-scattering and absorption. The result is La. Transmittance is the amount of light received at Pa from Pb, after it was attenuated while traveling through the atmosphere (that is T=La/Lb).

回到我們所說的關於瑞利散射的內容,你會記得βs(我們將以此計算βe)可以用海平面上的散射係數來計算,該散射係數由高度h與大氣標高(H)比值的指數函式來計算。

在遊戲裡模擬天空的顏色,太迷人了!


對於瑞利散射,可以忽略吸收,散射係數我們可以寫作:

在遊戲裡模擬天空的顏色,太迷人了!


你可以將βe(0)移出積分(因為它是常數),透射率方程重新寫為:

在遊戲裡模擬天空的顏色,太迷人了!


指數函式內的和可以看作Pa和Pb之間的平均密度值,並且方程本身在文獻中通常稱為點Pa-Pb之間的大氣的光學深度。

1.6 將陽光加起來

最後,我們將使用前一段中介紹的渲染方程來計算天空顏色,讓我們再寫一遍:

在遊戲裡模擬天空的顏色,太迷人了!


我們已經解釋瞭如何計算透射率,現在讓我們看看Lsun(X)這個量,並解釋如何評估它的值。Lsun(X)對應於在樣本位置X處沿觀察方向散射的太陽光量。為了評估它,首先我們需要計算到達X的光量,直接取陽光強度是不夠的,實際上,如果光線在Ps點進入大氣層,它也會在行進到X過程中衰減。因此,我們需要使用與我們用於計算沿著從Pa到Pc的觀察方向傳播的光的衰減的方程相似的方程來計算這種衰減(等式1):

在遊戲裡模擬天空的顏色,太迷人了!

Equation 1


從技術上講,對於沿著觀察射線的每個取樣位置X,我們將需要在太陽光方向(L)上投射光線並找到該光線與大氣相交的位置(Ps)。然後,我們將使用與觀察射線相同的數值積分技術來評估等式1中的透射率(或光學深度)項。將光線切割成段,並評估每個光段中心的大氣密度。構建計算天空顏色的演算法的最後一個要素是根據光線方向本身和視線方向來計算在觀察方向上散射的光量。這是相函式的作用,瑞利散射和米氏散射有各自的相函式,我們在之前提到過它們。如果你需要了解相函式,請閱讀體積渲染相關內容。簡而言之,相函式是描述來自方向L的光在方向V上散射多少的函式。米氏相函式包含一個額外項g,稱為平均餘弦(可能還有其他名稱),它定義光是否主要沿前向(L)或後向(-L)散射。對於前向散射,g在[0:1]範圍內,而後向散射g在[-1:0]範圍內。當g等於零時,光在所有方向上均勻散射,我們說散射是各向同性的。對於米氏散射,我們將g設定為0.76。

在遊戲裡模擬天空的顏色,太迷人了!


最後,我們需要考慮這樣的事實,例如,瑞利散射的藍光主要沿著視線方向散射。為了反映這一點,我們將前一個方程的結果乘以散射係數(βs),這給出了方程的光量部分的完整方程。但是回想一下βs隨高度而變化(它的值是高度的函式),其中h是X相對於地面的高度(更精確地說是海平面):

在遊戲裡模擬天空的顏色,太迷人了!


在遊戲裡模擬天空的顏色,太迷人了!


對於地球大氣而言,太陽是天空中唯一的光源,但是,為了寫出前一個方程的通用形式,我們應該考慮光可能來自多個方向的事實。在科研論文中,你通常會看到這個等式被寫為4π的積分,它描述了傳入方向的範圍。這個更通用的方程也可以考慮了地面反射的陽光(多次散射),我們將在本章中忽略這些多次散射的陽光。

1.7 計算天空顏色

把所有的元素拼接在一起我們得到以下方程(方程2和方程3):

在遊戲裡模擬天空的顏色,太迷人了!

Equation 2



在遊戲裡模擬天空的顏色,太迷人了!

Equation 3


將方程3代入方程2得到以下方程:

在遊戲裡模擬天空的顏色,太迷人了!


相函式的結果以及陽光強度是恆定的,我們可以將這兩項移到積分外:

在遊戲裡模擬天空的顏色,太迷人了!

Equation 4


這是用於渲染特定視角和光照方向的天空顏色的最終方程。因此,它並不複雜,但對它的計算要求更高,因為我們計算積分並對指數函式進行多次呼叫(通過透射率項)。另外,你需要記住它們的天空顏色實際上是瑞利和米氏散射的結果。因此,對於每種散射型別,我們需要計算兩次這個方程:

在遊戲裡模擬天空的顏色,太迷人了!


在微積分中,兩個底數相同的指數函式的乘積可用法則計算,即底數不變指數相加。

在遊戲裡模擬天空的顏色,太迷人了!


我們可以利用這個屬性(我們計算一個指數而不是兩個)並重新編寫方程4中兩個透射項的乘法:

在遊戲裡模擬天空的顏色,太迷人了!

Equation 5


2、大氣散射的GPU編碼實現

在第一部分中已經介紹了相應的理論基礎,在這部分我們將在GPU中實現對天空顏色的模擬。由於原文中給出的程式碼是在CPU中實現的,輸出最終的圖片需要一定時間且不方便除錯預覽,因此我們利用ShaderToy在GPU中實現,方便修改引數並實時預覽最終模擬的效果,最終效果如下視訊所示,可以看到隨著太陽接近地平線,靠近地平線位置的天空呈現紅色和橙色,另外也可在ShaderToy中看到該實現(https://www.shadertoy.com/view/3sSfDz):

在遊戲裡模擬天空的顏色,太迷人了!

Figure 9


下面開始介紹實現部分,首先是座標系的建立,想象一下中午你東西向躺在草坪上,太陽在天空正上方,你戴著墨鏡直視太陽,以視線直視方向為y軸正方向,雙臂展開從左手到右手的方向為x軸正方向,從你的腳底到頭頂的方向為z軸正方向,座標軸的原點在地心,以此建立一個左手座標系。在此座標系下平躺著朝著正上方用魚眼相機每隔一定時間拍攝一次照片,至太陽落山為止,最終將影像序列按建立時間順序壓縮成視訊,便得到和如上視訊中類似的效果。為了模擬魚眼效果,需要將單位正方形內的點集對映到單位半球面(不是嚴格的半球面,有一部分點在半球面以下,關於平面點集到半球面點集的對映,可參考《Ray Tracing from the Ground Up》中有關內容)上,然後從相機位置處向半球面上的這些點發射射線計算每個點上最終的顏色值。

在ShaderToy中,螢幕從左到右為x正向,從下到上為y正向,uv的x和y分量取值範圍為「0,1」,但螢幕的長寬不同,螢幕長度大於寬度,這意味著同一把尺子分別與x和y軸平行時投影得到的度量不同,x值會更大些,為了消除這種不一致,需要將uv值的y分量乘以螢幕的長寬比,然後利用球面座標系的角度和座標的轉換公式得到單位正方形內的點對應的單位半球面上的點,viewScale是為了對面畫整體進行縮放,類似於FOV的作用。

  1. vec2 uv =(2.*fragCoord/iResolution.xy1.)*vec2(iResolution.x/iResolution.y,1.);
  2. float viewScale = 1.;
  3. vec2 p = uv*viewScale;
複製程式碼

注意螢幕的xy平面相當於上面建立的座標系的xz平面,因此我們可以做如下對映:

  1. <blockquote>//單位正方形內的點對映到半球面上
複製程式碼

我們將相機位置放置在(0.,earthRadius+1.0,0.)處,將螢幕座標中單位正方形內的點對映到新建立的座標系中單位半球面上的點,然後從相機位置處向半球面上各個已對映的點發射射線。由於模擬的是正午到日落的動態效果,需要更新太陽的位置,假設太陽從正午到日落沿著我們建立的座標軸的x軸旋轉了90度,則可以對太陽光的方向做如下更新:

  1. //sun position
  2. float rotSpeedFactor = 3.;
  3. mat3 sunRot = rotate_around_x(-abs(sin(u_time/rotSpeedFactor))*90.);
  4. sunDir *= sunRot;
複製程式碼

沿x軸旋轉的變換矩陣可由如下輔助函式得到:

  1. //繞x軸的旋轉矩陣
  2. mat3 rotate_around_x(const in float degrees)
  3. {
  4.     float angle = radians(degrees);
  5.     float _sin = sin(angle);
  6.     float _cos = cos(angle);
  7.     return mat3(1.,0.,0.,
  8.                 0.,_cos,-_sin,
  9.                 0.,_sin,_cos);
  10. }
複製程式碼

接著我們定義一些巨集和常量:

  1. #define PI 3.14159265359
  2. #define USE_HENYEY 1
  3. #define u_res iResolution
  4. #define u_time iTime
  5. vec3 sunDir = vec3(0.,1.,0.);
  6. float sunPower = 20.;
  7. const float earthRadius = 6360e3;
  8. const float atmosphereRadius = 6420e3;
  9. const int numSamples = 16;
  10. const int numSamplesLight = 8;//光線與大氣層頂端交點到取樣點的取樣數量
  11. const float hR = 7994.;//rayleigh
  12. const float hM = 1200.;//mie
  13. const vec3 betaR = vec3(5.5e-6,13.0e-6,22.4e-6);//rayleigh
  14. const vec3 betaM = vec3(21e-6);//mie
複製程式碼

然後定義射線和球的struct以及射線與球相交檢測以及計算相交點的輔助函式:

  1. <blockquote>struct ray
複製程式碼

另外瑞利和米氏以及schlick相函式的定義如下:

  1. //----------------phase function----------------
  2. float rayleighPhaseFunc(float mu)
  3. {    return 3.*(1.+mu*mu)/(16.*PI);
  4. }
  5. const float g = 0.76;
  6. float henyeyGreensteinPhaseFunc(float mu)
  7. {
  8.     return (1.-g*g)/((4.*PI)*pow(1.+g*g-2.*g*mu,1.5));
  9. }
  10. const float k = 1.55*g-0.55*(g*g*g);
  11. float schlickPhaseFunc(float mu)
  12. {
  13.     return (1.-k*k)/(4.*PI*(1.+k*mu)*(1.+k*mu));
  14. }
  15. //----------------phase function----------------
複製程式碼

相函式定義為粒子在各個方向上的實際散射能,與粒子散射為各向同性時該方向的散射能之比,輸出是一個無量綱的值。可以通過desmos上的henyeyGreensteinPhaseFunction的影像來配合理解。

在遊戲裡模擬天空的顏色,太迷人了!


由於理論中提到最終的顏色值是由積分公式計算得到的,積分割槽間是一個連續的區間,因此只能通過離散取樣的方式計算累加和得到近似結果。

  1. //計算光學深度
  2. bool getSunLight(const in ray r,inout float opticalDepthR,inout float opticalDepthM)
  3. {
  4.   float t0,t1;
  5.     raySphereIntersect(r,atmosphere,t0,t1);
  6.     float marchPos = 0.;
  7.     float marchStep = t1/float(numSamplesLIght);
  8.     for(int i = 0;i<numSamplesLIght;i++)
  9.     {
  10.         //相鄰兩個取樣點的中點
  11.         vec3 s =r.origin+r.direction*(marchPos+0.5*marchStep);
  12.         float height = length(s)-earthRadius;
  13.         if(height<0.)
  14.             return false;
  15.         opticalDepthR += exp(-height/hR)*marchStep;
  16.         opticalDepthM += exp(-height/hM)*marchStep;
  17.         marchPos += marchStep;
  18.     }
  19.     return true;
  20. }
  21. vec3 getIncidentLight(const in ray r)
  22. {
  23.     float t0,t1;
  24.     if(!raySphereIntersect(r,atmosphere,t0,t1))
  25.     {
  26.       return vec3(0.);
  27.     }
  28.     float marchStep = t1/float(numSamples);
  29.     float mu = dot(r.direction,sunDir);
  30.     float phaseR = rayleighPhaseFunc(mu);
  31.     float phaseM =
  32. #if USE_HENYEY
  33.     henyeyGreensteinPhaseFunc(mu);
  34. #else
  35.     schlickPhaseFunc(mu);
  36. #endif
  37.     float opticalDepthR = 0.;
  38.     float opticalDepthM = 0.;
  39.    
  40.     vec3 sumR = vec3(0.);
  41.     vec3 sumM = vec3(0.);
  42.     float marchPos = 0.;
  43.     for(int i=0;i<numSamples;i++)
  44.     {
  45.         vec3 s = r.origin+r.direction*(marchPos+0.5*marchStep);
  46.         float height = length(s)-earthRadius;
  47.         //計算光學深度累加和
  48.         float hr = exp(-height/hR)*marchStep;
  49.         float hm = exp(-height/hM)*marchStep;
  50.         opticalDepthR += hr;
  51.         opticalDepthM += hm;
  52.         
  53.         ray lightRay = ray(s,sunDir);
  54.         float opticalDepthLightR = 0.;
  55.         float opticalDepthLightM = 0.;
  56.         bool bOverGround = getSunLight(lightRay,opticalDepthLightR,opticalDepthLightM);
  57.         if(bOverGround)
  58.         {
  59.             vec3 t = betaR*(opticalDepthR+opticalDepthLightR)+
  60.                 betaM*1.1*(opticalDepthM+opticalDepthLightM);
  61.             vec3 attenuation = exp(-t);
  62.             sumR += hr*attenuation;
  63.             sumM += hm*attenuation;
  64.         }
  65.         marchPos += marchStep;
  66.     }
  67.     return sunPower*(sumR*phaseR*betaR+sumM*phaseM*betaM);
  68. }
複製程式碼

通過調整瑞利散射係數或米氏散射係數,可以得到一些alien planet atmosphere的效果,如下所示:

在遊戲裡模擬天空的顏色,太迷人了!


此外,如果從相機位置處向成像平面發射射線計算著色,可得到如下渲染效果,比較接近unity中預設場景的天空顏色:

在遊戲裡模擬天空的顏色,太迷人了!


大氣渲染相關的技術點很多,本文僅實現了單次散射,對於多次散射、相函式推導、優化等相關主題未有涉及,在實時渲染中還有其他簡化模型以及各種trick來渲染天空。

相關文章