天天看點

GraphicsLab Project之基于實體的着色系統(Physical based shading)-直接光照

引言

近些年來,基于實體的光照着色系統(Physical based shading)越來越流行。主流的渲染器,遊戲引擎都支援了這種着色方式。相比于以前的Phong和Blinn-Phong的光照着色模型,這種着色方式更加真實,也更加容易适應不同場景。接下來的幾篇文章,我将從實作的角度分析如何設計實作一個這樣的光照系統。主要仿照Unreal4的實作方式來進行實作。

基于實體的着色

那麼,在我們進行實際的編碼之前,先大緻的了解下什麼是基于實體的着色。所謂基于實體的着色,就是根據實體世界中光與材質的互動方式,近似的提出一種方案來模拟這種效果。讀者需要注意,這種方式是“基于實體的”,并不是“實體精确的”。它不是真實的再現現實世界中光與物質的互動,而是把握它們互動的主要特性,通過數學近似的方法來模拟。一個基于實體的着色,需要有如下三個基本的條件:

1.基于微表面理論模組化的反射模型 

2.能量守恒 

3.基于實體的BRDF 

(除此之外,還有什麼互相性等等,不過本篇文章主要關注以上三個條件)

渲染方程

我們接下來的實作,将主要圍繞一個方程來展開。這個方程總結了想要渲染出一個場景的一般性步驟,這個方程稱為渲染方程(針對于實時渲染來說,又可以稱之為反射方程): 

Lo=∫Ωf(pi,wi,wo)Li(pi,wi)n⋅widwi

Lo=∫Ωf(pi,wi,wo)Li(pi,wi)n⋅widwi

下面簡要的介紹下公式中每一個部分的意思: 

LoLo:表示的是經過着色之後,從wowo方向觀察點pipi時的顔色

f(pi,wi,wo)f(pi,wi,wo):表示的是點pipi上,從wowo方向反射出去的光照與從wiwi方向的入射的光照的比值,該函數稱為BRDF(Bidirectional Reflection Distribution Function) 

Li(pi,wi)Li(pi,wi):從wiwi方向入射到pipi點上的光照 

n⋅win⋅wi:表示的是光照的反射強度與光照方向與接受光照表面法線方向之間角度的關系,即Lambert Law 

∫Ω…dwi∫Ω…dwi:表示的是在點pipi法線方向上的半球,所有入射光線方向的積分 

光與材質的互動

當一束光線照射到材質表面的時候,會分成兩個主要的部分,反射的光線和折射的光線。 

對于折射的部分,由于物質本身的特性,一部分會被吸收,一部分會在物質本身的粒子之間來回碰撞,直至又從物體表面反射回去。 

從物體内部再次反射出去的光線,是不規則的。想要精确的模拟這種情況,需要用到次表面散射的技術(Subsurface scattering)。引入這種技術,需要非常複雜的計算,消耗過多,是以業界一般使用漫反射(Diffuse)來進行模拟。 

是以,當處理一束光照射到物質表面的時候,需要分成了兩個不同的反射部分進行處理,分别是漫反射和鏡面反射。是以渲染方程就變成了如下的形式: 

Lo=∫Ω(fd+fs)Li(n⋅wi)dwi

Lo=∫Ω(fd+fs)Li(n⋅wi)dwi

也就是将原先的BRDF函數分為了兩個不同的部分:漫反射部分和鏡面反射部分。 

直接光照處理理論

由于本篇文章處理直接光照,也就是隻處了解析光源,比如平行光,點光源這樣的光源,是以渲染方程能夠再一次的簡化成為如下的形式: 

Lo=(fd+fs)Li(n⋅wi)

Lo=(fd+fs)Li(n⋅wi)

這種簡化是因為對于單一解析光源來說,物體表面上同一個點隻會從一個方向接受到來自光源的光照,是以不需要進行積分計算。對于多個解析光源,我們也隻要簡單的分别對光源進行上述公式的計算,然後疊加結果即可。

漫反射部分

前面說過,反射方程主要分為了兩個部分,其中一個為漫反射,它模拟的是從物體内部反射出去的光線效果。業界有很多模拟漫反射的方法,如經典的Lambertian Reflection模型和Oren-Nayar模型等。在Unreal4中采用的是Lambertian Reflection模型,這種模型十分簡單,很容易計算出來,消耗非常小,并且也能夠得到很好的結果。我的實作也将采用這個模型。 

Lambertian Reflection模型實際上假設物體内部反射出來的光線,是均勻的分布在半球上的,是以這種模型又被稱之為Perfect Diffuse Reflection,如圖所示: 

