Render M materials and N lights

在 hardware render pipeline 中,若現在場景內有 M 個 material 相異的物件與 N 盞 lights, render 的方式會有幾種選擇?

  • single-pass, multi-light
  • multi-pass, multi-light
  • deferred shading

Single-pass, Multi-light

for each object:
 render object with all lights in one shader

此種作法是最單純且直接,但是此方式會連 hidden surface 也一併 shading ,造成運算上的浪費。此外,當光源數目 N 很大的時候,shader code 必須擴展進而去計算每盞光源的作用。但若是指令數超過 shader model 的限制時(視 shader model 而定),就不得不拆成多個 render pass。

Multi-pass, Multi-light

for each light:
 for each object affected by light:
  render target += BRDF(object, light)

這方式是將每盞燈光的所影響的結果累加於 render target,+= 的部分是透過 ping-pong 的方式在兩個 render target 之間作替換。然而 invisible surface 所造成的計算浪費仍舊沒有獲得解決之道,新增的 geometry pass 也在 vertex transform 與 setup 的部分帶來了額外的開銷。這部份的問題,雖然可以透過粗略的 front-to-back sorting 獲得一些抒解,但 deferred shading 卻對此問題有著更根本的解決方案(當然也會帶來其他的限制)。

Deferred Shading

for each object:
 render lighting properties to G-buffer
for each light:
 render target += BRDF(G-buffer, light)

Deferred shading 之所以能解決 hidden surface 所造成的 shading 浪費,主要是因為它在第一階段先輸出 position, normal 與其他參數至 G-buffer。使第二階段的 lighting 運算能完全作用在 visible 的 fragment 上,同時也有助於降低 light/object 之間的相依性,藉此達到 render 效率的提升。一切看起來似乎都很美好,但故事就這樣結束了嗎?喔!不,故事才剛開始…

每個 pixel 的顏色在 lighting 的階段是由 ambient, diffuse 和 specular 這三個部分所組合而成的。由於 deferred shading 一般都把 ambient light 和 environment light 的部分拆在其他 pass 獨立運算,因此在僅考慮 local lighting model 的情況下,deferred shading 的公式主要可分為 diffuse 和 specular 兩個部分:

L_o(v)=\sum_{k=1} ^n{(c_{diff}*f_{diff}(v,n,I_{L_k})+c_{spec}*f_{spec}(v,n,l_k,I_{L_k}))}

其中 f_{diff}f_{spec} 是 diffuse 和 specular 的 local reflection model(ex. lambert, phong, blinn, … etc),c_{diff}, c_{spec} 是 surface 的 diffuse 和 specular color,l_kI_{L_k} 則分別代表著第 k 盞燈光的方向與 intensity。

傳統的 deferred shading 大致將上式的運算分解成下列步驟:

  1. render 場景內透明的物件,並將所需的參數存至 G-buffer (ex. position, normal, albedo, roughness, … etc)。
  2. render 一個 image plane (screen space quad),在 pixel shader 中透過 texture sampler 讀取所需的參數並完成上面 shading 的計算。
  3. 按照 back-to-front 的順序,render 場景中透明的物件。

現在,light/object 的相依性已經從原先的 O(MN) 轉變至 O(N),也就是說,現在運算複雜度主要會卡在步驟 2 的 fill-rate。其效率主要受到兩個因素所影響:傳輸 G-buffers 所需的 bandwidth 與燈光的個數 N 。

G-buffers 所儲存的資訊和場景中那 M 個 material 有著密切的關係,當 M 個 material 的 reflection model 過於複雜時,G-buffers 必然要儲存更多的資訊,否則便無法在步驟 2 中完成相對應的 shading 計算。因此,降低 bandwidth 的方式便是針對場景內 M 個 material 的內容作進一步的調整與簡化,試圖在 visualization 和 computation 之間作個取捨。而 G-buffers 裡各參數的配置也要作更詳盡的規劃,使其盡可能滿足 MRT 的 layout 以便增進步驟 1 中 G-buffers 的輸出效率。

有了 G-buffers 所提供的 material 資訊之後,便可在步驟 2 中搭配 lights 的各項參數完成 shading 的計算。為了要在一個 pixel shader 中同時支援 directional lights, spot lights 和 point lights 的運算,直覺的作法便是在 pixel shader 中用 branch 分派運算的內容,並盡可能在同一個 pass 處理複數盞燈(因為一旦 N 的數目變大,accumulate 的成本便隨之提升)。此外,為了增進 fill-rate ,對於 spot lights 和 point lights 等具範圍性的光源,尚可以搭配 stencil buffer 來濾掉一些燈光所照射不到的 pixels ,盡可能地降低 pixel shader 的負擔。

