GTA5:一幀精美的畫面是如何渲染的(二)

GTA5:一幀精美的畫面是如何渲染的(二)

GTA5是一款渲染和優化都十分出色的遊戲。本文將基於Adrian的GTA V - Graphics Study的內容繼續上一篇文章

DevidXu:GTA5:一幀精美的畫面是如何渲染的??

zhuanlan.zhihu.com圖標

的分析。這一篇里自己補充了很多技術細節,讀者需要一定的圖形學基礎去理解其中的概念。如有錯誤希望不吝指正!


Step 4. 改善皮膚的渲染質量

在延遲渲染模式下,利用像素著色器合成G-Buffer中的貼圖後,圖像已經初步成型。但延遲渲染也有相應的缺點:由於它統一地存儲所有像素點的數據(Normal,Specular等),一些材質的特性不得不捨棄。再加上G-buffer僅利用一個PS來合併貼圖數據,就無法針對性地渲染特殊材質(另一種渲染思路延遲照明開闢了一張light buffer存儲了光照屬性,可以相應地改善光照質量,參見Siggraph09的Light Pre-pass)。

上述缺點導致渲染出的皮膚有種布料的感覺。但我們知道皮膚通常會「白裡透紅」:因為皮膚具有一定通透性,光線射入皮膚之後會在皮膚內部多次折射,再從表面射出。所以我們需要對皮膚單獨計算」次表面散射「的顏色(Subsurface scattering,SSS)。

我們首先需要找出哪些像素點對應著皮膚。上篇文章提到:模板貼圖中以0x89值標記主人公佔據的像素,而輻照貼圖的alpha通道則用binary mask標記皮膚的像素點為1。這樣就能找出需要處理的點了(被衣服覆蓋的皮膚不需要處理,在與depth比較後被標為0)。

利用G-Buffer信息找出需要SSS處理的像素點

接下來就是SSS的顏色計算。SSS有多種計算方法,最精確的是基於物理的渲染方法:

對於皮膚上一點,基於物理的次表面散射渲染(BSSRDF)會計算出

  1. 從當前點射入的所有光通量中 被反射出去的光通量
  2. 從另一處皮膚射入,在皮膚內部多次折射後從該點射出射出的光通量

兩者之和,這種方法具有很高的精度但代價也很高昂,並不適合實時演算。因此發明了很多tricks型的渲染方式:比如事先利用面部形狀結構,算出各頂點的梯度從而做出一張light map,然後渲染時對light map進行混合模糊並覆蓋到臉上(對那些頭髮蓋住半張臉的人物應該很適用吧)。

個人猜想GTA5通過在shadow map中做深度比較去計算SSS的顏色(原理更近似於透射)。之所以這樣猜想,因為 I. 比較兩張圖發現,在陽光方向深度變化小的部位(如鼻子,嘴唇)周圍明顯變亮; II. 夜間在很亮的地方也無法察覺到面部的SSS現象。這種方法實現原理也比較簡單:對於皮膚所在像素點,算出其在太陽光方向上的深度,與shadow map中的深度相減(由於GTA5採用CSM,近處的shadow map精度可以到毫米),從而估算出陽光從該點射出時經過的距離 s 。然後對該點加上一個與 s 負相關的顏色(光線傳播距離越小越容易透射出去)。

對於陽光透射深度較小的區域,顏色變化更加顯著

可能有人覺得這個處理不值一提,但其實我們玩遊戲時注意力常集中在NPC的面部,因此面部渲染的小小提升都能提高沉浸感。GTA5對所有NPC都做了SSS處理,雖然代價較高,但卻是完全值得的。

利用遊戲截圖重建麥克面部模型

這裡推薦一個在線的根據照片人臉建模的項目網頁,你可以重建麥克的面部模型,自己嘗試實現SSS。當然你也可以用各種自拍照試試效果。(網頁載入3D模型速度較慢,耐心等候即可)


Step 5. 渲染屏幕中的水