而該模型的BRDF如下: 

fd=cπ

fd=cπ

其中c為該物體材質的Albedo屬性,和Phong模型中的Diffuse Color/Texture類似。而除以ππ的原因是為歸一化BRDF,進而使得BRDF符合能量守恒的條件,不會導緻反射出去的光線比入射的光線還要多的情況。 

采用Lambertian Reflection漫反射模型是不是非常的簡單,它和以前的Phong十分的類似,唯一不同之處就在于多了一個歸一化的操作。而這就是以前經驗式的光照模型,與基于實體的光照模型之間的差別。 

至此,我們的渲染方程變成了如下的模樣: 

Lo=(kd⋅cπ+fs)Li(n⋅wi)

Lo=(kd⋅cπ+fs)Li(n⋅wi)

(kd為漫反射部分所占的入射光照的比例,後文會詳細解釋)

鏡面反射部分

上面講解完畢了漫反射部分之後,接下來了解下鏡面反射部分。從前面的描述我們了解到,基于實體着色系統的漫反射光照和普通的Phong式的光照差别不大,那麼鏡面反射了?實際上,基于實體着色系統主要就是提高了鏡面反射部分的細節,能夠通過鏡面反射模拟出更加真實的物體表面屬性。 

在詳細了解具體的鏡面反射光照模型之前,我們先來了解下該模型提出的一個基本假設–微表面模型。 

微表面模型

以前的光照模型,都是假設被照射到的一個平面都是光滑的表面,但是實際上表面并不總是那麼光滑的。現實世界中,物體的表面從粗糙到光滑都存在,而粗糙的表面具有更加多的細節。微表面模型就是描述了一個表面粗糙程度的模型。 

我們可以假設,被光照到的一塊表面區域,實際上是由很多細小的光滑表面組成,如下圖所示: 

表面越粗糙,那麼微表面分布就越加的崎岖不平;表面越光滑,微表面分布就越加的平整,如下圖所示: 

是以,我們這裡要講解的鏡面反射光照模型就是根據此微表面模型來構想的。根據不同的輸入,模拟不同程度的微表面分布情況。 

Cook-Torrance鏡面反射光照模型

目前業界經常使用的鏡面光照模型就是Cook-Torrance鏡面反射光照模型。實際上,Cook-Torrance鏡面反射光照模型不是單一的模型,而是一種光照模型架構,它描述了一些基本的構成部分,模型可以自己根據需要來選擇合适的公式。 

下面就是Cook-Torrance的整體公式: 

fs=D⋅F⋅G4⋅(n⋅l)(n⋅v)

fs=D⋅F⋅G4⋅(n⋅l)(n⋅v)

D項

D項是一個Normal Distribution Function(NDF)函數,這個函數描述了微表面法線的分布情況。當越多的微表面法線與宏觀表面法線相近的時候,高光區域就越亮,亮斑就越小,反之就越暗,亮斑逐漸擴散。這個函數傳回的是一個标量。 

F項

F項模拟的是Fresnel效果。從前面的光與材質互動一節我們知道,光照射到表面的時候,分為了兩個不同的部分,反射(鏡面反射部分)和折射(漫反射部分)。那麼這兩個部分是如何分布的了,每個部分應該占據多少比例了?這個就是由F項來确定。大體上,當我們觀察到的視角向量vv與表面的法相向量nn之間夾角越小的時候,折射部分就越多,反射就越小;反之就折射越少,反射越多。這個效果你可以想象,當你垂直的觀察水面的時候,你能夠很清晰的看到水底的東西,而很難看到水面反射的東西;當你接近與水面平行的時候,你能夠很清晰的看到水面反射的物體,而很難看到水底的物體。是以,F項描述了鏡面反射部分所占的比例。同時需要知道,不同波長的光線,照射到同一個物體表面的時候,反射和折射的比例并不總是相同的,是以F項實際上是一個RGB三元分量。 

G項

G項,即Geometry Function,又稱之為Shadow and Mask項。當一束光線照射到表面的時候,由于表面的微表面特征,有一些光線會被其他表面遮擋住(Shadow);同時,當照射到微表面上之後,一些被反射出去的光線也會被其他光線給遮擋住(Mask)。是以就使用這個項來描述那些遮擋住光線的微表面占所有微表面的比值情況,是以這個G項也是一個标量。 

函數選取

接下來,我們就依次的講解Cook-Torrance三個函數D,F,G函數的選擇情況。這裡還是以Unreal4來舉例說明。 

D項

