2020年5月19日 星期二

Image Based Lighting (3)


  • 前言

經過 Image Based Lighting (1)Image Based Lighting (2) 的研究與討論,

本篇將展現實作結果並和 Unity 內建的 Standard Shader 做分析與比較。


  • 比對與分析

我使用 Unity 內建的 Standard Shader 作為對照組,

將兩組使用同樣的渲染參數並排比對,

檢視在 metallic = 0 和 = 1 時的 roughness 變化的結果 (註1),

環境光源貼圖使用我在 Image Based Lighting (1) 提到的下圖 cubemap





















上排是 Standard Shader 、 下排是 Image Based Lighting (之後簡稱 IBL) 渲染 shader。

當 metallic = 0 時,roughness(粗糙度)由左至右提升的比對結果

當 metallic = 1 時,roughness(粗糙度)由左至右提升的比對結果


Unity 對於環境光照的實作是參考 "Stupid Spherical Harmonics (SH) Tricks." [1] 的計算,

使用前3階的 Table of spherical harmonics (球諧函數表) 共9個球諧係數來計算環境光照。

比較結果可以看的出來使用 IBL 的環境光照效果會比較強,所以整體顏色會比較飽和,

反射效果也比較明顯。


  • 實作程式碼

主要是接續 淺談 physically based rendering (4) 的程式碼,新增間接光照的處理。

其中

float3 DiffuseIrradiance(float3 albedo, float3 normalDirection, float3 F0, float NdotV, float roughness)
{
    float3 F90 = max(1.0 - roughness, F0);
    float3 fresnel = F0 + (F90 - F0) * Schlick(NdotV);

    float3 refract = 1.0 - fresnel;
    float3 kD = refract * (1.0 - _metallic);

    float3 irradiance = texCUBE (_IrradianceD, normalDirection).rgb;
    irradiance = irradiance;

    return kD * irradiance * albedo;
}

是計算間接光照的 Diffuse 部分,可參考 Image Based Lighting (1)


float3 SpecularIBL(float3 reflectDirection, float3 F0, float NdotV, float roughness)
{
    float3 PrefilteredColor = UNITY_SAMPLE_TEXCUBE_LOD(_PrefiliterEnv, reflectDirection, roughness * UNITY_SPECCUBE_LOD_STEPS).rgb;

    float2 EnvBRDF = tex2Dlod(_IntegrateBRDF, float4(NdotV, roughness, 0.0, 0.0)).rg;
    return PrefilteredColor * (F0 * EnvBRDF.x + EnvBRDF.y);
}

上段程式碼是計算間接光照的Specular 部分,可參考 Image Based Lighting (2)


float3 Approx_ACES(float3 linearCol)
{
    float a = 2.51;
    float b = 0.03;
    float c = 2.43;
    float d = 0.59;
    float e = 0.14;
    return clamp((linearCol*(a*linearCol+b))/(linearCol*(c*linearCol+d)+e), 0.0, 1.0);
}

float3 ACES_tone_mapping(float3 linearCol)
{
    const float exposureBias = 2;
    return Approx_ACES(linearCol * exposureBias); 
}

最後的這一段是計算 Tone Mapping,可參考 Tone Mapping (色調映射)



  • Mobile 運作效果

在 Mobile 上,為了效能考量, 我限制 IBL pre-compute 的輸出結果,

將 prefilter Enviorment map 設定為 512x512 的 cubamp ,並包含 5 個 Mipmap,

BRDF integrate map 也是限制為 512x512,

Diffuse Irradiance 則因為效果不明顯,我使用 32x32 解析度來增加效能。

在 Sony L3 運作並使用 ApowerREC 錄製視頻。


左手邊為 Unity Standard Shader,右手邊為本篇所展示的 Shader。

  • References
1. Peter-Pike Sloan. "Stupid Spherical Harmonics (SH) Tricks." Presented at Game Developers 
    Conference, San Francisco, CA, February 18-22, 2008.


  • 附註
1. 在 Unity Standard Shader 是使用 Smoothness (平滑度) 作為 microfacet 崎嶇參數,
    和 roughness 的關係是 roughness  = 1 - Smoothness 。


2020年5月18日 星期一

Image Based Lighting (2)


  •  前言
在上一篇,我將 rendering equatiom 分成了 Diffuse 和 Specular 兩個部分,



並 pre-compute(預先計算) Diffuse 部分,將結果儲存在一個 cubemap 內。

本篇會承續上一篇未處理的 Specular 部分,討論如何實作 pre-compute,

以減少 real-time 渲染的負擔。


  • Specular BRDF