GTA5中每一幀都會進行水面的渲染。在這一幀中幾乎看不到水,其實是因為後面的海洋和城裡的水池都被建築給掩蓋住了。大型遊戲里的水都會計算反射和折射,但同樣是水,海水和池水的渲染卻存在很大的差別。

GTA5中精緻的水面

模型處理:首先水池網格中頂點規模較小,可以直接將處理後的網格傳入頂點著色器進行渲染;而海洋面積十分寬廣,每幀渲染一個近似無限大小的海洋網格會帶來極大的性能浪費。通常做法是:每幀內根據當前視錐體生成一個海洋的有限網格,從而恰好填滿視錐體;同時海洋的遠處不需要很高的精度,因此遠處會用更簡易的網格模型進行替代(當精度減到0時可以整體近似成有限網格)。

海洋網格隨著視角更新,近處具有更高的網格精度

另外,兩者通常會用不同的函數去模擬頂點的運動,原文沒有提到技術實現,我就稍微列舉一二:Tessendorf提出利用快速傅里葉變換(FFT)計算海面網格,效果十分逼真且原理推導嚴謹,但需要設置很多參數並且計算複雜;Yuksel提出利用隨機運動的粒子去模擬水面頂點,實現簡單而且效果令人滿意,被應用於神秘海域系列。

累積足夠多的隨機運動粒子模擬水面(引用自頑皮狗)

網格確定後,會利用MRT(multiple render targets)技術將每種水都同時渲染到兩張貼圖上,它們分別是diffuse map和opaque map。

Diffuse Map:存儲了不同種類的水的基本顏色(海水顏色比水池中的水更深更藍)

Opaque Map:信息儲存在了Red Channel和Green Channel。

Red Channel:該通道儲存了各類型水的透明屬性,比如海洋為0.109,水池為0.129;視錐 體內所有的水(無論是否通過深度測試)都會渲染該通道。

Green Channel:儲存了水的像素點在世界空間內離水面的深度 d (當你低于海平面或潛入水下時就會很有用)。淺層的水顏色接近透明,而深層的水著色時diffuse map存儲的本徵顏色佔有更大的權重。計算 d 需要先將水的像素點映射回模型空間,才能根據對應水面的高度算出與水面的距離。

由於變換回模型空間需要計算逆矩陣,代價較高,因此GTA5做了一個小優化:先基於對數深度緩衝做出一張一半解析度的深度貼圖,計算 d 之前先採樣比較深度,只有通過深度測試的點才會繼續計算。從下圖可以看出,只有少量綠色部分進行了 d 的計算。這也減輕了後續著色的計算量。

海面與水池基本顏色不同 針對可見區域計算水深

順帶一提:以前做某劍五的優化時,希望對部分粒子特效只做半屏渲染從而降低GPU Bound,結果發現DX9竟然無法從Z Buffer生成Depth Map,只好新開一張一半解析度的紋理記錄深度,但這也導致粒子的深度精度太低,沒法正確顯示。DX10及以上的API都支持從Z-Buffer獲取Depth Map。

渲染水還需要最後兩張貼圖(Reflection Map已經在第一步獲得):

I. 折射貼圖(Refraction Map):渲染的水面其實僅僅是場景中蓋上去的一層「紙」,並不會自動表現任何物理現象。想要正確渲染水面下的物體(即位於水的像素後面的物體),就必須基於水面形狀重新計算這些點對應的位置。好比渲染一根插入水中的筷子,我們只需要將水面所覆蓋的部分根據折射定律進行位置的偏移即可。

II. 凹凸貼圖(Bump Map):一般通過採樣這張貼圖來修正每個頂點的法線,從而影響亮度的計算結果,增強水面的細節表現。也可以設置係數來控制法線的偏移程度,決定水面是否平滑。注意:網格用於控制水面的整體形狀,而凹凸貼圖用於模擬細節表現。

用於計算水面的三張貼圖(引用自Adrian)

原文使用的場景渲染的水很少,因此難以體現最終的渲染效果。這裡我換了一個場景,並標出了折射與反射明顯體現的部分,凹凸貼圖用於整張水面的渲染。

