2022N.GAME網易游戲開發者峰會于「4月18日-4月21日」舉辦,本屆峰會圍繞全新主題“未來已來 The Future Is Now”,共設置創意趨勢場、技術驅動場、藝術打磨場以及價值探索場四個場次,邀請了20位海內外重磅嘉賓共享行業研發經驗、前沿研究成果和未來發展趨勢。
今天的干貨來自技術驅動場的嘉賓周潛,他是網易游戲大話事業部高級技術經理。
以下是嘉賓分享實錄:(部分刪減與調整)

大家好,我是來自網易游戲大話事業部的技術專家周潛,今天邀請大家跟我一起走進極速光影的世界,來把我們在高品質賽車游戲的一個光照方案分享給大家。
首先來介紹一下我們的游戲產品,名字叫Racing Master,中文名“巔峰極速”。我們的目標是去打造一個高品質、高擬真、高畫質的一個賽車游戲。

游戲中有非常豐富的場景以及鮮艷的色彩,賽車的物理是完全寫實的,漂移也是用真實的物理去計算的。
玩家可以對車輛進行非常豐富的自定義,包括改裝、涂裝等等。在大廳界面有車輛展示系統,車輛的建模是非常精致的,并且車燈、車漆這些內容都是還原度非常高的,在夜景下也有十分寫實的全局光照。
通過以上介紹,相信大家對我們游戲的美術品質有了一個大概的概念。為了達到這樣的美術品質,我們面臨的最大一個難點,就是“實時全局光照”。
實時全局光照分為兩個部分:直接光照和間接光照。為了在移動端實現這一目標,我們需要面臨非常多的挑戰,包括:寬帶、性能、以及兼容性等等。此外,還有著許多束縛,比如說我們需要用Forward管線來減少寬帶,畢竟我們還要減少DP,以及減少Pass等等。
那么,直接光照和間接光照在游戲里延伸出來兩個需要解決的問題,就是“實時多光源”和“實時環境捕捉”,我們今天的話題就從這兩個問題開始。

一、實時多光源
“多光源”在游戲里是非常常見的,比如在夜景下,會有許多路燈、車燈以及車輛的回火等等,它們會照亮周圍的物體。

這個是我們賽車在經過一排路燈下的表現,除了路燈之外,車輛還有一個前置燈。這么多的動態光源在Forward管線下是難以實現的。
因此,許多人對Forward管線進行了改進,以此來支持多光源。

首先是屏幕空間的Tile Shading,又稱為Forward+,它的思路是把屏幕空間劃分為多個格子,然后每個格子配合深度Buffer去進行燈光求交計算。這樣我們就能知道每一個格子里會有哪些燈光對其有影響,從而減少每個像素需要計算的燈光數量。
它雖然能夠解決多光源的問題,但需要一個預繪制深度的Buffer,即一個PreZ Pass。但如果當游戲場景非常豐富時,PreZ Pass便會帶來非常多的Draw Call,這對于我們來說是不能接受的。
此外,Tile Shading的求交計算必須得在Compute Shader里面進行,可對于許多手機而言,關于Compute Shader的支持效果并不友好。
另外還有一種方案是Clustered Shading,它的思路是在Forward+的基礎之上,對深度空間進一步地劃分,將視錐體劃分為多個視錐體分塊,之后再對每個分塊進行燈光求交計算。

但同樣的,Clustered Shading也需要一個PreZ Pass,并且它的求交計算也需要放在Compute Shader當中去進行。
為了在移動端去解決這些問題,我們提出了一種新的多光源方案,叫做“Grid Shading”。
Grid Shading的思路是在世界空間上,沿著XY軸方向進行齊軸的對齊網格劃分;然后采用一張燈光索引圖,其中每個像素代表一個格子且包含該格子內所受到的光源編號。
每個像素用RGBA四個通道,可以記錄四盞燈光。若燈光數目超出了四盞,則需要對這些燈光進行貢獻度排序。

貢獻度,即燈光的光照強度。在排序之后只保留貢獻度最大的四盞燈光于這個像素當中。而燈光信息則通過UniformBuffer上傳。
此外,我們可以在Z軸方向進行多層擴展,這樣就可以達到覆蓋范圍更廣的目的。
通過Grid Shading在場景中的應用可以看到,場景里產生了一個XY方向上64×64的網格,并且我們在攝像機水平高度方向上,向上向下各擴展兩層。因此產生的總網格大小是64x64x4。

