一篇光線追蹤的入門

一篇光線追蹤的入門

來自專欄 Life of a Pixel379 人贊了文章

從年初GDC放出DXR的消息已經有很長一段時間了(最初接觸DXR的相關API還是在NVIDIA工作的時候,轉眼大半年過去,我已經離開了NVIDIA),這是一篇基於我對光線追蹤的了解寫的入門文章(因為我本人也只是入門水平)。文中會少量涉及DirectX Raytracing的相關API,大部分還是闡述光線追蹤技術的基本原理,在末尾的部分會recap今年GDC上的兩篇[1][2]有關光線追蹤的演講。希望不太熟悉這個領域又時常聽說光線追蹤這個概念的同學能夠通過這篇內容粗略地了解整個光線追蹤技術的框架。

這篇內容會覆蓋的概念包括渲染方程蒙特卡洛積分重要性採樣低差異序列路徑追蹤,包括用實時光線追蹤實現貼圖採樣軟陰影AO間接反射間接漫射,半透明渲染和次表面散射。由於大部分內容都是概念性介紹,我會在文章的末尾把我看過/部分看過的書籍文獻列出來,以便大家深入學習。

什麼是光線追蹤?

這是一個很難回答的問題,它的概念根據上下文不同,可能是某個具體的實現技術,也可能是一個框架的總稱。一般來說,我們總是喜歡把它和另一個概念:光柵化渲染作為相對的兩個概念。

熟悉實時圖形學的同學都知道,現代的遊戲渲染引擎都是以光柵化渲染為基本框架搭建的:一個複雜的場景的渲染任務會以物體為單位劃分為若干個子任務,每個物體由若干三角面組成,我們將這些三角面經過幾何變換映射到屏幕的某些區域,然後將三角面覆蓋的區域拆解成一個個的像素,這個拆解的過程就叫做光柵化。在這個逐層拆解的過程中,下一層就會失去對上一層全局信息的了解,比如拆分成物體後,我們就不知道場景里其他物體的存在了,拆分成三角面後,我們就無法得知其他三角面的信息,到對每個像素進行著色時,我們連該像素所在的三角形的信息也丟失了。這樣拆解任務可以讓渲染過程高度並行化,所以非常的快,但是同時因為全局信息的丟失,我們很難實現一些需要全局信息的渲染效果。

與之相對的,近年來渲染技術的發展,多半是圍繞全局照明效果的,譬如環境光遮蔽,間接反射,漫射等。這些技術五花八門,實現方法完全沒有一個統一的框架可以遵循,也導致了近年來遊戲引擎的設計越來越複雜。而早在上個世紀,在離線渲染領域,這些技術就已經有了統一的解決框架,即光線追蹤。

與光柵化渲染不一樣的是,光線追蹤把一個場景的渲染任務拆分成了從攝像機出發的若干條光線對場景的影響,這些光線彼此不知道對方,但卻知道整個場景的信息。每條光線會和場景並行地求交,根據交點位置獲取表面的材質、紋理等信息,並結合光源信息計算光照。

一個典型的光線追蹤場景

管線

不論是光柵化框架還是光線追蹤框架,一個三角面/一根光線的整個繪製過程都可以劃分為若干個階段,這些階段合在一起就是我們通常意義上說的管線(Pipeline)。一個典型的光柵化管線(Raster Pipeline)如圖:

Raster Pipeline

這樣設計的好處是一個渲染任務變得模塊化並且耦合度較低,便於硬體及底層的圖形API設計。可以看到,圖形管線中有部分的階段是所謂的固定管線,而另外一部分則是可編程管線。固定管線完成一些固定的任務,比如頂點的獲取/剔除,頂點數據的插值,深度/模板的剔除等,這些模塊只能通過一些配置參數來進行控制;而可編程管線則允許用戶使用自定義的著色器(Shader)對數據(頂點,面,像素)進行處理。關於光柵化管線的細節可以看NVIDIA的這篇文章:Life of a triangle - NVIDIAs logical pipeline。

