這里分享的是Unreal在Siggraph 2015上關(guān)于SDF的相關(guān)技術(shù),原始PPT在參考文獻[1]中給出。
分享的作者是UE的圖形程序Daniel Wright,到2015年為止,他在Epic待了9年,專注于lighting & rendering相關(guān)的技術(shù),此前參與過Gears of War系列。
先來提出問題,在一個可以動態(tài)變化的場景而言,在如下的一些要求下,目前并沒有一套比較好的Occlusion(遮擋光效,比如AO/Shadow)方案:
- 精準(zhǔn)、柔軟
- 支持area shadows,sky occlusion, reflection shadowing
一個有效的解決策略是使用Ray Marching Signed Distance Fields。
受Inigo Quilez使用SDF+Raymarching生成漂亮效果(如下圖)以及Alex Evans 2006的Fast Approximations for Global Illumination on Dynamic Scenes文章激發(fā),這里嘗試使用SDF來解決上述的問題。

在開始計數(shù)方案介紹之前,我們先來看下此前SDF的應(yīng)用方式。

如上圖所示,UE3中層嘗試將SDF用在反射陰影(地面反射效果上的陰影)的計算上面,這里沒有介紹其中的實現(xiàn)細(xì)節(jié),后面有機會再補上。
這個方案對場景是有要求的:
- 場景需要是static的,需要單個volume texture(整個場景一張還是每個物件一張?)來存儲distance field數(shù)據(jù)
- 性能消耗較高,只有高端硬件才支持。

SDF的另一項應(yīng)用是用于計算天光遮罩(Occlusion)效果,同樣沒有給出實現(xiàn)細(xì)節(jié),后續(xù)有機會再補充。
大概的實現(xiàn)方式是,為屏幕空間的每個像素朝著某個方向(比如bent normal之類)以某個cone angle(bent normal計算時附帶輸出的未圍擋區(qū)域的范圍計算得來)進行cone march,以cone march的結(jié)果作為天光的illumination效果,這種方案的弊端在于:
- 需要27個cones(每個像素?)
- 即使在非常小的場景中依然只能得到2fps的幀率……

GDC 2015 Kite Tech Demo給出了SDF的另一項應(yīng)用,計算植被的Occlusion效果,同樣只支持高端硬件。

相對于上述的一些應(yīng)用場景的高昂的性能消耗,Epic目前在Fortnite上使用的方案則更為親民:
- 支持動態(tài)TOD
- 支持高動態(tài)場景光照效果
- 在PS4級別的硬件上都能有流暢的體驗
接下來進入正文,整個技術(shù)介紹分成五個部分,下面一一介紹。
1. SDF簡介

SDF的中文翻譯叫做有向距離場,這里的場描述的是3D空間中的數(shù)值分布,數(shù)值是距離,是到物件/場景的最小距離,而有向則指的是距離是帶符號的,當(dāng)某個點處于物件內(nèi)部,這時的距離是小于0的,否則是大于等于0的。
SDF的數(shù)據(jù)有很多存儲方式,一個常用的方式是使用volume texture,即3D貼圖,貼圖中的每個像素對應(yīng)于所覆蓋范圍中的一個點的SDF數(shù)值,其他未落在像素中心的點則可以通過對其他像素數(shù)據(jù)進行插值得到。

SDF的一個常見應(yīng)用是陰影計算,某個點是否被陰影覆蓋,只需要從這個點向著光源方向發(fā)射射線,判斷射線是否會跟場景相交即可,而如果沒有SDF,在沿著射線進行ray marching的時候,就需要以一個較小的step逐點步進判定,這個消耗是非常高的。
有了SDF之后,由于我們可以知道任意點的SDF,那么相當(dāng)于以這個數(shù)值作為marching step長度是完全可以保證不會穿到物體內(nèi)部的,也不會出現(xiàn)step過大某個遮擋體被跳過的可能(當(dāng)然,這要求場景的SDF表達的精度足夠高),因此可以對這個計算過程進行加速,這種算法有個術(shù)語叫做sphere tracing(如上圖中右邊的小圖所示)。