但由于網格是扁平形狀的,所以它只能覆蓋較為水平的范圍,并且使用這種方案時,玩家的視野范圍也必須受到限制,得在水平方向上的視野。

可在賽車游戲里,賽道基本是平鋪的,且玩家的視野基本處于水平方向,所以需要照亮的場景物體,包括賽車、路面等等都正好在覆蓋范圍之內。因而,產生的網格基本上能滿足我們的需求。
Grid Shading的流程如下。

首先需要對燈光進行遍歷,根據燈光的包圍盒計算出格子的范圍,然后在CPU層對每個格子進行求交計算。
接著計算出光照貢獻度并進行排序,將結果填充到燈光索引圖當中,并上傳燈光信息。
最后,在繪制階段,每個像素根據在GPU世界坐標的位置,計算出它所在的格子。并且在燈光索引圖中,查找出它所需要計算的燈光編號,計算燈光的光照。
在這當中最難的一個點是格子的求交計算。每個格子的形狀是立方體,可以把它近似成一個包圍球。如果對方是點光源,則燈光范圍恰好也是球體,球體與球體之間的求交計算非常簡單。
但如果對方是聚光燈,聚光燈的范圍是圓錐體,那么該如何做圓錐體與球體的求交計算呢?通常情況下,我們一般是提取出圓錐的包圍球,然后將該包圍球與格子的包圍球進行求交計算。
雖然這種比較簡單,但結果非常不精確,因為圓錐包圍球的大小與圓錐本身的大小差異非常大。為此,就需要一種更精確計算圓錐和包圍球的方法。思路如下。
首先,將圓錐的頂點置于一個大球的球心,則圓錐體的范圍可以看作是該大球的一個球面角內。然后根據大球和格子包圍球的位置關系,可以將求交情況劃分為4種。

第一種情況非常簡單,即格子包圍球包含了圓錐體頂點,根據這種情況可以很清楚地看到包圍球與圓錐體相交。
第二種情況是包圍球與圓錐體大球相互分離,那么根據這種情況,則可以看到格子包圍球與圓錐不可能相交。
剩下兩種情況較為復雜。第三種情況是,格子包圍球有不超過一半的體積在大球內部。那么此時就需要將相交的部分轉化成一個球面角,接著用該球面角與圓錐體的球面角進行相交判斷。
而第四種情況是,格子包圍球有過半的體積在大球內部。那么我們可以用大球的球心,與格子包圍球球面所構成的切線方向,來形成一個球面角。然后用該球面角與圓錐體的球面角進行相交判斷。
通過以上四種情況的討論,便可以很精確地判斷出聚光燈和格子的求交情況了。
接下來,是Grid Shading在實際場景當中的應用。
右邊這張圖,是在比賽場景里放置的一個路燈,以及車輛本身的前置燈二者所構成的光照情況;左邊這張圖是關于該場景的燈光索引圖。

怎么看這張索引圖呢?通過觀察發現,燈光索引圖從上往下分為4層,對應到網格當中便是4層不同高度的網格。
另外,每個像素是一個格子。
如果,該像素里有顏色,則代表這個格子受到了燈光影響。從索引圖中可看到,出現的藍色區域是聚光燈所覆蓋到的范圍。該范圍從上往下是逐漸增大的,那么也就對應了聚光燈上小下大的圓錐體形狀。證明索引圖的結果與場景完全對應。
因此,我們在實際繪制時,就可以采樣這張索引圖來判斷像素所對應的燈光到底是哪些了。
最后,來比較一下Grid Shading和另外兩種方案的區別。
首先在PreZ Pass階段,對于Grid Shading而言是完全不需要該階段的,而另外兩種方案對該階段則無法避免。這能夠為我們節省很多Draw Call和Pass。
在求交計算方面,Grid Shading完全可以放在CPU層面去進行,并且計算過程非常簡單,所得到的圓錐體相交結果也非常精確。但另外兩種方案不僅無法放在CPU層進行計算,且計算過程比較復雜。在面對聚光燈時,計算結果也不夠精確。
從劃分顆粒度方面來看,Grid Shading是一個能夠劃分得非常精細的方案,Clustered Shading由于在Z軸上有更進一步的劃分,因此相對來說也比較精細。但Tile Shading它是一個屏幕劃分,所以劃分的顆粒度非常粗。