三種貼圖是如何在水面上共同作用的


Step 6. 大氣的渲染

美麗夕陽下的洛聖都令人印象深刻。第三人稱遊戲的視野中天空占很大的比重,也直接影響場景的質量,因此GTA5在大氣的渲染上做了很多的功夫與細節,這一步我也將重點介紹。我將原文中大氣的渲染進行總結歸類,進一步分為近地環境的渲染天空的渲染

近地環境的渲染:霧(Fog) 和 體積陰影(Volumetric Shadows)

:霧的基本原理是根據距離給原本的像素點混合越來越深的白色。原文沒有提到實現細節,但絕不是簡單地線性映射:GTA5中經常能看到一些真實的霧蒙蒙的效果,需要加入高級特性才能實現(即將提到的體積陰影)。同時加入霧效也能夠掩蓋遠處的建築模型精度的不足。

加入霧效的前後對比(引用自Adrian)

體積陰影:首先理解霧的形成才能明白這步操作的原理。霧是空氣中懸浮的水滴,因為反射太陽光(各種顏色疊加成的白色)而顯出白色。相反易知:如果沒有陽光直接照射,那麼霧將會變暗。這就是體積陰影的原理:在霧的顏色計算完之後,體積陰影會去計算哪些像素點是未被陽光直接照射到的,並將這部分霧色彩調暗。這樣形成的霧效就是近似正確的。另外遠處的像素點一般會被霧均勻覆蓋,因此往往不被進一步計算。為了簡化計算,這一步使用了一半解析度並且對結果進行了模糊處理。

貼圖上半部分的白色對應遠處的大片建築,無需變暗處理

注意這裡有個很小的細節:在確定未照射到的像素點時,程序並非從Depth Map採樣獲取深度,而是用ray marching從攝像機發出光線和場景求交獲取深度 d ,然後跟太陽的shadow map(具有高精度)進行深度比較。

Ray marching不同於普通的ray tracing。這裡我對ray marching裡面的一個經典演算法Sphere Tracing稍作解釋:假設A為光線出發點,找出最小的 r 使得A為中心的圓與形狀相切;然後光線從A點沿射出方向移動 r 到點B。設B為新的光線出發點重複上述過程,直到 r 小於某個定值,說明ray已經marching到了離場景足夠近的位置。這樣就可以求出與場景的近似交點(深度)。

Ray Marching的相關演算法

之所以用ray marching採樣深度(如圖中 r_1+r_2 )是因為:ray marching可以沿著光的方向均勻前進(上圖左邊),從而計算每個點上陽光的內散射和外散射量並累積下來。考慮體積陰影的原理(陽光對霧的作用),這種基於物理的計算著色準確,可以表現出霧蒙蒙的效果。

附註------Ray Marching效率問題:遊戲一般會用各種場景結構來加速光線相交計算(如BVH,KD樹等)。GTA5利用GPU對這些結構進一步加速(周昆教授論文中利用GPU實時構建KD樹),因此執行效率較高。上一篇也提到過GTA5利用GPU中的compute shader進行視錐裁剪的加速。GPU正在逐漸轉化成更通用的計算管線。

到這裡就結束了近地環境的渲染,下面介紹天空的渲染。

天空的渲染:天穹貼圖,雲朵,太陽

我們首先看看GTA5同一場景中天空的漸變效果,然後仔細分析每一部分是如何實現的。

天穹:天穹的渲染原理就是將一張貼圖貼在半球上,然後後將半球頂罩在攝像機上方進行渲染(現在很少用立方體天空盒,質量很差),這樣可以保證天空與人相對靜止。但這張貼圖的計算重要而複雜,因為它需要模擬一天不同時刻的天空色彩。下面介紹天穹貼圖是如何計算的:

天穹的圓頂結構(引用自Adrian)