通過前面的sphere tracing,我們可以很容易的直到某點是否處于陰影中,這個過程得到的是硬陰影,要想得到軟影效果,還需要一些額外的處理:
- 根據(jù)當(dāng)前像素到遮擋體的距離來對陰影進行軟化(除以某個與距離(或距離的平方)成正比的數(shù)值)
- 在sphere tracing的過程中,當(dāng)前射線與場景相交的最小cone,如上圖中右邊小圖所示,以這個cone的角度與一個預(yù)設(shè)的全亮?xí)r的cone的角度(這個角度跟光源形狀有關(guān),可以用作面光源軟影的求取方式)作比,結(jié)果用作陰影的軟化程度。

跟直接用voxel來進行場景或者物件表達相比,SDF有如下的一些優(yōu)勢:
- 可以表示表面的朝向
- 可以根據(jù)數(shù)據(jù)進行插值求得更為精確的表達結(jié)果
- 可以用于對ray marching等算法進行加速
- 可以無需進行prefiltering處理以實現(xiàn)cone intersection求取。
當(dāng)然,相對而言,SDF也有其不足之處,比如只支持position/normal等數(shù)據(jù)的存儲(其他數(shù)據(jù)就不支持了),又比如,要想得到可見性就只能通過trace的方式來計算。
2. 場景表達
下面來介紹一下如何使用SDF完成對場景的表達。

每個物件的SDF是在離線的時候通過暴力遍歷所有面片的方式求得的,SDF使用一張3D的volume貼圖表達,貼圖數(shù)據(jù)格式為float point 16,平均而言,每個物件的3D貼圖分辨率只需要50x50x50即可。
除了CPU算法之外,還可以借助GPU的并行加速功能來完成這個計算。

由于3D貼圖分辨率有限,為了保證精度,就需要將其覆蓋范圍收縮得足夠小,而非直接覆蓋整個場景。
如上圖所示,在這種時候要計算某個點到場景的最小距離,則需要分成兩步:
- 第一步,是求取當(dāng)前點到覆蓋范圍bounds的交點,計算當(dāng)前點到交點的距離
- 第二步,從上個交點處獲取到物件的最小距離
將上述兩步的距離相加用作當(dāng)前點到物件的最小距離的近似。

將SDF用trace steps表示出來,大概如上圖所示。

這里的一個問題是,對于開口的物件而言,其SDF是如何表示的內(nèi),被半包圍的空間處的數(shù)值是正還是負(fù)?
如上圖所示,判斷某個點的SDF數(shù)值是正還是負(fù),主要看其與物件的碰撞面是frontface還是backface的,前者是正,后者是負(fù)。

樹木等物件的SDF是將樹葉當(dāng)成雙面物體來計算的,不過這種物件在ray marching的時候消耗會十分的高,因為面片過于復(fù)雜,光線在其中穿梭的時候,SDF比較小,因此step也就跟著變小,導(dǎo)致采樣數(shù)目劇增。

SDF是支持同一物件的不同實例的,使用同一套SDF數(shù)據(jù),只是變換不一樣。

因為分辨率較低的原因,一些物件的邊緣區(qū)域可能就被四舍五入掉了,而一些薄片可能就僅僅只保留了內(nèi)部的一個負(fù)值像素(為了避免射線marching過程中的遺漏,這個保留是必要的。)

在實際使用中,通常不會對單個物體進行ray marching,而是會將場景的所有物件的SDF組合成一個全局的SDF貼圖,對于一個長寬高都為512m的場景而言,這個貼圖的內(nèi)存消耗大約在300M左右。
這個全局SDF貼圖的更新是在GPU上完成的,CPU將需要更新的數(shù)據(jù)(比如要移動某個物件,只需要將物件的變換矩陣上傳即可)上傳到GPU,GPU根據(jù)數(shù)據(jù)對各個物件的SDF進行讀取,并將之寫入到全局SDF中,這個更新是增量完成的。

將SDF數(shù)據(jù)放到GPU,可以加速剔除,這里沒有給出剔除的實現(xiàn)細(xì)節(jié),推測一下,大概是從屏幕空間每個像素發(fā)射一條射線,計算交點即可判斷哪些物件是可見的了,而各個射線的求交是并行的。

這一頁從PPT給的關(guān)鍵字沒推測出描述的是什么,大概是使用SDF用作地形的heightfield?后面有機會再補上相關(guān)描述。