所以可看到在以上幾個方向上,Grid Shading非常有優勢,并且在移動端上它的性能非常可以接受。就算在終端機上,求交計算過程中的開銷也不超過1ms。
Grid Shading的問題在于,它需要一個平鋪的水平視野范圍。但對于賽車游戲而言,玩家的視野范圍也正好是平鋪的且處于水平方向。所以,這個限制對于我們來說并沒有太大影響。
因此,Grid Shading可以說是一個非常適用于賽車游戲的多光源方案。
二、實時環境捕捉
首先來看一個效果演示。

在演示場景中有非常多的高亮物體,比如煙花。那么可以看到,在夜晚或者燈光比較暗的場景下,這種高亮物體對場景的影響甚至比直接光照所帶來的影響更大一點。
反應在演示中就是,賽車和路面是可以被煙花這種物體所照亮的,而且賽車同時還會受到路面以及周圍物體反彈的間接光影響。那么為了達成這種環境的光照效果,最重要的一步就是實時環境捕捉。
在移動端通常采用的是一種叫做“雙拋物面映射”的方案。
它的思路是,將360°的環境通過兩個拋物面映射到上和下兩個方向上,通過兩張圖來表達整個場景的信息。

右邊這張圖就是我們在游戲里面捕捉到的兩張雙拋物面貼圖。賽車和賽道都必須要去采樣這兩張貼圖來獲取環境信息。

為什么要采用這種雙拋物面的映射方案呢?以下是我們對于幾種不同環境捕捉方案的比較,相信通過比較便能得出結果。
一般來說全場景捕捉有三種方案,分別是“球面映射”、“立方體映射”和“雙拋物面映射”。
在渲染目標數方面看,三者渲染的目標分別是1張、6張、2張。渲染數目越多代表需要更多的Pass以及Draw Call。
在畸變層面來說,球面映射會有一個非常大的畸變,且越是在邊緣處畸變越大;立方體映射完全不存在畸變,雙拋物面映射雖然也有畸變,但比較小,是可以接受的。

從映射質量來說,球面映射在邊緣處的映射質量非常差,并且有奇點;立方體映射的信息量最大,所以映射質量是最高的;而雙拋物面映射的映射質量一般,但在移動端仍可以接受。
從計算復雜度上看,我們需要對頂點做映射變換。因此球面映射的映射變換最為復雜,因為它需要用到開方運算;而立方體映射由于只是一個簡單的透視映射,所以相對簡單;同樣,雙拋物面映射的映射變換也比較簡單。
那么,綜合來看,雙拋物面映射是非常適合用于移動端的環境捕捉方案。
在捕捉方向的選擇上,我們可以選擇前后捕捉、左右捕捉或者上下捕捉。如果選擇前后捕捉或者左右捕捉這種方案,由于場景是平鋪的,因而賽道在這種劃分方向上會出現三角面的裁剪。最后在合成環境圖時就會有裂縫,這對我們來說是不可以接受的。

如果采用上下方向去捕捉呢?雖然也有三角面的裁剪,但裁剪的位置非常遠,玩家很難注意到。最后合成出來的環境光照也非常完整。
除了對捕捉方向的選擇外,還需要對捕捉點的位置進行選擇。

首先來看一張圖,左邊這個是濕滑路面的反射效果。大家可以看到這個路面反射有什么問題嗎?很容易注意到的是,路面反射與實際場景的位置是不對應的。再來看這張圖,右邊這張圖看起來就好多了。兩張圖為什么會有這樣的區別呢?
我們將相機位置給大家展示一下,左邊這張圖我們可以看到,朝前的相機是游戲視角相機,朝上和朝下的相機是環境捕捉相機。捕捉相機和游戲視角相機并不在同一個位置上,這就導致了畫面中位置不對齊的現象。

我們可以看到右上角的示意圖。當我們的捕捉點與相機在垂直方向上不一致時,它們在對于用一個反射方向,所捕捉到信息在橫向上是有差異的。如果捕捉點與相機在同一個垂直方向上,那么捕捉到的信息只會在縱向上有一定差異,但在橫向上是對齊的。

也可以看到左邊這張圖,雖然在縱向上有差異,但玩家很難注意到。可如果在橫向上有差異,玩家就會非常容易觀察到這個現象。
不過,這又帶來一個新的問題,如果想要保證車輛的反射正確,就必須將捕捉相機的位置放在車的附近。但在游戲中,游戲視角相機與車輛本身的位置是有一定差異的。
所以我們沒辦法保證地面反射與車輛反射處于同時精準的狀態,二者只能選其一。可是對于玩家而言,很難注意到車輛反射的不準確性,而是更容易注意到地面反射的不準確性。

