做者:i_dovelemonhtml
日期:2020-01-04git
主題:Rendering Equation,Irradiance Environment Map,Spherical Harmonicgithub
ChangeLog-2020/01/11: 添加 Light Probe Blend 相關描述wordpress
在實時圖形渲染中,Global Illumination 是聖盃級的效果。爲了實現這個效果,前輩們開發了不少的技術。可是這些技術大都只能用於靜態物體上,對於動態的物體卻不能很好的支持。因此,爲了讓動態的物體也有一點 GI 的效果,開發出了一系列的技術。今天,咱們就來介紹其中一種技術:Diffuse Irradiance Environment Map。在遊戲開發領域,通常稱之爲 Light Probe(注:固然 Light Probe 可以實現更多的效果,Diffuse 的 GI 是其中一種)。函數
Diffuse Irradiance Environment Map 是基於 Environment Map 來實現的。因此,它不會考慮陰影和模型自己的光照影響。同時,咱們也只探討光照中的 Diffuse 部分,即 Lambert BRDF 部分。this
文章中會存在大量的渲染相關的術語,諸如 irradiance,radiance,solid angle 等等。咱們假設你已經瞭解了這些基礎性的概念知識,若是不是,PBRT [文獻1] 是一個很好的參考資料。編碼
本文將主要從兩個方面來說述:一個是傳統的計算 Diffuse Irradiance Environment Map 的方法,咱們稱之爲 Brute force;另一種是基於 Spherical Harmonic 的方法。spa
咱們回顧下渲染方程,能夠知道一個點在半球範圍裏面受到的 irradiance 爲:.net
$E(\vec{n})=\int_{\Omega(\vec{n})}^{ }L(\vec{w})(\vec{n}\cdot\vec{w})d\vec{w} \ \ \ \ \ \ \ \ (1)$設計
也就是說,對於一個固定的 Environment Map (即 $L(\vec{w})$ 相同)來講,irradiance 只和 normal 有關。因此,咱們能夠經過預計算,將 Environment Map 對應的 Irradiance Environment Map 保存爲一個和 normal 相映射的形式,而後經過頂點的 normal 來獲取對應的 irradiance 信息。獲取到 irradiance 信息以後,帶入下面的公式,就可以獲得最終須要顯示的顏色值:
$B(\vec{p},\vec{n})=f(\vec{p})E(\vec{n}) \ \ \ \ \ \ \ \ (2)$
其中 $f(\vec{p})$ 表示的是 Diffuse 的 BRDF。
公式(1)中計算 irradiance 的方法,是一個在半球範圍裏面積分的形式,這種方式不存在解析解,沒有辦法直接去計算獲得。可是,因爲光照環境是經過 Environment Map 來表達的,咱們能夠將公式(1)轉化爲離散的形態,以下公式所示:
$E(\vec{n})=\sum_{i=0}^{N-1}L(\vec{w})(\vec{n}\cdot\vec{w})d(\vec{w})\ \ \ \ \ \ \ \ (3)$
其中,N 表示的是整張 Environment Map 上的全部像素的數量;$L(\vec{w})$ 表示的是在 $\vec{w}$ 方向上的 radiance;$d(\vec{w})$ 表示的是在 $\vec{w}$ 方向上像素的 solid angle。
這樣,咱們就有了一個方法來實際計算一個 normal 方向上的 irradiance 的值了。
咱們知道了如何計算一個 normal 對應的 irradiance 的值,那麼這個值該怎麼保存了?很明顯的,咱們能夠利用另一張 Cubemap 來保存各個 normal 計算出來的對應的 irradiance 的值,而這個新的 Cubemap 就是 Diffuse Environment Irradiance Map。如下是整個過程的僞代碼:
for pixel_iem in IrradianceEnvironmentMap n = GetNormal(pixel_iem) irradiance = 0 for pixel in EnvironmentMap L = GetRadiance(pixel) w = GetRadianceDir(pixel) dw = GetTexelSolidAngle(pixel) irradiance += L * max(0, dot(n,w)) * dw pixel_iem = irradiance
上面的代碼中,惟一可能比較難計算的是:GetTexelSolidAngle。[文獻2] 中詳細的解釋瞭如何定義這個函數,以及該函數背後的數學原理,這裏給出實際的代碼,再也不贅述:
private static float AreaElement(float x, float y) { return Mathf.Atan2(x * y, Mathf.Sqrt(x * x + y * y + 1.0f)); } private static float TexelCoordSolidAngle(float x, float y, int size) { // Scale up to [-1,1] range (inclusive), offset by 0.5 to point to texel center float u = 2.0f * (x + 0.5f) / size - 1.0f; float v = 2.0f * (y + 0.5f) / size - 1.0f; float invRes = 1.0f / size; // Project area for this texel float x0 = u - invRes; float y0 = v - invRes; float x1 = u + invRes; float y1 = v + invRes; return AreaElement(x0, y0) - AreaElement(x0, y1) - AreaElement(x1, y0) + AreaElement(x1, y1); }
咱們知道,solid angle 在整個球上的積分值爲 $4\pi$。前面咱們將公式(1)轉化成了離散的形式,這樣就致使全部像素的 solid angle 的總和與 $4\pi$ 存在必定的偏差,因此須要進行修正。修正以後的僞代碼以下所示:
for pixel_iem in IrradianceEnvironmentMap n = GetNormal(pixel_iem) irradiance = 0 totalSolidAngle = 0 for pixel in EnvironmentMap L = GetRadiance(pixel) w = GetRadianceDir(pixel) dw = GetTexelSolidAngle(pixel) irradiance += L * max(0, dot(n,w)) * dw totalSolidAngle += dw pixel_iem = irradiance * 4 * PI / totalSolidAngle
好了,至此咱們就獲得了一張 Diffuse Irradiance Environment Map。在渲染的時候,咱們只要經過像素的 normal 來採樣 Irradiance Environment Map 就能夠獲得對應的 irradiance。而後帶入公式(2)中,獲得最終須要顯示的顏色值。
這裏有一個容易引發困惑的地方。咱們知道,Lambert 光照模型的 BRDF 以下所示:
$f = \frac{c_{diff}}{\pi}\ \ \ \ \ \ \ \ (4)$
而有遊戲開發經驗的同窗就知道,在遊戲裏面咱們定義 Diffuse 的光照模型以下所示:
$B=c_{diff}*c_{light}*max(0,dot(\vec{n},\vec{w}))\ \ \ \ \ \ \ \ (5)$
這裏卻沒有 $\pi$ 相關的值。這是由於在傳統的遊戲裏面,咱們定義的 $c_{light}$ 並非以光學輻射度的單位來定義的,而是以一種對美術更加友好的定義方式:當一個純白的 Lambert 表面被一束平行於表面 normal 的光所照射時所呈現的顏色爲 $c_{light}$。也就是說,傳統遊戲開發中定義的 $c_{light}$,其實是真實光學輻射度單位輸入除以 $\pi$ 以後的結果,因此公式(5)中就不存在 $\pi$。
說這麼多的意思是,咱們定義 Environment Map 是以真實的輻射度單位來保存的,也就是說在計算最終顏色的時候,咱們須要本身除以 $\pi$ 來保證結果的正確性,即將公式(4)帶入公式(2)中計算最終的顏色值,即:
$B=\frac{c_{diff}}{\pi}E(\vec{n})\ \ \ \ \ \ \ \ (6)$
這裏爲了簡化 shader 中的計算,咱們將 $\pi$ 的計算放在了 Diffuse Irradiance Environment Map 裏,即:
for pixel_iem in IrradianceEnvironmentMap n = GetNormal(pixel_iem) irradiance = 0 totalSolidAngle = 0 for pixel in EnvironmentMap L = GetRadiance(pixel) w = GetRadianceDir(pixel) dw = GetTexelSolidAngle(pixel) irradiance += L * max(0, dot(n,w)) * dw totalSolidAngle += dw pixel_iem = irradiance * 4 * PI / totalSolidAngle pixel_iem = pixel_iem / PI
關於 $\pi$ 的詳細討論能夠參考[文獻3]。
如下是一些經過 BruteForce 方法計算出來的 Diffuse Irradiance Environment Map 和原始 Environment Map 的對比結果圖,Diffuse Irradiance Environment Map 大小是 32x32:
Spherical Harmonic 是信號處理裏面的一種變換方法。和 Fourier 變換類似,都是將信號轉化到頻域中去,以此來更加精簡的表達原始複雜的信號。不一樣的是,Spherical Harmonic 更加適合用來處理球面相關的信號。而渲染相關的問題,都是在一個球面範圍裏面進行,因此選擇使用 SH 的方法。關於 SH 的描述,[文獻4] 講解的很是詳細,這裏就再也不贅述。神奇的地方在於,BruteForce 的方法獲得的是最終的 Diffuse Irradiance Environment Map,而 SH 的方法獲得的是 SH 係數(通常是9個係數)。而後在實際渲染的時候,咱們根據這9個係數,重建原始的信號,獲得對應的 irradiance。
根據[文獻5]中的描述,咱們知道若是使用 SH coefficient 的表示方法來編碼 Environment Map 的話,將使用以下的公式:
$L(\theta,\phi)=\sum_{l,m}^{ }L_{lm}Y_{lm}(\theta,\phi)\ \ \ \ \ \ \ \ (7)$
而一樣的,使用 SH 編碼 Irradiance Environment Map 的話,將使用以下的公式:
$E(\theta,\phi)=\sum_{l,m}^{ }E_{lm}Y_{lm}(\theta,\phi)\ \ \ \ \ \ \ \ (8)$
同時定義:
$A=(\vec{n}\cdot\vec{w})$
$A(\theta)=\sum_l^{ }A_lY_{l0}(\theta)\ \ \ \ \ \ \ \ (9)$
根據上面的定義,咱們獲得:
$E_{lm}=\sqrt{\frac{4\pi}{2l+1}}A_lL_{lm}\ \ \ \ \ \ \ \ (10)$
引入新的變量:
$\hat{A}_l=\sqrt{\frac{4\pi}{2l+1}}A_l\ \ \ \ \ \ \ \ (11)$
將公式(9)(10)(11)帶入公式(8),獲得:
$E(\theta,\phi)=\sum_{l,m}^{ }\hat{A}_lL_{lm}Y_{lm}(\theta,\phi)\ \ \ \ \ \ \ \ (12)$
公式(12)中,$\hat{A}_l$是能夠預先計算出來的,$Y_{lm}(\theta,\phi)$ 經過帶入 normal,也可以計算出來,只有 $L_{lm}$ 是未知的。因此,咱們 Prefilter 操做的目的就是計算出 $L_{lm}$ 的值。
[文獻5]中講述了咱們只須要3階的 SH 係數,就可以很好的表達信號,因此咱們只須要計算出來 $l <= 2$ 的 SH 係數便可。
根據文獻[4]中的描述,計算 SH 係數的方式就是將信號投影到對於的基向量上去便可,即:
$L_{lm}=\int_{\Omega}^{ }L(\vec{w})Y_{lm}(\vec{w})d(\vec{w})\ \ \ \ \ \ \ \ (13)$
一樣的,咱們將這個積分形式的方程,轉化爲離散的形式[文獻6],以下所示:
$L_{lm}=\sum_{i=0}^{N-1}L(\vec{w})Y_{lm}(\vec{w})d(\vec{w})\ \ \ \ \ \ \ \ (14)$
這樣,咱們就可以經過計算,獲得3階球諧的9個係數。可是咱們知道光的單位是有RGB三個部分組成,每個部分能夠單獨的進行 SH 係數的求解,因此最終的結果是9個RGB係數。如下是求解這些係數的僞代碼:
foreach sh_coefficient sh_coefficient = 0 totalSolidAngle = 0 for pixel in EnvironmentMap L = GetRadiance(pixel) sh = GetSHBais(pixel) dw = GetTexelSolidAngle(pixel) sh_coefficient += L * sh * dw totalSolidAngle += dw sh_coefficient = sh_coefficient * 4 * PI / totalSolidAngle
計算過程十分簡單,惟一須要注意的點是GetSHBais 函數的實現。這個函數的定義能夠經過預先計算獲得,以下所示[文獻5]:
至此,Prefilter 的工做就完成了。
經過公式(12),咱們在知道了 SH 係數的狀況下,就能夠重建原始的 irradiance 信號。因爲公式(12)中只有 $L_{lm}$ 是未知的,其餘兩個部分都是能夠經過預計算獲得,因此合併預計算的部分,咱們獲得根據 SH 係數重建信號的公式[文獻5]:
這樣,在知道了 irradiance 的狀況下,帶入到公式(2)中,獲得:
$B=\frac{c_{diff}}{\pi}E\ \ \ \ \ \ \ \ (15)$
如下是經過 SH 的方法獲得的 Diffuse Irradiance Environment Map 和原始 Environment Map 的對比:
上面的描述中,咱們都是從一個特定的點去繪製環境貼圖,而後以此環境貼圖爲周圍光照的 radiance 描述來構建 light probe。對於 reflection environment map 來講,咱們能夠經過簡單的數學計算,就可以獲得一個 localized 的效果(見文獻[8])。可是對於 light probe 來講,因爲須要在半球範圍裏面進行積分運算,沒法使用文獻[8]中描述的方式實現 localized 的效果。可是,咱們能夠經過對兩張在不一樣的點生成的 light probe 進行插值,以此來構建他們之間某個點的 light probe,從而實現必定程度上的 localized 的效果。
對於 SH 表達的 light probe 來講,在兩個 light probe 之間進行插值就是簡單對 SH coefficient 進行插值便可(見文獻[9]):
固然在實際的遊戲開發中,你不可能只對兩個 light probe 進行插值。爲了較好的表達整個場景的效果,咱們須要在場景中擺放不少個 light probe。而後根據被渲染物體所在的位置選擇一個或多個 light probe,根據必定的權重進行插值。這部分的知識也十分複雜,能夠參考文獻[9]中的具體描述。文獻[9]描述了常見的系統設計方案,以及它所存在的問題,同時給出了 Unity 對此進行的改進和它所使用的方案。感興趣的讀者能夠去了解。
咱們假設原始 Environment Map 的尺寸是 NxNx6,而 BruteForce 方法計算獲得的 Diffuse Irradiance Environment Map 的尺寸爲 MxMx6,那麼對於 BruteForce 的方法來講,就是一個 O(NxNxMxM) 的操做。而對於 SH 方法來講,它的計算時間爲 O(9xNxN)。兩個方法在Prefilter上面,SH 的速度大大提升。同時,對於 BruteForce 方法來講,獲得的結果是一張 Cubemap,在渲染的時候須要進行採樣,而 SH 的方法則是經過一些簡單的計算獲得最終的結果。
如下是兩種方式獲得的 Diffuse Irradiance Environment Map 的對比:
能夠看到,經過 SH 方式獲得的結果和 BruteForce 的方法獲得的結果偏差很是小。
若是在實際使用過程當中,你須要使用 Diffuse Irradiance Environment Map,也是建議經過先求 SH 係數,而後重建 Diffuse Irradiance Environment Map,這樣的方法比 BruteForce 來計算獲得 Diffuse Irradiance Environment Map 的速度要快的多。
固然除了這裏提到的方法,還有不少其餘的方法來計算 Diffuse Irradiance Environment Map,好比[文獻7]中,使用 Rieman 積分的方式,加速 BruteForce 方法來獲得結果。
這裏只是介紹了基礎的知識,在實際項目開發過程當中還須要處理諸多的問題,好比:Light Probe Auto Layout,Light Probe Blend 等等複雜的問題,後面有機會會專門講解這方面的知識。
本文的配套代碼能夠在這裏獲取獲得:https://github.com/idovelemon/UnityProj/tree/62eff639347645f380d651dd80b8720010f6097b/IrradianceEnvironmentMap。值得注意的是,學術界對球面座標系的定義是 Z 軸朝上,而在 Unity 裏面是 Y 軸朝上,實際代碼實現的時候須要轉化下方向。
[1] Physically Based Rendering : From Theory to Implementation
[3] PI or not to PI in game lighting equation
[4] Spherical Harmonic Lighting:The Gritty Details
[5] An Efficient Representation for Irradiance Environment Maps
[6] Real-Time Computation of Dynamics Irradiance Environment Maps
[7] GraphicsLab Project 之 IBL - Diffuse 光照
[9] Light Probe Interpolation using Tetrahedral Tessellations
原文出處:https://www.cnblogs.com/idovelemon/p/12150757.html