D3D12學習筆記五:PBR光照入門
本節講述一下如何用D3D12實現最簡單的PBR光照,是我個人根據 LearnOpenGL CN 網站的 PBR光照一節中搬過來的,在龍書上並沒有這節,所以我把這節的例子程式碼上傳到了:
https://
github。com/wondeful18/D
3D12_PBR
,可以把整個目錄下載下來,並且在PBR1目錄找到sln檔案並雙擊開啟,在VS2017上編譯透過,大家可以對照著程式碼來除錯理解。
PBR的概念過於複雜,我就不嘗試解釋了,包括那些複雜的數學公式和推理等,完全理解這些概念應該挺耗費髮際線的,為了保護髮型,所以這裡我是直接用程式碼和圖示來試圖逐步理解它們。
我們從shader程式碼得出,PBR最重要的是這個公式:
如上圖,這個公式最重要的就是 DFG部分,這部分我們會重點講解一下:
D:正太分佈
這個函式是鏡面光照的關鍵相當於上一節的粗糙度函式,光滑的物體,光會集中在一點,並且亮度越大,而粗糙的物體,光會分散一些,亮度也相對沒有那麼大,如下圖:
如上圖是一個粗糙度在0。1到1。0的光照效果。正太分佈的公式如下:
如上圖,正太分佈主要傳入表面法線向量,中間向量和表面粗糙度的平方,並算出一個值,它的函式影象如下:
如上圖,粗糙度越小,光照越集中在一個小的角度,反射光的強度越大,粗糙度越大,則光越分散。它的shader程式碼如下:
// ——————————————————————————————————————
// 正態分佈函式:估算在受到表面粗糙度的影響下,取向方向與中間向量一致的微平面的數量。 DFG的D
float
DistributionGGX
(
float3
N
,
float3
H
,
float
roughness
)
{
float
a
=
roughness
*
roughness
;
float
a2
=
a
*
a
;
float
NdotH
=
saturate
(
dot
(
N
,
H
));
float
NdotH2
=
NdotH
*
NdotH
;
float
nom
=
a2
;
float
denom
=
(
NdotH2
*
(
a2
-
1。0f
)
+
1。0f
);
denom
=
3。14159265359f
*
denom
*
denom
;
return
nom
/
max
(
denom
,
0。0000001
);
}
我們修改一下例子程式的shader程式碼,讓只顯示D正太分佈函式的值,如下:
float
NDF
=
DistributionGGX
(
N
,
H
,
gRoughness
);
//Lo += (kD * albedo / PI + specular) * radiance * NdotL;
Lo
+=
(
float3
(
NDF
,
NDF
,
NDF
));
// 強制只顯示正太分佈函式結果
效果如下:
如上圖,正太分佈函式已經把鏡面光照的高光部分顯示出來了。
F:菲涅爾函式
菲涅爾函式在上一節也講過了,主要模擬的是在很小的角度觀察水面時,反射光較小,在很大的角度觀察水面時,反射光較大,如下圖:
這裡用的菲涅爾函式也和上一節基本一致,不過用的是 中間向量H 和 觀察向量V的夾角,如下圖:
公式如下:
它的函式影象如下:
它的程式碼如下:
// ——————————————————————————————————————
// 菲涅爾方程 當光線碰撞到一個表面的時候,菲涅爾方程會根據觀察角度告訴我們被反射的光線所佔的百分比
float3
fresnelSchlick
(
float
cosTheta
,
float3
F0
)
{
float
OneSubCos
=
1。f
-
cosTheta
;
return
F0
+
(
1。f
-
F0
)
*
OneSubCos
*
OneSubCos
*
OneSubCos
*
OneSubCos
*
OneSubCos
;
}
我們修改一下例子程式的shader程式碼,讓只顯示F菲涅爾函式的值,如下:
float3
F
=
fresnelSchlick
(
saturate
(
dot
(
H
,
V
)),
F0
);
//Lo += (kD * albedo / PI + specular) * radiance * NdotL;
Lo
+=
F
;
// 強制只顯示菲涅爾的結果
如上圖,球形的外圍,比中間亮一些。
當然。如果像上一節一樣,我把傳給菲涅爾方程的中間向量H和觀察向量V的夾角改成:法線N和V的夾角後,會更加明顯一些:
不過,這裡我依照LearnOpengl的教程,繼續用中間向量H和觀察向量V的夾角。
G:幾何遮蔽函式
最後讓我們來看看DFG的最後一個G幾何遮蔽函式,它主要是用來模擬微平面的一個點有沒有被遮住,它的大概效果如下:
如上圖,粗糙度大的物體,被遮蔽的地方會多一些,它的程式碼如下:
// ——————————————————————————————————————
// 幾何函式 GGX與Schlick-Beckmann近似的結合體
float
GeometrySchlickGGX
(
float
NdotV
,
float
roughness
)
{
float
r
=
(
roughness
+
1。0
);
float
k
=
(
r
*
r
)
/
8。0
;
float
nom
=
NdotV
;
float
denom
=
NdotV
*
(
1。0
-
k
)
+
k
;
return
nom
/
denom
;
}
// ——————————————————————————————————————
// 幾何函式 史密斯法(Smith’s method) 將觀察方向(幾何遮蔽(Geometry Obstruction))
// 和光線方向向量(幾何陰影(Geometry Shadowing))都考慮進去
float
GeometrySmith
(
float3
N
,
float3
V
,
float3
L
,
float
roughness
)
{
// 觀察方向
float
NdotV
=
max
(
dot
(
N
,
V
),
0。0
);
// 光線方向
float
NdotL
=
max
(
dot
(
N
,
L
),
0。0
);
// 觀察方向的幾何遮蔽
float
ggx2
=
GeometrySchlickGGX
(
NdotV
,
roughness
);
// 光線方向的幾何陰影
float
ggx1
=
GeometrySchlickGGX
(
NdotL
,
roughness
);
return
ggx1
*
ggx2
;
}
它的函式圖形如下,由於它是一個雙角度變數的二元方程,所以函式圖形是一個3D曲面:
如上圖,當觀察角度X,以及燈光角度Y都在0附近時,幾何遮蔽最少,亮度最亮,值最接近1。0。
我們修改一下例子程式的shader程式碼,讓只顯示G幾何遮蔽函式的值,如下:
float
G
=
GeometrySmith
(
N
,
V
,
L
,
gRoughness
);
//Lo += (kD * albedo / PI + specular) * radiance * NdotL;
Lo
+=
float3
(
G
,
G
,
G
);
// 強制只顯示幾何遮蔽值
效果如下:
如上圖,在只顯示幾何遮蔽的情況下,這個球也有一些立體感,中間凸出的部分亮一些。
把DFG三種效果最終組合起來的shader程式碼如下:
// 累計的光照
float3
Lo
=
float3
(
0。f
,
0。f
,
0。f
);
// 目前有4盞點燈
for
(
int
i
=
0
;
i
<
4
;
++
i
)
{
float3
LightPos
=
gLights
[
i
]。
Position
;
float3
L
=
normalize
(
LightPos
-
pin
。
PosW
);
float3
H
=
normalize
(
V
+
L
);
float
dis
=
length
(
LightPos
-
pin
。
PosW
);
// 距離衰減
float
attenuation
=
1。f
/
(
dis
*
dis
);
float3
radiance
=
gLights
[
i
]。
Strength
*
attenuation
;
// 正太分佈函式D
float
NDF
=
DistributionGGX
(
N
,
H
,
gRoughness
);
// 菲涅爾函式F
float3
F
=
fresnelSchlick
(
saturate
(
dot
(
H
,
V
)),
F0
);
// 幾何遮蔽函式G
float
G
=
GeometrySmith
(
N
,
V
,
L
,
gRoughness
);
float3
nominator
=
NDF
*
G
*
F
;
float
denominator
=
4
*
max
(
dot
(
N
,
V
),
0。0
)
*
max
(
dot
(
N
,
L
),
0。0f
);
// 鏡面光部分的值
float3
specular
=
nominator
/
max
(
denominator
,
0。001
);
// 鏡面光佔比為菲涅爾結果
float3
kS
=
F
;
// 散射光佔比:為了保持能量守恆,散射光佔比 和 鏡面光佔比之和不能超過1。0
float3
kD
=
float3
(
1。f
,
1。f
,
1。f
)
-
kS
;
// 金屬度越高,散射越少
kD
=
kD
*
(
1。f
-
gMetallic
);
float
NdotL
=
max
(
dot
(
N
,
L
),
0。0f
);
// 新增到最終Lo
Lo
+=
(
kD
*
albedo
/
PI
+
specular
)
*
radiance
*
NdotL
;
}
它的最終效果如下:
如上圖,從左到右粗糙度遞增,從上到下金屬度遞減。
我們的PBR入門教程就到這裡,未來我們會先學習如何在D3D12中載入紋理,以及法線貼圖之類,後續我們還會顯示附帶粗糙度貼圖和金屬度貼圖等更加逼真的PBR效果,敬請期待!
上一篇:孩子衣服上的油點子怎麼洗掉?