因此我們會將捕捉相機與游戲視角相機放在同一位置,這樣來確保地面反射的準確性。
在捕捉完場景之后,需要在IBL里面采樣這兩個捕捉內容來生成環境貼圖。但IBL有一個要求,即在粗糙度比較高的情況下,它需要對環境貼圖進行濾波。

原本我們直接對雙拋物面的捕捉結果進行動態生成Mipmap,來近似這個濾波之后的環境貼圖。但這樣會帶來一個問題,如下圖。

圖中是一個帶有粗糙度的球體,它的上下半球之間有明顯的分界線,這是怎么回事呢?
這是因為在捕捉時,朝上的半球受到的光照強度比較高,朝下的半球光照比較弱。在濾波時Mipmap只能對一張貼圖進行Mipmap,沒辦法對整個環境進行混合。因此就帶來了上下半球亮度不統一的現象。
怎樣解決這個問題呢?我們又回想到了球面映射方案。因為球面映射是一整張貼圖,所以對它生成Mipmap時,可以對全場進行濾波。于是,可以把雙拋物面捕捉到的結果通過球面映射合成到一張貼圖上。

此外,為了減少紋理的綁定及采樣,還可以把雙拋物面的兩個捕捉內容分別放在同一個紋理的不同Mip上,這樣就能減少一部分開銷。

接下來,我們來看一下雙拋物面捕捉的流程。
首先,為了減少Pass和Draw Call,會把上下半球的捕捉分成兩個階段進行。一幀捕捉上半球,一幀捕捉下半球,兩幀交替進行。這樣每一幀只需采樣一個半球便可。

采樣到環境之后,再把它用球面映射的方式合成到一張貼圖上,接著再生成Mipmap。最后繪制階段,把它應用到場景像素的繪制中。
那么,在室外場景下看,這樣的表現是非常好的。但是,當我們把車開進隧道之后,又出現了一個新的問題。
如右邊這張圖,這是一輛白色的918,但在進入隧道后,它就變成一輛黑色的車了。這是為什么呢?原因是我們在隧道中捕捉的場景非常暗,它缺少靜態光信息。


這時,就需要去獲取靜態光信息。那靜態光信息到底存放在哪里呢?它存在于我們的光照探針里,所以就需要從光照探針里獲取更多的信息來進行渲染。
一般來說,間接光分為Diffuse和Specular兩個部分。Diffuse是在游戲里通過求些系數的光照探針來表示的。它是一個預烘焙后的包含靜態光的信息。而Specular則是實時捕捉的內容。

在粗糙度變大的時候,需要把Specular向Diffuse的輻照度去靠攏。那么如何實現呢?UE4其實已經做了類似的流程,但那只是針對靜態的一個方案。我們將這套流程進行了改進來適用于動態捕捉。
計算分為兩階段。首先在Vertex階段,需要對這個球面映射的內容采取它最高級的Mips,并且將這個像素的內容點乘(0.3,0.59,0.11)。這是RGB各個通道的亮度權重值。

通過上面就得可以到環境的平均亮度。在Pixel階段,用Diffuse輻照度除以平均亮度,并用粗糙度在1.0到剛剛計算出來的這個數值之間做插值。然后將這個插值乘以Specular,這樣就能讓Specular的亮度進行提高,達到跟環境一致的效果。

那么這樣看來,在隧道中汽車原本的顏色也能回來了,并且它的效果也跟周圍的環境比較統一。這就是全局光照的一個表現。

在講完了實時環境捕捉以及實時多光源后,把這兩者結合起來,看看在游戲里的實際效果。
三、總結
最后,我們來做一個技術的展望。
我們這套方案可以用于實時環境光照變化比較劇烈的場景,對于光源高頻變化具有較好的適應性。除了賽車游戲之外,還可以用于不同游戲類型,比如說MMORPG、FPS等等。

未來,我們還將會把這套方案延展到大世界以及晝夜變換和天氣系統中。另外還計劃玩家自定義賽道,這就意味著需要去實時捕捉環境間接光Diffuse的光照,這也是我們目前正在研究的一個方向。
這就是我演講的全部內容,謝謝大家!
N.GAME是由網易互娛學習發展舉辦的一年一度行業交流盛事,至今已成功舉辦七屆。本屆主題為“未來已來The Future is Now”,邀請了20位海內外重磅嘉賓、高校學者匯聚一堂,共享行業研發經驗、前沿研究成果和未來發展趨勢。
點擊“閱讀原文”查看更多分享!