首先需要理解為何一天中天空會有不同的色彩:24小時中太陽與我們所在處的大氣層夾角不斷變化,不同的夾角允許不同色光透過,因此一天內會呈現不同的顏色。而同一時刻內天空不同處也會呈現不同的色彩,是因為大氣層具有一定厚度,太陽穿過大氣層射入眼中時不斷發生內散射和外散射,導致光線不斷衰減,因此不同層次大氣顏色不同。

不同的角度與距離決定了大氣顏色的豐富變化

因此,大氣真實感渲染往往利用光線傳播原理進行推導,詳情可以參考Precomputed Atmosphere Scattering(Eric08)這篇論文。該論文中基於物理的大氣渲染公式可以寫為

L(x,v,s)=(L_0+R[L]+S[L])(x,v,s)

這裡 L 定義為陽光方向為 s 時,從 v 方向射到 x 處的光通量。 L_0 是陽光的直接光照, RS 分別是反射光和散射光。散射光計算較為複雜:這裡既有從當前光束散射出去而損失的光(out-scattering),也有從周圍射入光束的光(in-scattering)。有了公式我們沿著光路進行積分,便能得到接近真實的效果,這裡涉及到很多公式就不予解釋了。遊戲中不會實時計算積分,而是先預計算一系列貼圖;實時渲染時進行採樣計算,就能大大簡化計算量,渲染出美麗的夕陽景色。

Eric論文的大氣渲染效果

天穹貼圖還使用了Berlin Noise進行修正,使得貼圖更具真實感。這是因為自然界中的噪音有一定規律,而計算機計算出的噪音往往是隨機生成的。Berlin Noise會基於公式生成一些更接近自然規律的noise貼圖(比如石頭上的紋理),從而表現出更接近現實的細節。

Perlin Noise:更「規則」的噪音

雲朵:渲染的方式跟天穹類似,只不過這次換成渲染一個雲「環」。這裡通過將貼圖首尾銜接,既可以做出無縫渲染的效果,又可以設置環的轉動來模擬雲朵的運動。渲染時用到了兩張2048*512的貼圖:法線貼圖和密度貼圖。密度貼圖可以更好地和後面的場景進行混合(比如說太陽),展示出雲被照亮的效果(觀察場景中被太陽照亮的雲朵)。

雲朵的密度貼圖和法線貼圖(引用自Adrian)

太陽:畫一個圓形的餅就OK了?遠非如此!很多和太陽直接相關的光學現象,也被細心的GTA5應用到了場景中。GTA5通過渲染光紋(light streaks)透鏡耀斑(lens flares),大大增強了場景的逼真程度。這兩個其實都是由陽光的反射和折射導致的光學現象,並且被很多電影鏡頭用來增強畫面表現力。光紋是太陽沿某些特定方向射出的光線,透鏡耀斑則是光紋方向上形成的亮斑。下圖可以直觀地體現出這兩者各是怎樣的形式:

這種特效渲染起來給人很強的鏡頭感。但遊戲中並非通過物理模擬去計算,而是通過以下兩種人工的trick實現:

I. 基於照片處理:當光源(包括太陽)渲染結束後,提取出照片最亮的區域並進行複製與變形等操作,這種方法對任意數量的光源都可以適用(比如夜間的車燈)

II. 基於sprite紋理處理:在渲染出的圖片中光源周圍添加sprite紋理並調整它們的位置,達到最好的渲染效果;這種方法需要對每個光源單獨處理,但它能更渲染更精確的特效。

實際上GTA5在繪製太陽時同時運用了這兩種方法,以下圖場景為例:GTA5在圖像通過bright-pass緩衝時加了一層層的藍色光圈(即基於照片的處理);然後圖像中最顯著的則是基於sprite紋理的處理,它只被用於太陽的渲染:首先在太陽周圍繪製十二個長方形sprite來渲染Light Streaks的效果(見下圖太陽周圍射出的十二條光線),然後又沿著太陽與屏幕中心的連線畫了約70個sprite(見下圖網格中重疊的小方塊)。這中間並沒有涉及複雜的物理計算,雖然是障眼法但卻取得了很棒的渲染效果。