對于D項,我們選取的函數是GGX,該函數如下所示: 

D(n,h,α)=α2π((n⋅h)2(α2−1)+1)2

D(n,h,α)=α2π((n⋅h)2(α2−1)+1)2

其中: 

nn:表面的法向向量 

hh:half向量,該向量可以通過l+v∣∣l+v∣∣l+v∣∣l+v∣∣計算得到 

αα:表面的粗糙值,通過表面的Roughness屬性計算得來 

F項

前面我們知道,F項描述的是鏡面反射部分的比例。精确的模拟Fresnel效果的方程十分複雜,業界一般使用如下的近似函數來模拟: 

F(n,v,F0)=F0+(1−F0)(1−(n⋅v))5

F(n,v,F0)=F0+(1−F0)(1−(n⋅v))5

其中: 

nn:表面的法向向量 

vv:觀察向量 

F0F0:表面的基本反射屬性 

這裡有一個需要注意的地方,既然F項描述的是鏡面反射部分的比例,那麼漫反射部分的比例自然就是1 - F了。大部分情況下,的确是這樣。但是對于金屬材質的物體來說,光照射到物體表面,一部分被反射,一部分被折射,而被折射的部分很快的被材質給吸收了,是以并沒有從内部出去。 

是以,對于金屬材質來說,隻有鏡面反射,沒有漫反射。也就是說,這裡的F項,需要差別對待金屬材質和非金屬材質。 

為了能夠讓我們的算法能夠更容易的處理這種情況,我們引入了一個屬性Metallic,用來描述表面的金屬度。同時,最好能夠讓算法統一的處理金屬材質和非金屬材質。 

下圖,是不同材質的F0F0值: 

觀察上圖,我們得到金屬材質的F0F0值普遍高于0.5,而非金屬材質的F0F0值都小于0.17。也就說,這個非金屬材質的F0F0值集中在一個非常小的範圍裡面,是以我們就使用一個平均值來近似的表示非金屬材質的F0F0屬性,這個值為0.04。也就是說,對于非金屬材質來說,我們就直接認定F0=0.04F0=0.04。由于金屬表面沒有漫反射部分,是以它的外觀主要由鏡面反射部分來表現,是以我們保留它的F0F0值。 

是以,當我們在使用這裡的F項函數的時候,需要計算一下它需要使用的F0F0值。 

G項

對于G項,我們使用的是Schlick-GGX函數,這個函數和前面的GGX很類似,如下所示: 

GSchlickGGX(n,v,α)=n⋅v(n⋅v)(1−k)+k

GSchlickGGX(n,v,α)=n⋅v(n⋅v)(1−k)+k

其中: 

k=α2

k=α2

nn:表面法相向量 

vv:觀察或者光照向量 

αα:表面的粗糙程度屬性,由表面的Roughness屬性計算得來。 

同時由于G項描述了Shadow和Mask兩中情況,是以最終的G項為: 

G(n,v,l,α)=GSchlickGGX(n,v,α)⋅GSchlickGGX(n,l,α)

G(n,v,l,α)=GSchlickGGX(n,v,α)⋅GSchlickGGX(n,l,α)

新渲染方程

是以,現在渲染方程變成了: 

Lo=(kd⋅cπ+D⋅F⋅G4⋅(n⋅l)(n⋅v))Li(n⋅wi)

Lo=(kd⋅cπ+D⋅F⋅G4⋅(n⋅l)(n⋅v))Li(n⋅wi)

直接光照處理實作

理論到這裡為止了,接下來将以代碼的形式給出最終的實作。 

算法的輸入

該光照算法,需要如下幾個輸入參數: 

其中, 

Albedo:對于非金屬材質,描述的就是基本顔色屬性,相當于以前的Diffuse Texture,不過去除了Diffuse Texture中的陰影和高亮,隻有純粹的顔色屬性;對于金屬材質來說,由于前面說過的,金屬材質沒有漫反射部分,而金屬材質需要一個F0F0參數,是以就用它來描述金屬材質的F0F0屬性。在前面那張表中也可以看出,金屬材質的F0F0參數的各個分量是不一樣的,是以剛好可以使用三分量來表示。 

Normal:就是表面的法線資訊 

Metallic:表面的金屬度,用來表示表面是一個金屬還是非金屬。從這句話的描述來看,它應該是一個二進制的值,要們是金屬,要麼不是金屬。理論上的确是這樣的,但是在實際設計的過程中,把這個值設計為0到1範圍的一個值。這麼做的原因是為了模拟那些金屬表面覆寫了一層其他非金屬材質的情況,比如金屬材質上的鏽迹,由于它既不完全是金屬,也不完全是非金屬,是以使用一個介于他們之間的值來表示更加合理,同時能夠模拟兩種材質疊加的效果。 