首先將 Specular 的部分使用在 淺談 physically based rendering (4) 

提到的 Cook-Torrance Model 展開,


其中:

ks 是物體的反射率(註1)

在這裡,可以觀察到反射的結果會受到兩個變數的影響,

 ωi (入射光方向) 和 ωo (觀察者方向)。

對這兩種的所有組合做積分是不實際的。

因此,勢必要想辦法減少變數的數量,才能達到實用的階段。

  • split sum approximation
首先,使用 Monte Carlo method (蒙地卡羅積分法) 對 Specular BRDF 做積分,



其中

p : 是 probability density function (簡稱 pdf)

lk : 使用 importance sampling (重點採樣) 計算出來的反射光方向樣本。

根據 [2]、[3] 得知,


pdf 代入積分式,得到



如此,原來的積分式就被分成了兩個部分


前面的部分為 Integral of BRDF (簡稱 DFG 項)

後面的部分為 Integral of Lighting (簡稱 LD 項)

這種將積分式拆開後分別處理,

最後再將結果一起計算的方法稱之為 split sum approximation [2]。



  • Integral of BRDF

觀察 DFG 項,可以發現變數的數量還是很多,

因此需將算式整理,看看是否能把變數數量壓到最小。

首先為了簡化算式,根據 淺談 physically based rendering (4) 提到的效能優化技巧,

將 G 項和分母結合使其成為 V 項。



接下來展開 F 項,

在 淺談 physically based rendering (4) 我是使用 Fresnel-Schlick approximation 實作F項。



將 F 項代入積分式,可以得到


由於同一個渲染物體  F0 是相同的,所以可視為一個常數。

到了這裡,已經將變數數量限制到一個可接受的範圍內,

half-vector 和 light-vector 可以用 importance sample 計算出來,

因此變數只剩下 roughness 和 view-vector 而已。

為了方便將 pre-compute 結果作為一個 lookup table 儲存在一張 texture 上,

需要將變數範圍限制在 [0,1] 之間,

所以,在 pre-compute 時,將 roughness 和 NdotV (normal 和 view-vector 的內積)作為引數,

計算出 DFG1 term 和 DFG2 term 的積分結果,儲存在 R和G通道。

在 run-time 時,將物體的 F0、roughness 和 NdotV 資訊引入來計算出結果。



在 pre-compute 實作上,我是使用 Unity 的 Compute Shader 來處理,

代碼如下


函數 ImportanceSampleGGX 和 Hammersley

可以參考 importance sampling (重點採樣)Low-discrepancy sequence (準亂數列)

這兩篇文章的討論。

比較值得注意的是在 淺談 physically based rendering (4) , G 項的 K值我是使用



但在 IBL 的 pre-compute , K 值我是使用 k = α / 2。

主要原因是參考了 [2] 的說明,認為 k = α / 2 在 IBL的計算上可以得到一個比較好的結果。

執行後,會得到下面的一個圖片(註1)



U軸為 NdotV, V軸為 roughness


  • Integral of Lighting

接下來觀察 LD 項,可以發現和在 Image Based Lighting (1) 裡,

做 Diffuse 的 pre-compute 很類似,都是收集周遭環境光源做積分運算。

但不同的是,為了簡化 Specular BRDF 積分式的變數數量,

使用 importance sampling 去找出取樣點,分割成 LD 項和 DFG 項。

因此,不能像處理 Diffuse 一樣,

對 Spherical Coordinates (球面座標系) 的  θ 和 φ 做等間隔取樣,

必須用 importance sampling 回傳的取樣點,去做積分計算。


但這裡發現了一個問題,在 LD 項只有 importance sampling 回傳的取樣點法線向量,

光靠法線向量是無法得到周遭環境光源的光線資訊(註2)。

為了解決這個問題,令 normal = view = reflect [2],

 importance sampling 回傳的取樣點法線向量,可以直接取得光線資訊。


圖片來源 : Learn OpenGL - Specular IBL

根據上圖可以觀察到,如果假設 normal = view = reflect 時,

反射結果會變得模糊但仍然可以保持原始輪廓。

[2] 為了減少假設帶來的失真,發現可以在 LD term 算式內,

加入 NdotL (normal dot light-vector) 作為權重值,來近似原始效果。


在 pre-compute 實作上,我同樣是使用 Unity 的 Compute Shader 來處理,

代碼如下

其中,要注意的是 LD term 算式存在著 roughness 這個變數,

根據 [5] 的文章,可以將 roughness 分成數個階段,