近年來GPU因為在並行計算上的優勢越來越多地被用在了通用計算(GPGPU)的領域,因此在原有光柵化管線的基礎上,也就引申出了計算管線(Compute Pipeline)。類似的,DXR API實際上就是定義了一種適用於光線追蹤的Raytrace Pipeline。

渲染方程

渲染方程[3]回答了這些問題:即究竟什麼是渲染?一個表面上某一點的亮度/顏色究竟如何計算?通常我們看到的渲染方程形式是這樣的:

一個表面的亮度(這裡不打算去解釋Irradiance/Radiance/Flux這些物理概念,大家僅做直觀理解即可,想要進一步去了解這些概念可以去看相關資料[4][5])是由兩個部分組成的,一部分是表面的自發光(上述公式中的 L_e ),另一部分是其他表面的射向該表面的光線(上述公式中的 L_i )經過BRDF(上述公式中的 f_r )作用後的結果。BRDF描述的是表面本身的性質,比如它的光滑程度,導電程度等等。由於四面八方的光線都會作用在這個表面,所以我們需要對所有方向進行積分,也就是一個球面上的積分,考慮到積分項中的 (w_icdot n) ,那麼只有位於正半空間的方向才會對最終積分有貢獻,所以最後這個球面的積分就變成了一個半球的積分。

蒙特卡洛積分

蒙特卡洛積分本身只是一種數值計算的方法,它和光線追蹤本身無關:我們知道要求出某個函數的解析形式的積分是一件很難的事,有時候甚至是不可能的,因此,我們為了能夠估計某個函數在一個定義域內的積分,就需要一些估計方法,蒙特卡洛積分就是這樣一種方法。它的思路很簡單:為了估計某個函數在一個定義域內的積分,我只需要在這個定義域內隨機地找一些採樣點,計算採樣點所在位置的函數值,把所有採樣點的函數值平均即可得到該積分的估計值:

Approximation(Average(X))=frac{1}{N}sum_{n=1}^{N}{x_n}

很多人在學習編程之初可能都寫過用拋針法計算圓周率的程序,仔細回憶,這個過程實際上用的就是蒙特卡洛積分。這個方法直觀簡單,對一些高維的難以求解的積分有非常好的效果,而渲染方程就是這樣的一類積分,所以它常常被用在求解渲染方程上。

拋針法計算圓周率

那麼如何採樣呢?最簡單的方法就是直接在積分的定義域內生成均勻分布的隨機樣本,但是實際上根據函數的形狀不同,不同位置的採樣點的函數值對最終積分結果的貢獻也是不一樣的。比如函數值較小變化又比較平緩的位置,生成太多的樣本會浪費;而在一些函數值比較大變化又快的位置,可能生成的樣本又太少。如果我們先驗地知道函數的形狀,那我們就可以針對性地生成非均勻分布的隨機樣本,這樣能夠在相同樣本數量的情況下對目標積分得到一個更準確的估計,這就是我們常說的重要性採樣。重要性採樣和它的名字一樣,就是盡量採樣積分定義域內重要的點,少採樣不重要的點。

重要性採樣的示意圖,說明了隨機樣本分布函數(pdf)選擇的重要性,pdf的形狀和被積函數越相似,則積分的估計值越接近真實值

改寫我們之前原始的蒙特卡洛積分,得到下面的公式:

Approximation(Average(X))=frac{1}{N}sum_{n=1}^{N}{frac{x_n}{pdf(x_n)}}

有關如何重要性採樣的更多內容可以看這個系列文章[6]。當我們得到了一個隨機分布的概率密度函數(pdf)後,我們可以使用逆變換採樣[7]的方法從一個均勻分布去生成任意概率密度分布的樣本。

對於均勻分布的樣本生成來說,有時候完全隨機的樣本反倒容易引入不必要的雜訊,我們通常希望一個樣本分布即有一定的隨機性,同時總體來說又是有序地,能夠很好地覆蓋整個樣本空間。這時我們就需要引入QMC[8](Quasi Mento Carlo)和低差異序列的概念了。關於低差異序列,我們可以認為它是一個確定的樣本序列生成方法得到的一組樣本。它看起來有一些隨機性,同時又最大程度上均勻分布在了整個樣本空間里。比較常用的兩個低差異序列是Hammersley Sequence和Halton Sequence。前者需要知道總樣本生成數量,適合確定樣本數的低差異序列,後者則不需要,更適合步進渲染(Progressive Rendering)。有關低差異序列的詳細介紹參見這篇文章[9]。