當(dāng)前使用volume texture的表達方式可以表達絕大部分場景的數(shù)據(jù),只是在如下的一些數(shù)據(jù)表達上還存在問題:
- 非均勻拉伸的物件
- 蒙皮等動態(tài)deform的數(shù)據(jù)
- 大型有機物或者volumetric terrain數(shù)據(jù)。
除了這種方式之外,當(dāng)然也還有其他的一些表達方法: - 分析式的Distance Function
- sparse volumetric SDF等
至于SDF的應(yīng)用,則凡是會有用到cone tracing的相關(guān)算法,應(yīng)該都能實現(xiàn)一定的加速。
3. 直接陰影

SDF的一個作用是可以很方便的計算直接光照的陰影,且得到的陰影分辨率會十分之高。

這張PPT給出的信息并不是十分直觀,在沒有對應(yīng)的演講視頻的情況下,只能根據(jù)個人理解推測其表達意思。
具有球狀外形的徑向光源(方向光、點光、聚光燈應(yīng)該都能應(yīng)用這種處理方式吧?)的處理邏輯:
- 根據(jù)光源的覆蓋范圍計算出受影響的物體
- 將屏幕分成tile,每個tile記錄一個光源列表
這個列表是通過對光源的cone與物件bounds相交來計算得到的?(沒有具體細(xì)節(jié))

這里給出了陰影計算的算法步驟:
對于每個處于光源cone覆蓋范圍之內(nèi)的物體而言:
繪制在屏幕空間中的每個像素發(fā)射一條朝向光源的射線
對于射線上的每個采樣點:
采樣SDF,得到當(dāng)前點到場景平面的最小距離。
計算DistanceToSurface/ConeRadius,這個數(shù)值實際上是cone的半角的tangent,判斷與此前紀(jì)錄的最小的比值的大小
每個采樣點前進步長為abs(DistanceToSurface)
當(dāng)射線與場景相交或者達到了最大采樣數(shù),則終止raymarching
最終的陰影值就為1減去記錄下的最小比值

這里給出了一個解釋,使用DistanceToSurface/ConeRadius得到的結(jié)果與(DistanceToOccluder/SphereRadius)^X的結(jié)果是一致的。
DistanceToOccluder指的是像素發(fā)出的射線到相交點處的距離,而SphereRadius則是遮擋體的半徑, 倒過來
可以看成是遮擋體占據(jù)的cone的比例,也就是陰影的濃度,那么倒過來之前就是光透過遮擋體進入當(dāng)前像素的比例。
DistanceToSurface/ConeRadius中DistanceToSurface表示射線上當(dāng)前采樣點到最近的遮擋體的距離,ConeRadius則是當(dāng)前采樣點對應(yīng)的cone的半徑,同樣這個公式可以用來表達光源經(jīng)過遮擋體之后進入當(dāng)前像素的光照比例,因此這兩個式子在直觀上應(yīng)該是一致的。

平行光的處理邏輯跟前面的徑向光源相似,不同的地方在于:
- 不再需要為光源查找對應(yīng)的屏幕空間tiles,而是全屏幕都參與
- 每個像素沿著光源方向的射線長度不再是光源的覆蓋球的半徑,而是max view distance。


由于SDF無法處理形變物體,因此帶有頂點動畫的植被等物件就不太適合使用這種方式來求取陰影,不過這也分情況,當(dāng)距離比較遠(yuǎn)的時候,完全可以停止頂點動畫,此時還是可以使用SDF求取陰影,否則只能考慮使用CSM來計算陰影了,上圖給出的效果可以看到,CSM的陰影精度相對于SDF的陰影精度有比較大的差距。

此外,遠(yuǎn)景植被如果退化成公告板,那么也是不能使用SDF來計算陰影的,這種時候可考慮使用conservative depth write(搜了一圈,沒找到相關(guān)技術(shù)文檔,不過這么遠(yuǎn)的距離,可以考慮直接將陰影烘焙到公告板上吧?)來解決這個問題。