將每個階段的結果存儲在 cubemap 的 mipmap 裡面。

在實作上,我是分成5個階段 (i.e. roughness = 0, 0.25, 0.5, 0.75, 1),

由於 roughness 越高,光線越模糊,

因此根據 roughness 的程度由小到大依序儲存在 cubmap 的 minmap 裡。



  • 結論
到了這裡,已經完全說明了 IBL 的基本原理,

在下一篇會展示渲染結果並和 Unity 內建的 standard shader 做個比較和分析。


  • References



  • 附註
1.在 [2] 文章裡面,是使用 R16G16 的圖片格式來儲存 Integral of BRDF 的資料,
   主要是認為精準度會對渲染結果有很大的影響。
   但在我的實作中,因為考慮到 mobile 裝置支援性,選擇了傳統的 RGB24格式,
   這部分可以視情況做修改。

2.本篇是延續  Image Based Lighting (1) 使用 Environment Map (環境貼圖)
   作為周遭的環境光源,因此需要反射光向量來取得光線資訊。

2020年5月12日 星期二

Image Based Lighting (1)


  • 前言
本章節是接續 淺談 physically based rendering 裡面未提到的多方向光源渲染的實作方法。

由於在 real-time 去計算所有方向的光源會是一個極大的開銷。

因此通常的作法是將環境內所有方向的光線情報 pre-compute(預先計算) ,

將結果儲存在一張影像圖(通常為一個 Cubemap)。

在渲染物體時,從這張影像圖去取得光線資料。

這種作法又稱作 Image Based Lighting (以後簡稱 IBL)。

  • rendering equation
回頭複習 rendering equation (渲染方程式)[1],


物體的渲染是由物體的自發光(emitted)和來源光線涵蓋整個半球的反射光所組成的。

在 淺談 physically based rendering (2) 裡,

解釋到反射光可以分解成 Diffuse 和 Specular。

因此,反射光方程式可以改寫成



在預先計算光線情報時,也拆分成 Diffuse 和 Specular 來處理。

本篇接下來將集中處理 Diffuse 的部分。


  • Diffuse irradiance
將焦點集中在 Diffuse 的部分,



根據 淺談 physically based rendering (3) 的討論,

我使用 Normalized Lambert 作為 Diffuse BRDF。

所以方程式可以改寫成:


其中:

kd : refraction ratio (折射率)

c : Albedo

對同一個物體來說,其 kd 和 c 值都是固定,可視為一常數值。

因此,在預算 Diffuse 部分,只要對積分部分作處理即可。

積分部分可以改寫成:



其中 θ 和 φ 的定義如下圖所示

圖片來源 LearnOpenGL


使用在 importance sampling (重點採樣) 提到的 Spherical Coordinates (球面座標系)。

積分的實作上,則使用 Monte Carlo method (蒙地卡羅積分法) 最後提到的 Riemann Sum。



  • Unity 實作
由於是預先處理,且需要大量的計算。

所以我使用 Unity 的 Compute Shader 來實作 Diffuse BRDF 的預算。

首先,準備一張 Environment Map (環境貼圖),作為周遭的環境光源。

素材來源 HDRIHaven

接著在 Compute shader 內,對 θ 和 φ 每增加 0.025 就做一次取樣。



因為 Diffuse 資訊不需要太詳細,

因此我將結果儲存在一個 32x32 的 Cubemap 裡。


到此,就完成了 Diffuse BRDF 的預算處理。


  • 結論

就如同 淺談 physically based rendering (3) 的討論,

Diffuse BRDF 的影響並不如 Specular BRDF 來的明顯,

因此整體的計算所使用的演算法並不複雜。

但在計算 Specular BRDF 時,為了確保採樣的精準度,將會使用稍微複雜的演算法來處理。

下一篇將繼續討論並實作 Specular BRDF 的部分。



  • Reference
1. Kajiya, James T., The rendering equation (PDF), Siggraph 1986, 1986: 143–150, ISBN 978-0-89791-196-2, doi:10.1145/15922.15902

2. Learn OpenGL - Diffuse irradiance







2020年5月11日 星期一

Tone Mapping (色調映射)



色彩是人眼接收到光線後,會對其波長和振幅的不同而產生出來的反應。

根據 International Commission on illumination (國際照明委員會,簡稱 CIE),

提出的 CIE 1931 x,y chromaticity diagram 所示: 


圖片來源 : wikipedia

人眼的可辨識色度(chromaticity)範圍類似一個馬蹄狀。


在計算機領域,通常會將顏色用 RGB 結構來儲存,