偽隨機序列和低差異序列的區別

由於蒙特卡洛積分是一種隨機採樣的方法,因此它的估計結果也是一個隨機變數,這個隨機變數的統計特徵就被我們用于衡量不同的渲染框架的效果,最常用的統計量是期望和方差(或者叫一階矩和二階矩),如果一個估計(隨機變數)的期望和被估計量的真實值是一樣的,我們就說這個估計是無偏的,如果一個估計的方差隨著樣本數的增加逐步減小(表示這個估計和真實值越來越接近),那麼我們就說這個估計是一致的。樣本數量和方差的關係可以用于衡量一個估計的收斂速度。有關這兩個概念的解釋可以看這個答案:文刀秋二:如何理解 (un)biased render?

路徑追蹤(Path Tracing)

渲染方程本身給了我們一個很好的數學形式上的定義,讓渲染不再是一個盲目嘗試的過程,但渲染方程本身在絕大多數情況下是無法直接求解的,為此,人們把渲染方程用不同的數學等價形式改寫,然後對新的方程形式進行近似求解。其中路徑追蹤就是這樣的一種方法。它將渲染方程改寫成了這樣的形式:

L_o = L_Nprod_{i=1}^{N}{c_i}

其中,

c_i=frac{f(w_i,w_o,p)cos	heta}{p(w_i)}

按照經典的渲染方程的形式,我們為了某個表面的點的顏色,首先要以它的法線為中心向半球內發射若干條光線,求出每條光線和場景的交點,要進一步以交點的法線為中心發射若干條光線,這是一個極難求解的遞歸過程。而路徑追蹤的方法把某個點的顏色看作是若干條光路(Path)合起來的貢獻。一條光路可以認為是若干表面點連接而成的一條線段,為了計算某個點的著色,我們只需要以該點為起點,構建若干條路徑,並將每條路徑上的光照貢獻疊加即可。

結合光線追蹤的基本框架,我們可以認為路徑追蹤就是把光線以路徑的形式重新組織了起來。

path tracing中的一條路徑

DirectX Raytracing API

有關DXR的介紹主要是來自於官方文檔[10],本文不打算介紹API的細節,只就整個Pipeline和一些基本概念做簡單介紹,希望大家能夠了解編寫DXR程序的基本原理。由於目前DXR API只支持Volta架構的GPU,相信大部分同學都和我一樣買不起Titan V,所以微軟也提供了一套基於Compute Shader的DXR Fallback Layer實現。只要顯卡能夠支持DX12即可在其基礎上進行編程。此外微軟的PIX已經支持了DXR API的調試,但是如果使用的是Fallback Layer的話,看到的還是Compute Pipeline的一些調試數據。

回憶一下我們在DX12中使用Raster Pipeline時如何繪製場景

(1)定義一些描述場景的數據,如幾何數據,貼圖,材質、燈光等信息,以Buffer和Texture的形式上傳這些數據到VRAM;

(2)定義如何繪製模型的著色器(VS,PS,GS,HS,DS等);

(3)通過Root Signature定義Shader的形參,並使用Pipeline State Object(PSO)完成整個管線的配置(shader及一些固定管線的設定);

(4)通過各類數據的訪問視圖(CBV,UAV,SRV,RTV,DSV)規範化數據的訪問形式,把其中的一些View綁定給shader作為它們的實參;

(5)調用DrawXXX的命令完成一次Draw Call。

DXR API的設計在最大程度上復用了DX12的已有框架,諸如Buffer,Texture,Command List等概念在DXR API中仍然有效,本篇並不打算單獨對DX12進行詳細的介紹,對DX12不熟悉的同學可以去看看微軟的官方Sample或者這本書[11]。

著色器

在DXR API中一共有五類著色器:

