前言
在前面的章節中,我們學到了法線貼圖和曲面細分,現在我們可以將這兩者進行結合以改善效果,因為法線貼圖僅僅只是改善了光照的細節,但它并沒有從根本上改善幾何體的細節,從某種意義上來說,法線貼圖只是一個光照的小把戲,接下來我們將會學習如何使用位移貼圖來改善網格細節,
在此之前你需要了解如下章節:
| 章節 |
|---|
| 25 法線貼圖 |
| 33 曲面細分階段(Tessellation) |
學習目標:
- 了解位移貼圖
- 熟悉如何用曲面細分來改善網格細節
DirectX11 With Windows SDK完整目錄
Github專案原始碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
位移貼圖(Displacement Mapping)
位移貼圖的想法是利用一個額外的貼圖,稱作高度圖,它描述了一個表面的凸起和縫隙,換句話說,法線貼圖有三個顏色通道來為每個像素產生法線向量(x, y, z),而高度圖僅僅由一個顏色通道來為每個像素產生高度值h,從視覺上來看,高度圖僅僅是一張灰度圖(因為灰度圖只有一個顏色通道),每個像素可以解釋成一個高度值,它基本上是一個2D標量場的離散表示h = f(x, z),當我們對網格進行曲面細分時,我們在域著色器對高度圖進行采樣,然后利用法線的方向來對頂點進行偏移,以此來增加網格的幾何體細節,
盡管我們通過鑲嵌來對幾何體增加三角形,但是它并沒有增加其本身的細節,那是因為如果你對三角形進行多次細分,你只是獲得了更多的和原來的三角形同屬于一個平面的三角形,為了增加細節(如凸起和縫隙),你需要以某種方式來偏移這些經過鑲嵌后得到的頂點,高度圖是其中一個座位輸入的紋理資源,它可以用來對鑲嵌后的頂點進行偏移,通常情況下,我們會用到下面的公式,為此我們還需要用到法線貼圖采樣出來的法向量來確定偏移的方向:
\[\mathbf{p'}=\mathbf{p}+s(h-1)\mathbf{n} \]
其中標量h∈[0, 1]是從高度圖得到的高度值,我們對高度值減1來讓區間[0, 1]→[-1, 0],因為表面的法向量通常是面向網格的外部,這意味著我們以向內偏移的方式來替代向外偏移,一般將幾何體彈入會比將幾何體拉出更為方便一些,標量s則是用來控制在世界空間的塌陷程度,這樣高度值的將從[0, 1]→[-s, 0],即高度值最大的時候將不會有向內的偏移,而高度值最小的時候將會產生最大的向內偏移,通常我們會將高度圖存放在法線貼圖中的alpha通道,


生成高度圖是一項藝術性的作業,紋理藝術家可以繪制它們,或者使用工具來產生(例如:crazybump)
位移貼圖的著色器代碼
位移貼圖的代碼主要在頂點著色器、外殼著色器和域著色器有所變化,像素著色器則和我們之前使用了法線貼圖的版本一樣無需改動,
圖元型別
為了將位移貼圖整合到我們的渲染當中,我們需要曲面細分的支持,這樣我們就可以細化我們的幾何解析度,使得他能夠與位移貼圖更好地匹配,接下來我們將使用圖元型別D3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST而不是D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST來繪制我們的網格三角形,通過這種方式,三角形的三個頂點將解釋成三角形面片的3個控制點,以允許我們來對每個三角形進行鑲嵌,
頂點著色器
當我們處理曲面細分的時候,我們必須決定每個三角形的細分程度,這里我們將引入一個簡單的距離量來確定細分數目,若三角形離攝像機越近,它的細分程度越大,頂點著色器通過計算每個頂點和攝像機之間的距離來幫助我們計算出曲面細分因子,然后將其傳遞給外給著色器,
在常量緩沖區中,我們引入了下面這些資料來控制距離的計算,這些值的設定非常依賴于場景(你的場景有多大,以及你想要怎樣的細分程度):
cbuffer CBChangesEveryFrame
{
// ...
float g_HeightScale;
float g_MaxTessDistance;
float g_MinTessDistance;
float g_MinTessFactor;
float g_MaxTessFactor;
}
g_MaxTessDistance:從攝像機到該頂點的距離拉近到某個閾值時,將會達到最大的曲面細分因子g_MinTessDistance:從攝像機到該頂點的距離拉遠到某個閾值時,將會達到最小的曲面細分因子g_MinTessFactor:曲面細分因子的最小值,比如說你想讓每個三角形無論距離攝像機多遠,都要讓它最少被鑲嵌成3份g_MaxTessFactor:曲面細分因子的最大值,比如說你想讓這些三角形無論距離攝像機多近,它最多的鑲嵌份數不超過6.此外,回想起上一章所提到的建議,鑲嵌后的三角形如果少于8個像素將會變得低效,
此外我們應該留意到g_MaxTessDistance < g_MinTessDistance,因為隨著頂點距離我們的攝像機越近,鑲嵌的份數將會越多,
使用這些變數,我們就可以創建一個關于距離的線性函式來決定如何根據距離來確定鑲嵌的份數,
// DisplacementMapObject_VS.hlsl
#include "Basic.hlsli"
// 頂點著色器
TessVertexOut VS(VertexPosNormalTangentTex vIn)
{
TessVertexOut vOut;
vOut.PosW = mul(float4(vIn.PosL, 1.0f), g_World).xyz;
vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
vOut.TangentW = mul(vIn.TangentL, g_World);
vOut.Tex = vIn.Tex;
float d = distance(vOut.PosW, g_EyePosW);
// 標準化曲面細分因子
// TessFactor =
// 0, d >= g_MinTessDistance
// (g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance), g_MinTessDistance <= d <= g_MaxTessDistance
// 1, d <= g_MaxTessDistance
float tess = saturate((g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance));
// [0, 1] --> [g_MinTessFactor, g_MaxTessFactor]
vOut.TessFactor = g_MinTessFactor + tess * (g_MaxTessFactor - g_MinTessFactor);
return vOut;
}
// DisplacementMapInstance_VS.hlsl
#include "Basic.hlsli"
// 頂點著色器
TessVertexOut VS(InstancePosNormalTangentTex vIn)
{
TessVertexOut vOut;
vOut.PosW = mul(float4(vIn.PosL, 1.0f), vIn.World).xyz;
vOut.NormalW = mul(vIn.NormalL, (float3x3) vIn.WorldInvTranspose);
vOut.TangentW = mul(vIn.TangentL, vIn.World);
vOut.Tex = vIn.Tex;
float d = distance(vOut.PosW, g_EyePosW);
// 標準化曲面細分因子
// TessFactor =
// 0, d >= g_MinTessDistance
// (g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance), g_MinTessDistance <= d <= g_MaxTessDistance
// 1, d <= g_MaxTessDistance
float tess = saturate((g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance));
// [0, 1] --> [g_MinTessFactor, g_MaxTessFactor]
vOut.TessFactor = g_MinTessFactor + tess * (g_MaxTessFactor - g_MinTessFactor);
return vOut;
}
外殼著色器
回想上一章說的,常量外殼著色器對每個面片進行計算,并且它的任務是要輸出該面片的曲面細分因子,曲面細分因子將告訴鑲嵌器階段對該面片以怎樣的程度來進行鑲嵌處理,曲面細分因子計算的大部分作業是由頂點著色器所完成的,但仍有一部分的作業需要交給常量外殼著色器處理,特別地,我們通過對頂點曲面細分因子進行求平均值的方式來得到邊緣的曲面細分因子,至于內部的曲面細分因子,我們就隨意挑選了第一條邊的曲面細分因子,
PatchTess PatchHS(InputPatch<TessVertexOut, 3> patch,
uint patchID : SV_PrimitiveID)
{
PatchTess pt;
// 對每條邊的曲面細分因子求平均值,并選擇其中一條邊的作為其內部的
// 曲面細分因子,基于邊的屬性來進行曲面細分因子的計算非常重要,這
// 樣那些與多個三角形共享的邊將會擁有相同的曲面細分因子,否則會導
// 致間隙的產生
pt.EdgeTess[0] = 0.5f * (patch[1].TessFactor + patch[2].TessFactor);
pt.EdgeTess[1] = 0.5f * (patch[2].TessFactor + patch[0].TessFactor);
pt.EdgeTess[2] = 0.5f * (patch[0].TessFactor + patch[1].TessFactor);
pt.InsideTess = pt.EdgeTess[0];
return pt;
}
那些與多個三角形所共享的邊應當擁有相同的曲面細分因子,否則可能會出現網格三角形間的縫隙(見下圖),舉個例子說下不計算曲面細分因子的情況,加入我們通過攝像機到三角形中心點的距離來計算內部曲面細分因子,然后我們將內部的曲面細分因子也設定到邊緣曲面細分因子上,如果兩個鄰接三角形擁有不同的內部曲面細分因子,它們的邊也將會擁有不同的曲面細分因子,從而導致在進行位移映射后會產生T型連接的縫隙效果,

可以看到,圖a展示了兩個三角形共享一條邊,圖b上面的三角形進行了一次邊緣細分,下面的三角形則沒有細分,圖c上面的三角形進行了一次內部細分,經過位移映射后,新產生的頂點被移走了(一般是向內移動),從而在兩個三角形之間產生了一條縫隙,
控制點外殼著色器以面片的控制點作為輸入,每次呼叫處理一個控制點并輸出,在本章示例專案中,控制點外殼著色器僅僅是將資料進行直傳:
// 外殼著色器
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("PatchHS")]
HullOut HS(InputPatch<TessVertexOut, 3> patch,
uint i : SV_OutputControlPointID,
uint patchId : SV_PrimitiveID)
{
HullOut hOut;
// 直傳
hOut.PosW = patch[i].PosW;
hOut.NormalW = patch[i].NormalW;
hOut.TangentW = patch[i].TangentW;
hOut.Tex = patch[i].Tex;
return hOut;
}
域著色器
經過鑲嵌器創建出來的每個頂點都會有呼叫一次域著色器,在這里我們將會對高度圖(即法線貼圖的Alpha通道部分)進行采樣,然后利用法向量對頂點偏移,從而完成整個位移映射的程序,
// DisplacementMap_DS.hlsl
#include "Basic.hlsli"
[domain("tri")]
VertexOutNormalMap DS(PatchTess patchTess,
float3 bary : SV_DomainLocation,
const OutputPatch<HullOut, 3> tri)
{
VertexOutNormalMap dOut;
// 對面片屬性進行插值以生成頂點
dOut.PosW = bary.x * tri[0].PosW + bary.y * tri[1].PosW + bary.z * tri[2].PosW;
dOut.NormalW = bary.x * tri[0].NormalW + bary.y * tri[1].NormalW + bary.z * tri[2].NormalW;
dOut.TangentW = bary.x * tri[0].TangentW + bary.y * tri[1].TangentW + bary.z * tri[2].TangentW;
dOut.Tex = bary.x * tri[0].Tex + bary.y * tri[1].Tex + bary.z * tri[2].Tex;
// 對插值后的法向量進行標準化
dOut.NormalW = normalize(dOut.NormalW);
//
// 位移映射
//
// 基于攝像機到頂點的距離選取mipmap等級;特別地,對每個MipInterval單位選擇下一個mipLevel
// 然后將mipLevel限制在[0, 6]
const float MipInterval = 20.0f;
float mipLevel = clamp((distance(dOut.PosW, g_EyePosW) - MipInterval) / MipInterval, 0.0f, 6.0f);
// 對高度圖采樣(存在法線貼圖的alpha通道)
float h = g_NormalMap.SampleLevel(g_Sam, dOut.Tex, mipLevel).a;
// 沿著法向量進行偏移
dOut.PosW += (g_HeightScale * (h - 1.0f)) * dOut.NormalW;
// 生成投影紋理坐標
dOut.ShadowPosH = mul(float4(dOut.PosW, 1.0f), g_ShadowTransform);
// 投影到齊次裁減空間
dOut.PosH = mul(float4(dOut.PosW, 1.0f), g_ViewProj);
// 從NDC坐標[-1, 1]^2變換到紋理空間坐標[0, 1]^2
// u = 0.5x + 0.5
// v = -0.5y + 0.5
// ((xw, yw, zw, w) + (w, w, 0, 0)) * (0.5, -0.5, 1, 1) = ((0.5x + 0.5)w, (-0.5y + 0.5)w, zw, w)
// = (uw, vw, zw, w)
// => (u, v, z, 1)
dOut.SSAOPosH = (dOut.PosH + float4(dOut.PosH.ww, 0.0f, 0.0f)) * float4(0.5f, -0.5f, 1.0f, 1.0f);
return dOut;
}
這里值得注意的是,我們需要在域著色器中自行選擇mipmap等級,像素著色器中的方法Texture2D::Sample在域著色器中是不能使用的,所以我們必須使用Texture2D::SampleLevel方法并手工指定mipmap等級,
如果我們只是學了法線貼圖的話,到這里基本上就了解的差不多了,但如果學了陰影映射和SSAO的話,那么這里就又多了兩個坑要填了,如果我們用了位移映射來繪制,那么在繪制陰影的時候,也一樣要走一遍位移映射;對于SSAO來說也更是如此,如果不對SSAO寫入深度值的程序加入位移映射,那么在正式繪制場景的時候就會因為像素深度值不一致而被剔除,從而導致了在運行龍書的SSAO Demo時,在開啟了位移映射之后,那些擁有法線貼圖的物體都沒有被畫出來的現象:

所以接下來做的事情就是體力活了,把DisplacementMap從頂點著色器到域著色器的實作原理也要搬運到繪制陰影圖的程序,以及在SSAO繪制法向量/深度緩沖區順便寫入深度/模板緩沖區的程序中,因為代碼上高度相似,這里我就只是列出本章新增的著色器檔案串列:
| BasicEffect | SSAOEffect | ShadowEffect |
|---|---|---|
DisplacementMapObject_VS |
SSAO_NormalDepth_ObjectTess_VS |
ShadowObjectTess_VS |
DisplacementMapInstance_VS |
SSAO_NormalDepth_InstanceTess_VS |
ShadowInstanceTess_VS |
DisplacementMap_HS |
SSAO_NormalDepth_HS |
Shadow_DS |
DisplacementMap_DS |
SSAO_NormalDepth_DS |
Shadow_HS |
C++端代碼實作
在本章中,與位移映射直接相關的類有BasicEffect、SSAOEffect、ShadowEffect類,都是在前面的基礎上作的修改,然后GameObject的Draw也為此有所修改,GameApp類承擔了實作程序,和SSAO的相比繪制框架的變動比較小,這里就不放出修改的部分了,讀者可以自行瀏覽,
網格細節問題
首先要注意的是,我們是對頂點進行位移映射,如果網格的三角形比較大,比如說只有4個頂點的地板,經過曲面細分后能生成的新頂點也比較有限,位移映射的效果就不明顯,為此,我們需要增大網格模型的頂點密集程度,意味著我們增大了高度圖的采樣點數目,以讓我們能夠逼近真實的地形,如果我們不走曲面細分,那我們就需要提前準備三角形密集的網格資料,這樣需要占用比較多的顯存或記憶體,但即便是用了曲面細分,我們要權衡初始網格的頂點密集程度,以及經過曲面細分后的頂點密集程度如何,
在本章的示例中,我們的地面不再使用Geometry::CreatePlain,而是用Geometry::CreateTerrain來創建出更加精細的地面網格,由于一開始寫的Geometry::CreateCylinder它的側面三角形比較大,曲面細分后的頂點數目也不夠密集,為此我已經修改了它的實作,讓側面能夠支持分層的三角形,
演示
下面的動圖展示了基礎繪制、法線貼圖繪制、位移貼圖繪制下的區別,以及曲面細分前后網格的區別,

而下面的動圖則展示了不同的HeightScale下位移映射的效果,

DirectX11 With Windows SDK完整目錄
Github專案原始碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/27780.html
標籤:其他