Light Indexed Deferred Lighting

既然,material 的複雜度會嚴重影響到 bandwidth,那何不反過來,把參數複雜度較為固定的 light 資訊存進 buffers 中呢?LIDL [Damian Trebilco] 的概念就是作了一個這樣的嘗試,不過它不是在每個 shading pixel 裡直接紀錄 light 的參數(ex. position, color, attenuation, … etc),而是先賦予每個 light 一個獨立的 index,將其記錄在每個 shading pixel 中,然後透過這 index 去另一批參數 buffers 擷取 light 的參數。主要的步驟大致如下:

  1. render depth。
  2. 將每盞燈影響的範圍(light volume)  render 至 light index texture。
  3. 用 forward rendering 的方式 render geometry,從 light index texture 擷取參數並完成 lighting 的運算。

步驟 1 是為了讓步驟 2 能免去 hidden surface 造成的運算浪費。步驟 2 主要的目的則是記錄每個 pixel 會被哪些燈光所影響,降低後續 lighting 計算方面的開銷。儘管現在需要 cache 的參數相對降低,但在步驟 2 中有時會發生 light volume 重疊的情況。RGBA 共可以記錄 4 個 indices,倘若每個 pixel 受到超過 4 盞光源的照射,一種作法是按照 priority 篩選出 4 盞燈光,或是增加 buffer 來記錄更多組 index(但除非每個 pixel 都被超過 4 個的光源所照射,不然在一般情況下,這方式恐怕不會帶來令人滿意的效益)。

除了 light index texture 之外,還需要另一批 textures 來儲存每盞燈光的各項參數。根據燈光種類的不同,各參數於 textures 中的配置會是另一項議題。若參數儲存配置不得宜,一樣會造成 bandwidth 的瓶頸。要舒緩這部份的問題,還是需要做出取捨,去調整與簡化 lighting 運算的實作才行。

Pre-Lighting / Light Pre-Pass Render

為了降低傳統 deferred shading 於 bandwidth 的瓶頸,pre-lighting/light pre-pass render 的觀念也在近幾年應運而生[Wolfgang Engel] [CryEngine3]。和以往最大不同的地方是它把 lights 相關的性質(ex. light color, attneuation, N.L, …etc),再進一步從 shading 公式中抽離出來:

L_o(v)=c_{diff}*\sum_{k=1} ^n{f_{diff}(v,n,I_{L_k})} + c_{spec}*\sum_{k=1}^n{f_{spec}(v,n,l_k,I_{L_k})}

  1. render 非透明的物件,輸出 normal, depth 等資訊。
  2. render 一個 screen quad pass,將 lights 的屬性輸出至 light buffers。
  3. 再 render 一次場景內所有不透明的物件,搭配 light buffers 的各項資訊完成每個 material 的 shading 計算。
  4. render 場景內透明的物件。

這方式最大的優點是有效降低參數傳遞的 bandwidth,但是它最大的代價是需要兩次 geometry pass (步驟 1 和 3 )。此外,步驟 2 輸出的資訊可能無法充分滿足 material 的運算式,進而造成 material 設計彈性上的犧牲(像是 roughness 在步驟 2 中便無法得到確切的數值,替代方案是用定值或是從步驟 3 中 reconstruct 出 R.V 再運算 roughness 的影響)。此法的中心思想是降低 bandwidth,所以步驟 1 和 2 輸出的資訊自然也不能為了滿足 material 的運算而過度膨脹,若是 bandwidth 沒有達到某種程度的節約,第二次的 geometry pass 就會顯得不太值得,變成無謂的額外開銷。

以上,便是我目前為止對 deferred shading  粗淺的認識。基本上,deferred shading 的目的就是要去除 invisible surface 的 shading 浪費,同時並降低 light 與 object 之間的耦合度,使其在 M 個 material 相異的物件與 N 盞 lights 的情況下,盡可能地提升 render 的效率。各種形式的 deferred shading 都沒有絕對的優劣高下,所以在實際應用的過程中,還是免不了要在彼此的優缺點中不斷地作出權衡與取捨…

References

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s