Ray Generation Shader:這個Shader負責初始化光線,是整個DXR可編程管線的入口,我們在其中調用TraceRay函數向場景發射一條光線。Ray Generation Shader從結構上非常像Compute Shader(在 GPU硬體中,它很可能也是通過Compute Pipeline來實現的),不同的是Compute Shader的組織形式是Group->Thread這樣的架構,同一個Group中的Thread可以使用Shared Memory及一些原子操作進行數據共享和同步,但在Ray Generation Shader中,每條光線是彼此獨立的,他們之間不需要線程間的同步和共享。此外RayGeneration Shader也負責最終著色結果的輸出(通常是輸出到一個UAV對應的Texture上)。

Intersection Shader:這個Shader是可選的,它只負責一件事,就是定義場景內基本的幾何單元和光線的相交判定方法。如果場景的基本幾何單元是三角面,則用戶不需要自定義這個Shader,但如果不是三角面,而是用戶自定義的幾何形式,比如是用於體渲染的體素結構,或者是用於曲面細分的參數曲面,則用戶需要提供提供相交判定的方法。這樣的設定使得一些過程生成式模型(細分曲面,煙霧,粒子等)也能夠使用光線追蹤的框架進行渲染。需要注意的是,對於三角面來說,通過相交測試返回的是交點的重心坐標,用戶需要根據重心坐標自行插值得到交點的相關幾何數據(uv, normal等)。

Any Hit Shader:這個Shader的作用是驗證某個交點是否有效,典型的應用是alpha test,比如我們找到了一個光線和場景的交點,但該位置剛好是草地中被alpha通道過濾掉的像素位置,我們希望光線穿過它繼續查找新的交點,那麼就可以在這個Shader中忽略找到的交點,此外,我們也可以在這個Shader中發射新的光線,一個可能的應用場景是折射/反射效果的實現。

Closest Hit Shader:當一條發射的光線經過和場景的若干次求交最終找到一個有效的最近交點後,就會進入這個Shader,這個Shader的作用很像我們在Graphics Pipeline中用的Pixel Shader,它用於某個找到的樣本點的最終著色。

Miss Shader:這是一個可能會被忽略的細節,即如果找不到有效的交點怎麼辦?在真實世界中一條光線總是會和某個表面相交,但虛擬場景的有限空間內卻並非如此,這時候我們可能希望進行一次cube map的採樣,或者告訴Ray Generation Shader它沒找到交點,這些行為都可以在Miss Shader里定義。

Shader之間如何傳遞參數?

就像Graphics Pipeline使用可變寄存器傳遞參數一樣,Raytracing Pipeline使用用戶自定義的payload數據結構傳遞參數。不同的是,可變寄存器的數據傳遞是一對多的,頂點處理階段傳遞的參數要經過GPU的插值後傳遞給PS,但payload是伴隨著光線在場景中傳遞的,一條光線只有一份,它伴隨光線傳遞。每個階段的shader都可以動態的讀寫這個數據結構,把信息傳遞到Raytrace Pipeline的下一個階段。

場景數據結構

在Raytrace Pipeline中,幾何數據由兩層結構存儲,其中底層的數據結構稱之為Bottom Level Acceleration Structure,上層的數據結構叫做Top Level Acceleration Structure。

Bottom Level AS:這層存放一般意義上的頂點數據,類似於Raster Pipeline的Vertex/Index Buffer的集合。當然如果是非三角面的話,可能存放的是其他數據(比如參數曲面的控制點)。

Top Level AS:Top Level AS是模型級別的場景信息描述,它的每一項數據引用一個Bottom Level AS(當做該模型的幾何數據),並單獨定義了該模型的模型變換矩陣以及包圍盒。

基於這樣雙層的數據結構,我們就可以調用DXR API創建整個場景的查詢結構,這個查詢結構是一個BVH(Bounding Volume Hierarchy),用來加速光線-場景的相交測試。可以想像,當一個相交測試開始時,光線首先會和BVH進行相交測試,通過的對象才會進一步訪問其Bottom Level AS數據執行具體的光線-三角面相交測試。

