DirectX11 With Windows SDK--34 位移貼圖

前言

在前面的章節中,咱們學到了法線貼圖和曲面細分。如今咱們能夠將這二者進行結合以改善效果,由於法線貼圖僅僅只是改善了光照的細節,但它並無從根本上改善幾何體的細節。從某種意義上來講,法線貼圖只是一個光照的小把戲。接下來咱們將會學習如何使用位移貼圖來改善網格細節。html

在此以前你須要瞭解以下章節:git

章節
25 法線貼圖
33 曲面細分階段(Tessellation)

學習目標:github

  1. 瞭解位移貼圖
  2. 熟悉如何用曲面細分來改善網格細節

DirectX11 With Windows SDK完整目錄app

Github項目源碼框架

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。dom

位移貼圖(Displacement Mapping)

位移貼圖的想法是利用一個額外的貼圖,稱做高度圖,它描述了一個表面的凸起和縫隙。換句話說,法線貼圖有三個顏色通道來爲每一個像素產生法線向量(x, y, z),而高度圖僅僅由一個顏色通道來爲每一個像素產生高度值h。從視覺上來看,高度圖僅僅是一張灰度圖(由於灰度圖只有一個顏色通道),每一個像素能夠解釋成一個高度值,它基本上是一個2D標量場的離散表示h = f(x, z)。當咱們對網格進行曲面細分時,咱們在域着色器對高度圖進行採樣,而後利用法線的方向來對頂點進行偏移,以此來增長網格的幾何體細節。ide

儘管咱們經過鑲嵌來對幾何體增長三角形,可是它並無增長其自己的細節。那是由於若是你對三角形進行屢次細分,你只是得到了更多的和原來的三角形同屬於一個平面的三角形。爲了增長細節(如凸起和縫隙),你須要以某種方式來偏移這些通過鑲嵌後獲得的頂點。高度圖是其中一個座位輸入的紋理資源,它能夠用來對鑲嵌後的頂點進行偏移。一般狀況下,咱們會用到下面的公式,爲此咱們還須要用到法線貼圖採樣出來的法向量來肯定偏移的方向:函數

\[\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;
}
  1. g_MaxTessDistance:從攝像機到該頂點的距離拉近到某個閾值時,將會達到最大的曲面細分因子
  2. g_MinTessDistance:從攝像機到該頂點的距離拉遠到某個閾值時,將會達到最小的曲面細分因子
  3. g_MinTessFactor:曲面細分因子的最小值。好比說你想讓每一個三角形不管距離攝像機多遠,都要讓它最少被鑲嵌成3份
  4. 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++端代碼實現

在本章中,與位移映射直接相關的類有BasicEffectSSAOEffectShadowEffect類,都是在前面的基礎上做的修改。而後GameObjectDraw也爲此有所修改。GameApp類承擔了實現過程,和SSAO的相比繪製框架的變更比較小。這裏就不放出修改的部分了,讀者能夠自行瀏覽。

網格細節問題

首先要注意的是,咱們是對頂點進行位移映射。若是網格的三角形比較大,好比說只有4個頂點的地板,通過曲面細分後能生成的新頂點也比較有限,位移映射的效果就不明顯。爲此,咱們須要增大網格模型的頂點密集程度,意味着咱們增大了高度圖的採樣點數目,以讓咱們可以逼近真實的地形。若是咱們不走曲面細分,那咱們就須要提早準備三角形密集的網格數據,這樣須要佔用比較多的顯存或內存。但即使是用了曲面細分,咱們要權衡初始網格的頂點密集程度,以及通過曲面細分後的頂點密集程度如何。

在本章的示例中,咱們的地面再也不使用Geometry::CreatePlain,而是用Geometry::CreateTerrain來建立出更加精細的地面網格。因爲一開始寫的Geometry::CreateCylinder它的側面三角形比較大,曲面細分後的頂點數目也不夠密集,爲此我已經修改了它的實現,讓側面可以支持分層的三角形。

演示

下面的動圖展現了基礎繪製、法線貼圖繪製、位移貼圖繪製下的區別,以及曲面細分先後網格的區別。

而下面的動圖則展現了不一樣的HeightScale下位移映射的效果。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索