直接渲染的太陽 (引用自Adrian) 添加人工處理後

絢麗效果後的樸實渲染方式(引用自Adrian)

你會發現上圖的渲染網格中light streaks和lens flares的貼圖正好對應於以下sprite

渲染太陽利用的sprite(引用自Adrian)

GTA5還處理了其他一些精緻的細節:

I. 這些光斑的大小是和攝像機的aperture成正比的(不同的aperture光斑大小差別明顯)

II. 當你切換到第一人稱視角時是看不到這些光學現象的,因為這時是通過人眼觀察,而非通過第三人稱的攝像機進行觀察。


Step 7. UI

在上一步完成後,GTA5將開始繪製它的UI(這裡主要講解左下角的小地圖)。這裡的小地圖只是標出了重要的道路和商店,它跟主要流程中的渲染並沒有聯繫(顯然我們沒有必要為了小地圖而去渲染整個場景)。這裡為了提高效率,GTA5將地圖分成了數片方塊。在渲染時,它會首先提取出位於人物附近的可能出現的方塊(如下圖所示)。每個方塊只用一個drawcall畫出。這就避免了大量不必要的地圖渲染。

可能被繪製的方塊都會被調用(引用Adrian)

然而我們只需要左下角的部分,可以利用一個裁剪測試去掉不必要的部分。然後再加上一些商店,家,各種建築的圖標,最終和原來的buffer結合在一起即可。

GTA5的UI地圖

GTA5用向量的形式去存儲每條道路,因此可以支持很大範圍的縮放而不失真,但存儲和渲染也更為麻煩(畢竟道路的形狀較為複雜)。這裡推薦讀一篇Valve的渲染UI的很棒的文章,它利用了基於distance field的方法,不需要提供向量形式的圖片,只要一張合適大小的distance field貼圖就可以渲染出各種大小的高精度UI。這裡稍作解釋:distance指的是當前像素點到最近的輪廓線的像素距離 d ,輪廓內的點 d 設為正值,輪廓外的點 d 設為負值。然後把所有的 d 都映射到0~1之間。 容易知道:當 d<0.5 時,點在輪廓外;當 d=0.5 時,點在輪廓上,當 d>0.5 時,點在輪廓內。因此我們在渲染時把閥值設為 0.5 就可以渲染出高精度的UI。可以用smoothstep函數將 d 映射到更小的範圍 (Dist_{min}, Dist_{max}) 以減少鋸齒。

引用自Horde3D

這一幀的渲染到這裡基本已經大功告成啦!當然還有很多後處理和細節無法全部顧及。回顧整個渲染流程,這一幀一共調用了4155個draw call,1113張紋理和88個渲染對象,特別是其中很多細節處理和優化,使得GTA5這款畫面出色的龐大遊戲能在配置較低的電腦也能流暢運行(我筆記本上的GTA5已經達到82G了)。

這就是加入UI之後最終的渲染效果:

添加完UI後的最終結果(引用自Adrian)


寫到這裡原文也介紹的差不多了,其中加入了很多個人理解與補充技術,希望能對大家有所幫助。這裡特別感謝我在Nvidia實習時的導師張帆,他指正了上一篇的一些錯誤並提出了寶貴的修改意見。

最近正值繁忙的畢業季,即將踏上前往加州的旅途,但還是抽出不少時間翻譯這篇文章,一方面是因為自己很喜歡這款遊戲,另一方面國內與國外的渲染引擎技術還有著不容忽視的差距,因此希望多多借鑒取長補短。看了今年的E3感觸很深,衷心希望未來某一天,國內能做出一款像GTA5這樣的遊戲!

推薦閱讀:

EDG.M賽後群訪如何?
《魔獸世界》這款遊戲的開發有哪些趣事?
遊戲版號
關卡設計與《合金裝備5幻痛》
仙劍奇俠傳6第三章第4關怎麼通關?

TAG:遊戲 | 計算機圖形學 | GrandTheftAutoVGTA5 | 3D渲染 |