由於整個BVH只用於進行光線-場景相交測試,因此它只包含頂點位置的信息,如果我們需要頂點位置之外的信息(uv,normal等),則往往需要額外自定義一個SRV/UAV Buffer用於存儲這些數據。

著色器數據綁定

類似Raster Pipeline,Raytrace Pipeline也通過Root Signature去綁定shader所需的參數,不同的是,Raytrace Pipeline的Root Signature分為Global和Local,由於Raster Pipeline在場景繪製時是per object的,所以object相關數據和全局數據是沒有區別的,但是Raytrace Pipeline是per ray的,為了傳遞per object的數據(模型用的貼圖,矩陣等),我們必須再定義一系列的Local Root Signature。Global Root Signature和Raster Pipeline中一樣,用於傳遞一些與具體模型無關的數據(比如整個場景的燈光列表,攝像機矩陣等)。

以上只是設定了shader的形參,在實參的傳遞時,Global Root Signature和Raster Pipeline裡面一樣,而Local Root Signature的實參傳遞則需要一個叫做Shader Table的數據結構(本質上是一個SRV Buffer)。這個Shader Table是一個數組,數組中的每一項被稱之為一條Shader Record,每條Shader Record存儲一個shader的id(固定長度)以及它對應的Local Root Signature的實參列表(變長)。這樣,當某個shader操作具體的模型時,就可以根據它的object id去訪問Shader Table中的某一條Shader Record來獲取對應的局部實參。

管線狀態對象(Pipeline State Object)

類似於Raster Pipeline,Raytrace Pipeline也有它自己的PSO,其中定義了會用到的Shader和一些基本的光線參數(光線最大遞歸層數等)。

繪製命令

在所有數據都準備完成後,我們可以調用DispatchRays發起一次光線追蹤。

Raytrace Pipeline的流程

下圖是Raytrace Pipeline的基本流程圖,其中綠色的部分表示可編程單元,灰色的則是固定單元。可以看出Raytrace Pipeline以可編程管線為主,只有極少的固定單元。基本概念中我們已經介紹過,任何一個光線追蹤的渲染程序都是從Ray Generation Shader開始,它負責初始的光線生成,生成的初始光線會通過固定的軟/硬體單元對整個場景(我們構建好的BVH,在DXR中也就是Top Acceleration Structure)進行遍歷求交,這個求交過程可以是用戶自定義的一個Intersection Shader,也可以是默認的三角形相交測試。一旦相交測試通過,即得到了一個交點,這個交點將會被送給Any Hit Shader去驗證其有效性,如果該交點有效,則它會和已經找到的最近交點去比較並更新當前光線的最近交點。當整個場景和當前光線找不到新的交點後,則根據是否已經找到一個最近交點去調用接下來的流程,若沒有找到則調用Miss Shader,否則調用Closest Hit Shader進行最終的著色。

我也嘗試了這套API,渲染了一張滿是噪點的圖:

圖中的反射,AO及軟陰影是Raytrace Pipeline生成的,其餘部分則由Raster Pipeline及Compute Pipeline完成。

DXR API在實時渲染中的應用

混合已有的管線

儘管DXR看起來非常美好,也代表了圖形渲染未來的方向,但是現階段性能仍然是它最大的問題。因此,混合使用多種管線(Raster/Compute/Raytrace Pipeline)仍然是最經濟有效的方案。同時這也能保證現有引擎的最少改動來支持光線追蹤。文章[1]中的這幅圖很好的說明了這個問題:

可以看到,一個場景的繪製分為多個子任務,其中G-Buffer的繪製這類不需要全局信息的任務可以交給Raster Pipeline去做;而像是光照、後處理這類基於全屏繪製的任務,則可以交給Compute Pipeline去充分發揮;至於軟陰影、反射、環境光遮蔽(AO)、半透明這類大量依賴全局光照信息的效果,則是Raytrace Pipeline的特長。此外,傳統的光線追蹤框架首先需要從攝像機向屏幕發射主光線並和場景求交,有了G-Buffer,我們可以直接讀取G-Buffer的信息,這樣就避免了主光線的生成。