Roughness:物體表面的粗糙屬性,從0到1變化,0表示最平滑,1表示最粗糙。 

Fresnel效果計算

由于F項表述了漫反射和鏡面反射的比例關系,是以最先計算出這個值來,如下代碼計算:

vec3 F0 = mix(vec3(0.04, 0.04, 0.04), albedo, metalic);

vec3 F = calc_frenel(h, v, F0);

第一句就是計算出合适的F0F0值出來,如果是非金屬材質,那麼就使用0.04來表示;如果是金屬材質,就是用albedo裡面存放的F0F0屬性;如果是介于之間的,就使用金屬度進行插值。 

第二句就是簡單的帶入Fresnel公式,不過有一個注意點。細心的讀者可能發現了,這裡我帶入的公式裡面使用的是h(half-way)向量,而不是表面的法線n。這是因為,前面我們在讨論Fresnel公式的時候,都是脫離微表面模型,直接從一個光滑表面的角度去看這個公式的。對于一個光滑表面來說,的确就是使用法線n來計算Fresnel系數。但是在這裡,我們使用了微表面模型,而微表面本身并不是一個完全光滑的表面,它是有很多具有一定分布形式的微型光滑表面所構成。對于微型的光滑表面,由于它們都是完美的光滑表面,是以隻有那些入射光線經過反射然後進入觀察者眼中的表面才能夠對宏觀表面的外在表現作出貢獻,而這些能夠做出貢獻的微型表面就滿足了如下的條件: 

reflect(l,n)=v

reflect(l,n)=v

也就是說,隻有具有滿足如上條件法線n的微型表面才是有效的。由此我們得出了這個n: 

n=l+v∣∣l+v∣∣=h

n=l+v∣∣l+v∣∣=h

(關于half-way向量的說明,大家可以參考Blin-Phong模型) 

calc_fresnel代碼如下:

vec3 calc_frenel(vec3 n, vec3 v, vec3 F0) {

    float ndotv = max(dot(n, v), 0.0);

    return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - ndotv, 5.0);

}

單單看Fresnel效果的計算的話,效果如下所示,你可以根據此圖來看看你的計算是否正确: 

漫反射比例

由于F項表示的就是鏡面反射的比例,那麼剩下的就是漫反射的比例,再考慮金屬材質沒有漫反射部分的情況,得到如下的代碼:

vec3 T = vec3(1.0, 1.0, 1.0) - F;

vec3 kD = T * (1.0 - metalic);

關于公式中αα的計算方式

前面的D項和G項,都有關于αα值的使用,而在注解中,我提到他們的計算方式需要通過Roughness屬性得來,而不是直接使用Roughness。這樣做的原因是,我們希望Roughness本身能夠保持一個線性的變化,而各函數中αα并不線性變化的,是以對于這兩個函數來說,我們需要将Roughness重新映射到αα上去。 

D項

采用Disney的映射方式: 

α=Roughness∗Roughness

α=Roughness∗Roughness

最終得到如下的代碼:

float calc_NDF_GGX(vec3 n, vec3 h, float roughness) {

    float a = roughness * roughness;

    float a2 = a * a;

    float ndoth = max(dot(n, h), 0.0);

    float ndoth2 = ndoth * ndoth;

    float t = ndoth2 * (a2 - 1.0) + 1.0;

    float t2 = t * t;

    return a2 / (PI * t2);

}

單單看這個函數的結果,根據不同的Roghness值,如下圖所示: 

G項

同樣采用Disney的映射方式: 

α=Roughnessremapping∗Roughnessremapping

α=Roughnessremapping∗Roughnessremapping

同時需要注意,上述使用的是RoughnessremappingRoughnessremapping,而不是直接使用Roughness,是以: 

Roughnessremapping=Roughness+12

Roughnessremapping=Roughness+12

是以最終,G項中的: 

k=(Roughess+1)28

k=(Roughess+1)28

是以關于G項的代碼如下所示:

float calc_Geometry_GGX(float costheta, float roughness) {
    float a = roughness;
    float r = a + 1.0;
    float r2 = r * r;
    float k = r2 / 8.0;

    float t = costheta * (1.0 - k) + k;

    return costheta / t;
}

