前言
在上一篇中,作者以相對簡短(其實也挺充實)的方式帶我們回顧了Nubis體積雲系統的前置技術路線,及其優缺點。
簡單概括來說,本身對於遠處觀看的和近處觀看的體積雲,渲染上自然有著不同的設計——在上篇中分別翻譯為垂直輪廓法和封包法的兩種建模方式,其數據源格式、採樣方式、是否支持光照、是否支持演進(可以理解為通過數學推算的動畫,例如隨風移動)等維度上都有區別。而且舊的兩種雲建模方式在製作上都是相對不友好的,不容易直接在製作階段預覽到結果。
這次的內容就會正式展開Nubis3系統中的體積雲系統,或者說是體素雲系統——上一篇中作者提到了,這一技術在最終要在成品遊戲的DLC中交付給玩家是有一定挑戰的,這次讓我們一起看看這個方案落地的設計和實現。
文章還是以翻譯原文的講稿為主,並且由於原文的篇幅很多地方較長,會進行適當的精簡和概括。由於圖文內容量都很大,這次分為了上中下三篇——這是其中的中篇;打星號的部分則是我個人的補充說明。
1 體素雲建模——Voxel Clouds Modeling
經歷了短短幾個月的快節奏開發後,我們得以圓滿完成了這一任務——沒有計劃上的延誤。
這裡開始介紹我們為沉浸式(immersive)的實時體積雲準備的解決方案。
回顧一下我們的光線步進(ray-march)的過程:
- 我們在(從攝像機)開始追蹤時並不確定具體雲的位置。
- 當命中雲層時我們開始採樣雲的密度。
- 直接光能量計算——需要一個(讓人懊惱的,annoyingly)昂貴的第二條ray-march射線朝向光源。
- 之後計算環境光的能量(*基於概率估算)。
- 最終需要累加次一級的光源的能量(*基於概率估算)。
- 之後我們對於每一個單位步長中都重複這一步驟,直到密度達到不透明(或出了雲層)才停止march過程。
None of these operations are terribly efficient or were designed to work efficiently with voxels, but our approach to voxel cloud rendering replaces and optimizes almost every one of them with faster better voxel–based methodologies. Let’s start this overhaul at the beginning.
這些步驟都不是特別高性能、或是面向高效採樣體素而設計,但我們的體積雲渲染方案替換和優化了其中幾乎每個步驟,使之變成更快的基於體素的方法。讓我們從頭開始介紹這次大型的改造。
開發一個高效的渲染與光照解決方案離不開穩固的建模方法,因為3A級實時圖形技術中,你需要做(很多)“如何做”以及“消耗多少內存”的決策,(這些決策)可能會提升性能也可能影響性能。
回顧我們的2.5D雲方案的密度採樣,其中從2D建模數據中構建了3D雲的體積——在提高其分辨率之前。
在一個基於體素的方法中,整個昂貴的操作可以被移到shader外,但這意味著需要能開發一種建模雲的方式——存儲並高效地通過採樣器來訪問它們。
There are many tedious and unrealistic ways to model voxel clouds by hand but the most promising approach that I found the most success with in the past was using fluid simulation to “grow” clouds.
儘管也有許多乏味而不現實的手工(建模體積雲)的方式,但以過去的經驗我探索出的最靠譜的方式是——通過流體模擬器來“生成”雲。
在我們2014年未最終使用(un-shelving)的流體模擬實驗的基礎上進行“人造雲層”,似乎會是一個不錯的開始,但我們需要一種能將流體模擬數據導入體積雲的方法。(*原文用了一個生造詞,“frankenclouding”)
We developed a set of authoring tools in Houdini, that we call Atlas. At its heart, Atlas is a compositing pipeline for voxel data with various ways to generate or source and manipulate the data.
我們在Houdini中開發了一組資源加工工具——它被我們稱為Atlas。在其內核中,Atlas是一組能將體素數據以多種方式進行生成、存儲(作為來源)及操作的工具鏈組合。
例如,我們的一個大氣環境藝術家Bryan Adams,從遊戲中的長頸獸模型建立了一個體積雲(如圖)。
One of the things we learned early on was that multiple voxel grid boxes bounded us both figuratively and literally. Keeping them in memory and switching between them in the inner loop of a ray-march was not ideal and lead to slow performance.
其中我們很快總結出的一件經驗就是——多重體素格對我們來說是一種限制(無論從比喻意義上還是字面意思上)。將其保留在內存中並在一次ray-march過程中(在其中)切換,本身就是不理想並會導致低性能的。
*這裡作者又用了bounded這個詞來做了一次雙關,感覺又是一種特有的冷笑話了(上篇裡也有過)。而且這個限制並不是被體積邊界限制了,更多還是(同一空間裡)多重體積這個數據設計導致的。
因此,下一步的思路就是當整個雲的形體被構造後,我們將每立方米(every cubic meter)的雲的密度信息寫入一個密度體素柵格(dense voxel grid)中。
這將解決建模雲過程的第一個挑戰,並將2.5D的雲採樣中相對開銷大的部分替換掉——如果不是引入了使人崩潰的幾個問題的話...
第一個問題是我們要面對的是天量的體素柵格,這肯定會有內存瓶頸。
遊戲的DLC區域面積有4平方公里,而我們需要大約500米垂直高度的空間來放置雲層——從地面到空中。由於玩家可能飛躍雲層,因此我們也需要足夠的體素柵格的精度以保存細節信息。
即使放低期望並進行估算,也需要2048x2048x256尺寸的體素柵格以保證每2平方米的精度——玩家的飛行坐騎的翼展就有大約2米,因此這已經是比較極限的數據估計了。
這個密度的柵格需要很多內存,並且會因此導致更長的ray-march時間——因為潛在需要消耗更大的內存帶寬。之前的經驗告訴我們這樣不可行。
也有一些稀疏的體素格式,能僅在有物體的區域內存儲一些(相對)更高精度的數據,但需要一些間接定位(indirection)的步驟,這會增大每次採樣的開銷。對於已經有相當大開銷的ray-march來說這是很致命的——並且由於時間也很有限,我們決定擱置這種方案。
我們決定從BC4壓縮格式,8米的精度開始。(*就是暫時不考慮稀疏體素這個方式了,另外這裡8米應該是長寬,高度應該還是1米)
讓我們來總結一下體積雲建模的技術路徑:
- 我們通過Houdini中一款稱為Aero雲解算模擬器來生成雲。
- 之後我們修改並將其整合到我們的“人造雲層”中。
Then store them in obscenely low resolution voxel grids to be sampled at render time. It sounds like its hardly a solution to our goals, but…
- 最終我們可恥地將密度數據以較低分辨率存儲在體素柵格中,並在渲染時對其進行採樣。聽起來這差不多也算是一種解決方案,然而...
*最後一句作者用了很多作為技術分享不太常見的語氣詞,比如obscenely和sounds like、hardly,可見到這一步勉強實現的效果確實不太讓人滿意。obscenely這個詞直譯比“可恥”可能還嚴重一點,也比較奇怪,原意我這裡就不寫了,感興趣可以去查一下。
*體素階段精度的不足,後續作者團隊通過利用節省出的性能空間進行了視覺效果精度上的彌補。
2 採樣密度——Sampling Density
Past experience had taught us that balancing work between memory accessing and instructions in the compute shader can yield better performance on the GPU, so we chose to solve this in the density sampler itself as we have done in the past.
過去的經驗教育了我們,在compute shader中平衡內存的使用和指令的調用能獲得GPU端更更好的性能表現,因此我們在解決這個採樣密度的問題時也參照了這一思路。
回顧一下之前(非體積採樣)的方案:
- 將2D的NDF模型數據解壓成空間輪廓。
- 之後“超精度”這一空間輪廓——通過以高頻噪聲侵蝕的方式。
問題是——當採樣體素數據時,這一超精度方法還能生效麼?答案是肯定的。
比起明確定義出每個體素的(高分辨率的)密度,我們的方法是從一個低清晰度的基於體素的空間輪廓進行“超精度”——使用一個新的細節噪聲。
這使我們得以規避內存瓶頸——通過分流了一部分工作至超精度指令的方式,正如之前在2.5D雲中做的那樣。
*這裡還沒有具體描述細節噪聲的結構,但給出了體素塊的尺寸和形狀。
這裡用對比圖簡單做一個預覽:上方的圖是初始的空間輪廓渲染後的形態,下方的圖是超精度之後的結果。
空間輪廓橫截面(Cross Section)
The Dimensional Profile is generated from a signed-distance field of the cloud to ensure that we get a gradient from outside to inside.
空間輪廓生成自一個有向距離場(縮寫成SDF),以確保能得到一個從外到內的梯度。
我們的Atlas工具允許用戶建模(被稱為)Nubis體素數據場的數據,縮寫成NVDF。作為對空間輪廓數據的補充,製作者能建立額外的NVDF,例如:
- 細節類型(Detail Type),用來混合從纖細到波浪狀的噪聲細節。
- 密度縮放(Density Scale),在噪聲應用於超精度時適當降低密度。
這些數據使用BC6格式壓縮(*之前的文章介紹過),支持3個通道——每個紋素(texel)佔用1字節。
下面讓我們看看體素雲採樣函數在超精度過程中的具體實現,以及新的3D噪聲的一些特色。
我們的密度採樣器中的第一步是:採樣空間輪廓NVDF數據。
之所以第一步執行它,因為如果結果為0,則可以跳過後續關於雲採樣的步驟;如果空間輪廓是非0值,則我們將雲模型數據傳入超精度函數。
在深入超精度函數前,我們需要先來看看新的3D雲細節噪聲。
讓我們看看這組實景定時拍攝(*原文中是較長一段視頻)——雖然當時如果用了三腳架(Tripod)會使拍攝更穩定,不過目前也足以展示重點了。大家都看過類似的鏡頭,然而為了更好理解流體的特性,最好從不同的視角來觀察它。
尤其是上下顛倒(觀察)時,它看起來更像潑入水中的牛奶。從這個視角更容易想象水蒸氣(water vapor)所受的壓力和雲整體受的擠壓。這一點是我們建模細節噪聲時所重視的細節。
*這裡作者採用流體類比的思路來觀察,讓人覺得既吃驚又合理。
回顧一下,我們將雲的細節分類為了波浪狀的和纖細的。
當水蒸氣進入一個冷空氣區域時,它會更容易(原文是effectively)地向外被擠出。
The billows that we see are the result of some of that expanding vapor punching through weak spots in this squeezing force.
我們看到的波浪狀效果是一些擴散的蒸氣,在這類擠壓力下穿透稀薄的部分後的結果。
現在,在腦中倒裝這一過程,就能得到一個更纖細的荷葉邊(scalloped)的形體。
這一過程發生在水蒸氣蒸發,同時周圍的空氣擠壓蒸氣時。
這裡展示了另一組延時拍攝——雲稀薄得就像網一樣(*原視頻後面雲逐步消散了)。
此時,空氣的擾動就起到了更決定性的作用,為雲的形體帶來了額外的扭曲。
小結一下:
- 在密度降低處,表現為多層的纖細細節和捲曲(curly)的擾動。
- 在密度增加處,表現為多層波浪狀的視覺。
在之前的方法中,我們使用反相的Worley噪聲來生成波浪狀的細節,但我們不得不重複採樣很多層以獲得類似雲的效果——而不是堆疊的球體。
這次我們決定使用Houdini中的Alligator噪聲(如圖),既有著和之前的噪聲相似的結構,又有著更像雲的不均勻的空隙(lacunarity)。
對於纖細的細節,之前我們使用了Perlin-Worley混合噪聲——不過,這對於超精度生成的過程來說還不夠細緻。
作為替代,我們從一個反相的alligator噪聲開始,並使用一個捲曲噪聲將其扭曲成需要的形態——這被我們稱為Curly-Alligator噪聲。(*Alligator直譯是短吻鱷,這裡屬於專有指代)
我們生成的這些3D紋理是4通道128x128x128的體素。
前兩個通道分別存儲低頻和高頻的Curly-Alligator噪聲,而後兩個通道則是低頻和高頻的Alligator噪聲——講座的最後會介紹一些輔助產生這些3D噪聲的工具。
我也樂於分享一個生成Alligator噪聲的工具的源碼(*圖中鏈接),它是由SideFX Software提供的。
瞭解了這些3D噪聲的具體格式後,我們可以回到超精度部分的具體實現了。
第一步是(類似2.5D雲中一樣)使我們的細節噪聲捲動——我們通過添加一個“風偏移量”來改變採樣位置,以實現這一效果。(*類似UV動畫,只不過是三維的)
下一步我們依據MIP級別(根據攝像機距離計算)來採樣3D噪聲。這能提升大約15%的性能,並且一個合適的MIP級別並不會導致結果上可察覺的影響。
由於採樣器會被頻繁調用,因此能省一點是一點(原文是every little bit helps)。
下面讓我們看看如何定義纖細和波浪狀的細節噪聲(在超精度函數內部)。
首先,讓我們看看纖細雲的照片。
注意在密度沒那麼高的部分,有一些非常高頻的細節;而在更緻密的區域有著低頻的纖細結構。我們想要在超精度的過程中模仿這種關係。
回顧我們的空間輪廓NVDF,可以看到它已經提供了一個從外到內的梯度。
*雖然箭頭是向外的,但原文描述的方向確實是from the outside to the inside;箭頭更多可能是表達受擠壓擴散的方向。
為了模仿這一點,我們可以簡單地基於空間輪廓來對低頻和高頻纖細噪聲做混合。
*lerp函數就是線性插值,看多了的應該都不陌生。
*三張圖依次是:只有高頻、只有低頻、混合噪聲的結果。
下面來看一些波浪狀雲層的照片。
注意到在靠近內核的部分有著低頻的波浪狀形體,而靠近表面則有著高頻的波浪狀形體。
對於波浪狀形體,我們希望在邊緣(相對核心)採樣更多的球狀結構。同樣的我們也是基於空間輪廓來混合低頻和高頻的噪聲。
*同樣,三張圖依次是:只有高頻、只有低頻、混合噪聲的結果。
之後我們基於類型數據(type data),在採樣位置對兩種噪聲(的混合結果)再做混合。
*三張圖從上到下依次是:只有纖細噪聲、只有波浪噪聲、兩者基於類型數據混合。
這在遠距離時運作良好,但仍然無法符合我們想要飛躍雲層的目標——在近處仍相當缺乏細節。因此,我們開發了一種重用高頻噪聲的方式,以在必要時添加一些更高頻的噪聲,同時不再進行額外的高開銷的噪聲採樣。
我們創建了一種被稱為雙摺疊噪聲的算法,以獲得兩種類型各自對應的更高頻噪聲。
*具體公式如圖,基礎思想就是展開到-1和1的範圍後,又通過取絕對值abs“摺疊”,之後pow取多次方。
之後我們基於細節類型數據對兩種類型的更高頻噪聲進行混合,最後再基於到攝像機的距離和之前(遠距離情況下)的噪聲雲層進行混合。
*上面兩張圖展示了:未增加細節和增加了細節後的不同結果。
這裡展示了飛躍雲層的過程中這些細節噪聲是如何生效的——並且可以看出這一效果是無縫切換的。(*因為都是連續插值的)
現在我們明白了噪聲的混合及運作過程,讓我們將其整合到函數中:
- 首先,我們使用一個重映射(remap)函數來侵蝕雲的空間輪廓。
- 之後我們將超精度密度(up-rezed density)乘上密度縮放。
- 最後有一個使結果顯得更銳利的方式是通過pow函數——更多地在低密度區域進行這項處理能使這部分提高清晰度。
- 之後我們將其作為超精度的結果返回。
*解說稿沒有完全覆蓋公式的每一步,但是參照各處的命名一致性應該比較清晰了。
總結一下:
從結果來說,這套實現是一種能從地面到空中近距離的無縫體積雲渲染方案,它有著簡化後的採樣函數,既迴避(sidesteps)了高內存使用同時又能提供足夠高的細節度。
我們的空間輪廓能提供相對低精度的每8米一樣本的精度,通過一些超精度方法能提升至(相當於)0.5米的精度。
解決方案的關鍵是平衡內存(從1米降至8米)和指令調用(引入一些噪聲採樣)來獲得相對更好的性能。
不過我們還遠沒有結束這一改造(overhaul)過程,因為在最差的情況下,960x540像素的渲染會消耗10毫秒——而在地面觀看時,我們需要和其它的一些資源一起分配性能預算。從影視特效或宣傳動畫的角度來說或許足夠了,但作為一個實時渲染的遊戲來說不能達到3A標準。
*最後一句及後面有一個bonus頁,作者展示了一些在緻密柵格採樣(dense grid sampling)方面的探索。不過截至這一段落為止作者並沒有明確後續是否要應用進一步提升精度的方案。
3 光照計算——Lighting
光照,讓人哭笑不得的是(ironically)——它也是體積ray-march步驟中開銷最重的部分之一。在它的面前我們的任務似乎是不可完成的:既需要節省大量的性能,又要提高質量以支持飛過雲層時的效果。
回顧我們之前的介紹,我們把光照分解為直接散射(Direct Scattering)、環境散射(Ambient Scattering)和次級光源(Secondary Sources)三部分。
其中直接散射的計算開銷非常大,因為需要向光源發射第二級的ray-march採樣射線(*而且是逐採樣點的)——我們也嘗試了各種可能來加速或替換第二級的ray-march採樣。
直到我們以體素的角度來考慮這一問題,之前有一個看起來比較高傲(loftier)的點子就顯得有些合理了。
主程(Principal Tech Programmer)Nathan Vos一直想將光采樣射線與視線採樣射線中解耦,不過直到使用了體素方案後,改動的複雜度才降到了合適的範圍。
在我們的新方案中:
- 前2次採樣的方法沿用之前的方案。
- 但剩餘的採樣在一個單獨的pass中被預計算,並存儲於256x256x32尺寸的體素柵格中。
這有助於保持雲的近處表面的細節,並提供一種更彌散(diffuse)的視覺效果。需要明確的是,預計算是發生在ray-march的步驟之外的。
Here is how it works: If a voxel has cloud in it, then a ray is marched between that voxel and the boundaries of the voxel grid and density is accumulated at each step.
這裡展示了它(預計算的體素柵格)運作的方式:如果一個體素中有云的數據,則向光源發出一根射線,直到達到定義的邊界,而過程中的每一步都對密度做累加。(*這裡density因為是採樣的光照,可以理解成通透程度。但後面還是簡化翻譯成密度。)
我們發現這大約減少了40%的渲染時間。這項操作自身消耗在0.1-0.2毫秒之間,取決於在一天裡的時間以及射線到太陽的距離。這一開銷是平攤到8幀之後的結果。
作為節省性能之後的補充,我們終於能渲染遠距離雲內陰影了。
這裡(上面的圖)是之前的方案中256米,10條光采樣射線的結果。
下面的圖則是基於體素的光照方案的結果。這是一次難得的場合,基於性能的優化得出了更好的效果。(*下面的圖雖然看著更黑了,看似少了一些細節,但是明暗關係更“對”了一些)
切換到體素方案也為我們帶來了一些自由,以固化(solidify)我們建模多重散射效果的方式——對應雲的內發光到暗邊緣的效果。讓我們深入看看這一實現。
讓我們看看休斯頓的Natural History博物館展出的水晶樣本。
It’s interesting because you can peer deeply into the core through the little gaps between each crystal. If you blur your eyes, you can almost see what looks like a lump of lighter material under the surface.
有趣的是你能從水晶針的微小縫隙中觀察到其內核部分——如果你讓視覺稍微模糊點,你就能把它近似看作(暗色)表面下的一小塊發光材質。
很難不觀察到其中的相似點。在雲的表面邊界處,有著更少的材質(*這裡指實際的水蒸氣、冰晶等)——使入射光線更容易散射到隨機的方向,因而不容易進入人眼。
因而你能想象雲內部有這樣一個概率場——朝向觀察者的內散射,隨著深度越深就越是各向同性(isotropic)或全方向的(omnidirectional)。這種透射向表面的光照很像隔著一層厚棉花的閃光燈。
或是像水晶體結構由內而外發出的光。
從結果上觀察這種散射的潛在規律,能更有助於我們建模實時散射效果。
*從自然界的實際效果觀察,是這個作者很重要的一種思路。雖然實際上設計這種概率場是一種近似,但視覺上又很“真實”。
幸運的是,我們已經有了這種體積描述。讓我們以圖中的云為例。
下方的圖展示了它的空間輪廓——讓我們把這作為(散射)概率場的基礎。
先讓我們將這片雲對齊到視線和太陽的中間。
然後取這個角度中心位置的一個切片作為樣本。
這裡是切片從初始角度進行觀察的效果。
這裡展示了我們使用空間輪廓作為散射參數的效果。為了模擬光散射被雲體吸收的情況,我們額外應用了一個beer-lambert衰減曲線。(*上圖是無衰減的,下圖是模擬了衰減的)
這一曲線值在靠近太陽和雲的內部的位置更大,以模擬前述的內部光散射效果。
讓我們看看實際交付版的遊戲中雲光照的效果。
*上圖是沒考慮多重散射估計的,下圖是應用了這一算法的。
*這是太陽在觀察者背後的情況。上圖是沒有多重散射估計的,下圖是應用了這一算法的。
就像在2.5D雲中的那樣,我們也使用空間輪廓作為估算環境光散射的概率場。
*上圖是沒有計算環境光貢獻的,下圖是計算了的。
由於我們(基於體素的)預計算光照採樣的方案運轉不錯,我們終於考慮改進環境光照的計算方式,從概率估計變成採樣累加。
當我們以射線採樣預計算天空光照時,我們獲得了能局部調整環境光強度的參數,使得天空被其它雲遮擋的區域會計算更少的光強度。
在計算時,我們就直接乘上這一累加後的密度係數(*負值的exp,見圖中公式)。
*上圖展示了無方向性(舊算法)的環境散射效果,下圖展示了基於體素光的方向性環境光散射。
而燈光之類的次級光源我們還是基於概率場來估算。
首先,我們定義了一個基於球體的潛在能量範圍,並乘上一個模擬出的非均勻(non-homogeneous)密度係數。
這裡展示了遊戲中的一處效果。
由於這是一次大改造,因此確保雲的渲染在一日循環中的始終有效是很重要的。這裡讓我們執行一個循環以進行觀察。
那些遠距離雲上的陰影也極大提升了觀感。
最後總結一下:
- 我們解耦了光線的ray march過程,預計算並存儲在一個體素柵格中的累加密度結果集合中。
- 通過這一過程(相對原來)減少了每幀40%的開銷,我們把這些性能空間用來添加遠距離雲的陰影。
- 我們也使用這一方案來改進了環境光的效果。
- 額外地,我們的一些光照估算方式被簡化成直接查找(體素)空間輪廓。
有時就是這樣,改良一項效果會開啟其它解決方案的門。
結語
存儲空間和指令調用(或算法複雜度)的平衡是作者提出的一項很有實踐智慧的視角。在給定的需求——飛入雲層作為大前提下,體素數據的使用就不可避免。在此基礎上,雲的建模從體素的結構和性能指標開始考慮,並在後續的密度採樣和光照計算儘量發揮體素(對畫面)帶來的優勢來改進渲染管線。
另一個很重要的視角就是——從物理光學中觀察。畢竟這些方案無論顯得如何“真”,其實中間不乏估算與trick;但很多時候,使用概率場或概率密度來計算光照或採樣等,很類似數學擬合後的結果——其結果首先是要有合理的曲線,其次就是不會發生能量憑空增加的情況。只有做到了這一點,在實現全天候光照的時候才能有相對準確的結果。
這一篇讀到的這三部分應該也算是Nubis3體積雲系統的核心,下週更新的剩餘的部分會包含一些系統整合方面的介紹。
最後是資料鏈接:
Nubis3: Methods (and madness) to model and render immersive real-time voxel-based cloud 1080P PPTX PDF