有了這個思路,我們只需在一些全局照明的效果部分替換現有的技術模塊(如SSR,VXGI,SSAO等)即可升級引擎支持光線追蹤。

AO

AO的部分比較簡單,一個公式就可以說清楚:

簡單的說,就是以主光線的交點位置為起點,交點的法線為中心,發射若干條隨機光線做半球上的蒙特卡洛積分,積分結果再除以PI歸一化到[0, 1]即可。積分函數也比較簡單,就是某個方向上的可見性(Visibility)乘以採樣方向和法線的點積,根據這個積分函數形式,我們可以選擇做cos weighted的重要性採樣。至於去噪的方法,文章[1][2]提到的都是直接用TAA。

我們知道,SSAO的方法為了找到半球內的可見性,一般是在GBuffer的某個像素為中心取一系列相鄰像素作為積分的採樣點,這是假設相鄰像素在位置上離得也比較近,缺點是幾乎只能找到比較近的遮蔽,稍微遠一點的遮擋關係就找不到了,這兩張圖很好的說明了問題:

軟陰影

軟陰影的實現[1][2]也基本類似。我們知道,精確光源(點光源,方向光源)無論如何是無法生成軟陰影的,因為半影區的形成就是因為光源本身有一定的體積。為了讓這類光源產生軟陰影,我們可以假設光源本身是一個球形,那麼這個球形對於當前計算陰影的點張的立體角就是一個錐形區域,我們只需要以著色點為起點,在錐形區域內隨機選擇一個方向,用這條光線去做可見性測試。對於點光源,我們可以設定它的球形半徑,這樣立體角就是一個隨著相對位置變化的值;對於方向光,我們認為它是一個無限遠的球形,它對應的立體角是一個固定值。可以想像,如果我們將這個立體角逐步減少到0時,得到的就是一個精確的硬陰影。

基於cascade shadow map的方法和基於uniform cone sampling的光線追蹤生成的陰影對比,視覺上後者更符合直覺。

間接反射

反射這部分[1][2]中的實現看起來是有比較大的差異,但總體來說思路都是從Screen Space Reflection這類技術擴展而來,有關SSR的各類方法我會在今後的文章中專門去綜述。

[1]中沿用了自家Frostbite引擎的在SIGGRAPH 2015上用到的方法[12],好處是能夠物理正確地處理粗糙度相關的反射(而不是簡單的鏡面反射)。具體實現時使用半解析度的UAV Texture,把原方法中基於depth buffer的ray marching改為了直接用raytrace去找反射的交點,計算反射點的顏色,然後記錄對應採樣方向的概率密度函數(pdf),最後用一個resolve pass在全解析度去做合成。關於這部分的實現,作者很貼心的在slides裡面提供了偽代碼,想要自己實現的同學可以去看代碼。

一張對比圖,比SSR的方法最大的好處是反射點測量精確,不被屏幕空間內這一條件限制,著色方法則是和SSR類似

間接漫射

這部分兩篇文章實現的思路差異比較大,[1]的思路是完全依賴實時的光線追蹤,方法和AO的計算方法類似,也可以用cos weighted的重要性採樣,只是被積分的函數換成相應的BRDF函數即可。但是不同於AO的是,因為要積分的不是簡單的可見性,所以要得到一個視覺上較好的漫射結果,需要大量的光線樣本。為此,[1]使用了一個point based的方法,這個思路就比較像我們傳統的irradiance volume的方法,不同的是,irradiance volume的probe是設定在三維空間里,並且是離線烘焙的,而[1]中的每個point是分布在模型表面的,並且是實時(步進式)計算的,由於point的解析度遠低於屏幕解析度,所以可以多使用一些樣本去計算該點的indrect diffuse,計算最終顏色時,只需要根據像素的位置基於point去插值即可(這個和irradiance volume差不多)。文章中沒有提到如何去分布這些point,但感覺這個分布的演算法似乎可以用來生成草間彌生式的風格化渲染。。。

point based方法的可視化,下面的圖讓我想到了草間彌生,好像直接用這個結果也很好看呢。