相對于傳統(tǒng)Shadow Map方案,使用SDF計算陰影有如下的一些好處:
- 可以實現(xiàn)帶有比較明顯輪廓的面陰影(如上圖中的下方小圖所示,效果更為真實)
- 不會出現(xiàn)此前shadow map精度不足導(dǎo)致的毛刺或者peterpan問題
- 陰影消耗與場景復(fù)雜度無關(guān),只取決于raymarching過程中的采樣點數(shù)目(與場景密度有關(guān))與屏幕分辨率,當(dāng)然可以使用半分辨率進行計算來降低消耗,且支持一個較大距離的投影,還沒有CPU的消耗。
- 相對于傳統(tǒng)的陰影方案如cubemap/CSM而言,會快30%~50%。
4. Sky Occlusion 問題與解決方案

SSAO只能給出小尺寸的遮擋效果,而我們經(jīng)常需要一些較大范圍(10m)的遮擋效果。

在SDF表達的場景中,使用單個cone進行trace可以得到較為柔軟的陰影效果,但是對于來自四面八方的天光而言,使用單個cone就不合適了。

這里考慮使用分布在以法線為中心方向的半球面上的多個cone trace來計算天光遮蔽,將多個cone trace的結(jié)果平均一下作為最終的天光遮擋效果。

每個cone的trace范圍限定在10m左右。

如果覺得cone的數(shù)目太多會導(dǎo)致消耗過高,可以考慮只使用一個cone,選擇bent normal作為cone的trace方向,trace的結(jié)果會被用在天光的SH diffuse lighting部分。

此外也可以使用視線的反射方向作為cone trace的方向來獲取specular。

這里給出天光遮蔽計算的一些優(yōu)化策略(信息有限,加入個人推測):
- 將屏幕空間劃分成大小均勻的tile
1.1 每個tile存儲兩個bounds,每個bounds的邊長存儲在一張depth貼圖中,分別表示起始trace距離與終止trace距離?這個bounds怎么計算?下面有說
1.2 每個tile還包含兩個culled list,什么作用? - 還可以借助SDF進一步降低計算消耗,how?

這里說到,在AMD的GCN架構(gòu)下,使用光柵化算法比直接使用CS效率要高。其實現(xiàn)算法給出如下:
- 從frustum culling處理后的結(jié)果中構(gòu)建Draw Buffer
- 每個物件的覆蓋范圍bounds作為渲染的數(shù)據(jù),使用DrawIndexedInstancedIndirect一次性完成所有bounds的繪制
- 在PS中完成各個像素的min/max bounds的輸出。

為了提升計算效率,這里cone trace是在1/8分辨率(單緯度)下計算的,但是這種情況下計算的結(jié)果會有很強的鋸齒感,需要添加一個1/2分辨率的bilateral filtering,這個filtering是geometry aware的。

還可以進一步通過借助TAA的velocity數(shù)據(jù)來降噪。

另一個優(yōu)化策略是將物件的Distance Field合并成一個場景Distance Field,因為如果是對物件的SDF進行采樣的話,由于物件數(shù)目眾多,在采樣的過程中需要頻繁切換采樣貼圖,可能還有一些其他消耗,而合并成一張貼圖之后流程就更為清晰簡潔了。

場景SDF是存儲在以相機為中心的四級Clipmap(每級clipmap都以相機為中心,不過覆蓋范圍逐級遞增(比如翻倍))中的,每級Clipmap的分辨率都是128^3。
clipmap的覆蓋范圍會隨著相機的移動而進行逐行或者逐列更新。
覆蓋范圍大的clipmap的更新頻率會低一些。

場景SDF的問題在于靠近物件時的精度會有所不足,這里可以考慮使用兩級策略,在cone開始的時候使用物件SDF,其他地方使用場景SDF。

這種處理策略可以極大的降低SDF的堆疊復(fù)雜度,提升運行效率。

這里給出了天光遮蔽算法各個步驟的執(zhí)行消耗。

這個算法目前還存在一些問題,上面的描述比較抽象,似乎是說室內(nèi)遮擋效果會存在問題,具體后面有更多信息再來補充。

SDF除了前面的應(yīng)用之外,還可以用在gameplay中,比如用于實現(xiàn)粒子與場景的交互效果。

還可以用于實現(xiàn)軟體模擬、gameplay玩法以及漸進式flow map生成等。