float calc_Geometry_Smith(vec3 n, vec3 v, vec3 l, float roughness) {
    float ndotv = max(dot(n, v), 0.0);
    float ndotl = max(dot(n, l), 0.0);
    float ggx1 = calc_Geometry_GGX(ndotv, roughness);
    float ggx2 = calc_Geometry_GGX(ndotl, roughness);
    return ggx1 * ggx2;
}
           

單獨看這個函數的效果如下圖所示: 

線性空間

PBS強烈的依賴于你的計算空間中各個值是否是線性的,是以需要HDR和Gamma修正。我這裡使用了最簡單的Tone mapping計算,相關代碼如下:

// base tone mapping
color = color / (color + vec3(1.0, 1.0, 1.0));

// gamma correction
color = pow(color, vec3(1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2));
           
#version 330

in vec3 vs_Vertex;
in vec3 vs_Normal;

uniform vec3 glb_Albedo;
uniform float glb_Roughness;
uniform float glb_Metalic;
uniform vec3 glb_EyePos;

out vec3 oColor;

const float PI = 3.1415927;

vec3 calc_frenel(vec3 n, vec3 v, vec3 F0) {
    float ndotv = max(dot(n, v), 0.0);
    return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - ndotv, 5.0);
}

float calc_NDF_GGX(vec3 n, vec3 h, float roughness) {
    float a = roughness * roughness;
    float a2 = a * a;
    float ndoth = max(dot(n, h), 0.0);
    float ndoth2 = ndoth * ndoth;
    float t = ndoth2 * (a2 - 1.0) + 1.0;
    float t2 = t * t;
    return a2 / (PI * t2);
}

float calc_Geometry_GGX(float costheta, float roughness) {
    float a = roughness;
    float r = a + 1.0;
    float r2 = r * r;
    float k = r2 / 8.0;

    float t = costheta * (1.0 - k) + k;

    return costheta / t;
}

float calc_Geometry_Smith(vec3 n, vec3 v, vec3 l, float roughness) {
    float ndotv = max(dot(n, v), 0.0);
    float ndotl = max(dot(n, l), 0.0);
    float ggx1 = calc_Geometry_GGX(ndotv, roughness);
    float ggx2 = calc_Geometry_GGX(ndotl, roughness);
    return ggx1 * ggx2;
}

vec3 calc_lighting_direct(vec3 n, vec3 v, vec3 l, vec3 h, vec3 albedo, float roughness, float metalic, vec3 light) {
    vec3 F0 = mix(vec3(0.04, 0.04, 0.04), albedo, metalic);
    vec3 F = calc_frenel(h, v, F0);

    vec3 T = vec3(1.0, 1.0, 1.0) - F;
    vec3 kD = T * (1.0 - metalic);

    float D = calc_NDF_GGX(n, h, roughness);

    float G = calc_Geometry_Smith(n, v, l, roughness);

    vec3 Diffuse = kD * albedo * vec3(1.0 / PI, 1.0 / PI, 1.0 / PI);
    float t = 4.0 * max(dot(n, v), 0.0) * max(dot(n, l), 0.0) + 0.001;
    vec3 Specular = D * F * G * vec3(1.0 / t, 1.0 / t, 1.0 / t);

    float ndotl = max(dot(n, l), 0.0);
    return (Diffuse + Specular) * light * vec3(ndotl, ndotl, ndotl);
}

void main() {
    vec3 view = glb_EyePos - vs_Vertex;
    view = normalize(view);

    vec3 lightPos = vec3(0.0, 0.0, 200.0);
    vec3 light = lightPos - vs_Vertex;
    light = normalize(light);

    vec3 half = normalize(view + light);

    vec3 color = calc_lighting_direct(vs_Normal, view, light, half, glb_Albedo, glb_Roughness, glb_Metalic, vec3(2.5, 2.5, 2.5));

    // base tone mapping
    color = color / (color + vec3(1.0, 1.0, 1.0));

    // gamma correction
    color = pow(color, vec3(1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2));

    oColor = color;
}
           

總結

本文的執行個體代碼放在: 

https://github.com/idovelemon/GraphicsLabtory/tree/master/glbcodebase/graphicslab/glb_pbs 

這裡,感興趣的讀者可以自行下載下傳了解。

參考文獻

[1] LearnOpenGL 

[2] s2010_physically_based_shading_hoffman_notes 

[3] s2013_pbs_epic_notes 

[4] Ray Tracing From Ground Up 

[5] Physical based Rendering: From Theory to Implementation

————————————————

版權聲明:本文為CSDN部落客「i_dovelemon」的原創文章,遵循 CC 4.0 BY-SA 版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/i_dovelemon/article/details/78945950