[2]的方法相對來說沒那麼激進,是一個混合irradiance volume和raytrace的方案,它的思路也不複雜:對著色位置,基於重要性採樣在半球發射一系列短光線(這個很重要,因為短光線可以很大程度上加速單根光線的raytrace,這個優化對於AO也適用),如果短光線有擊中某個臨近表面,那麼我們就基於位置對那個表面位置進行一般性的irradiance的計算,如果短光線沒有擊中附近表面,我們就改用光線的終點位置的irradiance volume的信息。這樣,對遠處的靜態物體,大部分貢獻來自於irradiance volume技術,而近處的動態物體,則是依賴於實時光線追蹤的結果。下面是這個方法的示意圖:

沒有hit到的光線用irradiance volume的數據

hit到的光線用光線追蹤的結果

這個方法相對於傳統irradiance volume的方法有兩個好處,一是可以用於動態物體,而是減少了irradiance volume的漏光問題。

玻璃和SSS材質

[1]提到了他們的玻璃和SSS材質的做法,玻璃這部分比較簡單,大致就是在兩種材質交界的位置根據折射率生成對應方向的折射光線,然後根據材質本身的吸收率對光線進行吸收即可。需要注意的是,如果是毛玻璃,只需要在材質交界處的錐形區域內隨機發射若干折射光線即可。

光滑玻璃和毛玻璃在折射光線生成時的區別

SSS材質的實現比較有意思,它用了Frostbite在GDC 2011上提出的一個老技術[13]。具體做法是,對每一個著色表面,將頂點位置沿著法線的反方向推一小段距離到模型內部,以該點為起點,向整個球面發射光線做光線追蹤,針對每一個光線追蹤的交點(仍然是模型表面的點),利用比爾定律和各向異性的HG phase function計算它相對於模型內部點的單散射,最後gather所有交點的計算結果作為原表面點的BTDF。做過體積雲、體積霧的同學對這些概念一定不陌生。有關散射、體渲染的內容我們今後會有專門的文章去綜述,這裡不做展開。

計算BTDF的基本流程

紋理過濾

紋理過濾其實是一個很容易被忽略但又很重要的問題。我們知道紋理貼圖是一個多級的結構,也就是我們常說的mip-map,有了mip-map,在對紋理進行採樣時,我們就可以選擇一層合適的mip-map。但有許多人不了解mip-map的原理:當我們從屏幕出發渲染一個場景是,屏幕上的每個像素實際上不是一個點,而是對應場景模型上的一個小的表面,我們一般叫做footprint,這個表面上包含的紋素(texel)不止一個,如果我們只採樣單個紋素就會造成shading alias,為此我們構建mip-map。mip-map的下一層是上一層解析度的一半,並且是由上一層經過卷積運算得到的。這樣就可以根據屏幕像素對應的紋理空間的footprint大小去選擇一個層合適的mip-map去採樣。我們可以使用u,v坐標對於屏幕空間x,y方向的偏導數形成的向量的叉積去估計footprint的大小。footprint面積和面積到mip-map level的映射公式如下:

接下來的問題就是如何計算uv相對屏幕空間xy軸的偏導數,在光柵化管線中,這是自動獲得的。因為pixel shader會以32/64個像素(N卡和A卡及各代架構會有所不同)打包為一組去執行(在vertex shader中則是32/64個頂點打包為一組),這一組像素稱為一個warp,而一個warp內,又會以2*2的像素塊組織,這一個像素塊稱之為一個pixel quad,在像素塊內的四個像素在執行PS時是完全同步的(實際上warp內的所有像素也都是完全同步的),也就是說當某個像素執行到某一行時,其他像素也必須執行到了同一行。有了這個特性,我們就可以使用pixel quad相鄰像素的uv值做差分得到uv的偏導數,也因此我們可以使用ddx,ddy的命令去求取任意一個變數的差分。

到了光線追蹤管線中,我們不再有pixel quad這樣的概念,為了獲取uv的偏導數,就只能給原始光線添加兩根輔助光線用來計算偏導數,這兩根輔助光線會在原始光線的基礎上向上/右偏移一個像素的距離。然後兩根光線獨立做光線相交,交點的uv和原始光線交點的uv做差分去獲得偏導數。