其方法是將顏色分成 3個部分(Red、Green、Blue),

並將每個部份的數值限制在 0~255。

RGB 結構可儲存的色度範圍就叫做 sRGB (standard Red Green Blue),

將 sRGB 和 CIE 1931 x,y chromaticity diagram 合併顯示,可以得到下圖:

圖片來源 : wikipedia

由上圖可以得知,sRGB 並沒有辦法涵蓋所有人眼可辨識的色度資訊


因此,為了要能夠涵蓋所有可識別的色度資訊,需要一個更大範圍的資料結構去儲存,

這個結構就稱為 High-dynamic-range (簡稱 HDR),

而只能儲存部分色度資訊的結構,就相對稱為 Low-dynamic-range (簡稱 LDR)。


既然 LDR 不夠完整,那只使用 HDR 作為色度資料資訊不就好了?

很遺憾的是不是所有裝置都支援 HDR。


在圖像處理上,為了將 HDR 的色度資訊能夠映射在 LDR 上,

所研究的演算法就叫做 Tone mapping。


在本篇文章,我並不打算對 Tone mapping 演算法去做分析與研究,

主要目的是要讀者理解 Tone mapping 的目的與接下來實作 Image Based Lighting 的關聯。

為了能夠將  Image Based Lighting 的渲染結果顯示在 LDR 的裝置上,

有需要做 Tone mapping 的轉換。


如果想了解 Tone mapping 演算法,個人推薦 Delta - Blog by 64 這個網誌,

上面有針對每個常見演算法的詳細介紹並附上實作代碼。


2020年5月1日 星期五

Low-discrepancy sequence (準亂數列)


  • 前言
在上一章介紹了如何使用一個2維變數 (ξ12)來得到一個 GGX 分布的法線向量,

在這裡衍生了一個問題,

要如何找到一個合適的2維變數陣列作為積分用的採樣點的法線向量。

本章節就是介紹使用 Low-discrepancy sequence (準亂數列) 

作為 Quasi Monte Carlo Method 取樣點的優點與實作方法。


  • Monte Carlo Method
Monte Carlo Method 是使用亂數取樣點作為積分計算的數列,

若使用 N 個樣本數,則他的積分誤差的收斂速度為 O(N−0.5)[1]

這代表著如果我要提升2倍的精度,我需要提供4倍的樣本數。


  • Quasi Monte Carlo Method
不同於 Monte Carlo Method 使用亂數列作為採樣點,

 Quasi Monte Carlo Method 使用了 Low-discrepancy sequence 作為採樣點,

使得其積分誤差的收斂速度為 O(log(N)dim/N), 其中 dim 為 dimension(次元數)。

這代表著如果在 2 維空間使用 Quasi Monte Carlo Method 做積分,

要提升2倍的精度,只需要提供2倍的樣本數。

因此可以得知若在低次元空間下做積分,

Quasi Monte Carlo Method 比 Monte Carlo Method 有著更高的收斂速度。


  • Hammersley Sequence
回到了一開始的問題,如何找到一個合適的2維變數陣列作為積分用的採樣點的法線向量。

在這裡就是使用 Quasi Monte Carlo Method,

利用 Low-discrepancy sequence 找出採樣點陣列做積分計算。

本章節接下來會說明並實作一個常用的 Low-discrepancy sequence,

也就是 Hammersley Sequence。


基本理論很簡單


我給定一個索引值 α,先將其轉為2進制,

再依序將2進制的每個位數由後往前依序放入小數點後,在由前往後乘以2的倒數,

就是回傳結果。

ex.

0.1     = 1 * 2−1 = 0.5
0.01   = 0 * 2−1 + 1 * 2−2 = 0.25
0.11   = 1 * 2−1 + 1 * 2−2 = 0.75
0.001 = 0 * 2−1 + 0 * 2−2 + 1 * 2−3= 0.125
...

如此可以得到一個在 [0,1] 數列。

下圖為  Hammersley Sequence 的2維採樣結果


從上圖可知,這是一個非隨機的數列,

但是它的結果分布卻是有著微小差異(不知情的人可能會認為他是一個隨機採樣結果)。

這種數列就叫做 Low-discrepancy sequence。


2維 Hammersley Sequence 的實作方式如下

float RadicalInverse_VdC(uint bits) 
{
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}

float2 Hammersley(uint i, uint N)
{
    return float2(float(i)/float(N), RadicalInverse_VdC(i));
}  



  • References
1. Søren Asmussen and Peter W. Glynn, Stochastic Simulation: Algorithms and Analysis, Springer, 2007, 476 pages