這是傳統光線追蹤渲染器解決紋理過濾的方案,但是它使得我們需要發射的光線變成了原來的三倍,是比較低效的。這裡EA和NVIDIA合作開發了一個新的紋理過濾方案,但是怎麼做沒提,要等NVIDIA放出相關的論文實現了。

降噪

這個部分也沒有太多描述,可以總結為一句話:TAA大法好。[1]借鑒了SVGF的方法[14],在TAA的基礎上再做空域的濾波,[2]基本只用了原有的TAA框架。

結尾

有關本文介紹的光線追蹤的一些基本概念,建議大家去看Scratchapixel有關光線追蹤和蒙特卡洛積分的系列入門文章[6][15]。

進一步去了解全局照明演算法相關的知識,大家也可以去讀經典的PBRT:《Physically Based Rendering: From Theory to Implementation》,秦春林老師寫的《全局光照技術》也是非常好的相關書籍。

需要進一步了解DXR API,建議去看微軟的官方Sample:Microsoft/DirectX-Graphics-Samples,此外知乎上也有兩篇這個主題的文章[16][17]。

另外明年會出版《Ray Tracing Gems》,應該會有大量的工業界如何使用實時光線追蹤的例子,大家可以關注。

嗯。。。所以說人生就好像一個蒙特卡洛積分,你的世界觀就是那個積分值,而世界觀的形成就是在以你自身為中心對這個世界進行估計,估計的方法就是去經歷各種事,每一件經歷過的事給你的內心體驗最後都會作為一個樣本更新你的世界觀。隨著時間的推移,你經歷的事越來越多,世界觀也就越來越穩定,幾乎沒什麼事再讓你興奮了,你就變成了一個無趣的成年人。接受教育就像是重要性採樣的過程,它能保證有些事即使你沒有經歷過,也能通過已有的知識去推測和判斷,但同時也很可能讓你更快地變得無趣。。。有關渲染的話題還有幾篇內容在寫,希望今年草稿箱可以清空。另外最近初來杭州,喜歡圖形的朋友(特別是網易的各位大佬)可以考慮找我面基。也歡迎喜歡做遊戲(尤其是galgame)的小夥伴一起聊聊或者做些有意思的東西。

PS:今天在Siggraph 2018上我的前老闆(還好去年年會的時候完成了和黃老闆合影打卡的新手任務)發布了新一代圖靈架構顯卡,也是首款硬體支持光線追蹤的GPU,感興趣的同學可以關注新顯卡發布的一些消息:NVIDIA Unveils Quadro RTX, World』s First Ray-Tracing GPU,重點可以關注GigaRays/sec這個光線追蹤的指標,大家可以自己換算一下,大概相當於多少spp per frame。另外,接下來可能會考慮再寫一篇有關光線追蹤實時去噪技術的綜述,作為本篇文章的進階內容。

引用

[1] Shiny Pixels and Beyond: Real-Time Raytracing at SEED

[2] Experiments with DirectX Raytracing in Remedys Northlight Engine

[3] Rendering equation

[4] Real-Time Rendering, Third Edition. 8.1 Radiometry for Arbitrary Lighting

[5] Introduction to Light, Color and Color Space

[6] Monte Carlo Methods in Practice

[7] Inverse transform sampling

[8] Quasi-Monte Carlo method

[9] 低差異序列(一)- 常見序列的定義及性質

[10] DXR Functional Spec

[11] Introduction to 3D Game Programming with DirectX 12

[12] Stochastic Screen-Space Reflections

[13] Approximating Translucency for a Fast, Cheap and Convincing Subsurface Scattering Look

[14] Spatiotemporal Variance-Guided Filtering: Real-Time Reconstruction for Path-Traced Global Illumination

[15] Mathematical Foundations of Monte Carlo Methods

[16] 光線追蹤與實時渲染的未來

[17] DirectX Raytracing(DXR) functional Spec閱讀筆記+註解


推薦閱讀:

TAG:DirectX光線追蹤DXR | 計算機圖形學 | 遊戲引擎 |