前言
陰影既暗示著光源相對于觀察者的位置關系,也從側面傳達了場景中各物體之間的相對位置,本章將起底最基礎的陰影映射演算法,而像復雜如級聯陰影映射這樣的技術,也是在陰影映射的基礎上發展而來的,
從本章開始我們將使用EffectHelper進一步簡化所撰寫的特效,為此你可能還需要了解下面的章節:
| 章節 |
|---|
| 深入理解Effects11、使用著色器反射機制(Shader Reflection)實作一個復雜Effects框架 |
學習目標:
- 掌味訓本的陰影映射演算法
- 熟悉投影紋理貼圖的作業原理
- 了解陰影圖走樣的問題并學習修正該問題的常用策略
DirectX11 With Windows SDK完整目錄
Github專案原始碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
核心思想
陰影映射技術的核心思想其實不復雜,對于場景中的一點,如果該點能夠被攝像機觀察到,卻不能被光源定義的虛擬攝像機所觀察到,那么場景中的這一點則可以被判定為光源所照射不到的陰影區域,
以下圖為例,眼睛觀察到地面上最左邊的一點,并且從光源處觀察也能看到該點,因此該點不會產生陰影,

再看下面的圖,眼睛可以觀察到地面上中間那一點,但是從光源處觀察不能看到該點,因此該點會產生陰影,

具體落實下來應該怎么做呢?對于點光源來說,由于它的光是朝所有方向四射散開的,但為了方便,我們可以像攝像機那樣選取視錐體區域(使用一個觀察矩陣 + 透視投影矩陣來定義),然后經過正常的變換后就能計算出光源到區域內物體的深度值;而對于平行光(方向光)來說,我們可以采用正交投影的方式來選取一個長方體區域(使用一個觀察矩陣 + 正交投影矩陣定義),和一般的渲染流程不同的是,我們只需要記錄深度值到深度緩沖區,而不需要將顏色繪制到后備緩沖區,
陰影貼圖
陰影貼圖技術也是一種變相的“渲染到紋理”技術,它以光源的視角來渲染場景深度資訊,即在光源處有一個虛擬攝像機,它將觀察到的物體的深度資訊保存到深度緩沖區中,這樣我們就可以知道那些離光源最近的像素片元資訊,同時這些點自然是不在陰影范圍之中,
通常該技術需要用到一個深度/模板緩沖區、一個與之對應的視口、針對該深度/模板緩沖區的著色器資源視圖(SRV)和深度/模板視圖(DSV),而用于陰影貼圖的那個深度/模板緩沖區也被稱為陰影貼圖,
光源的投影
在考慮點光源的投影和方向光的投影時可能會有些困難,但這兩個問題其實可以轉化成虛擬攝像機的透視投影和正交投影,
對于透視投影來說,其實我們也已經非常熟悉了,在這種做法下我們只考慮虛擬攝像機的視錐體區域(即盡管點光源是朝任意方向照射的,但我們只看點光源往該視錐體范圍內照射的區域),然后對物體慣例進行世界變換、以光源為視角的觀察變換、光源的透視投影變換,這樣物體就被變換到了以光源為視角的NDC空間,
而對于正交投影而言,我們也是一樣的做法,正交投影的視景體是一個軸對齊于觀察坐標系的長方體,盡管我們不好描述一個方向光的光源,但為了方便,我們把光源定義在視景體xOy切面中心所處的那條直線上,這樣我們就只需要給出視景體的寬度、高度、近平面、遠平面資訊就可以構造出一個正交投影矩陣了,

我們可以看到,正交投影的投影線均平行于觀察空間的z軸,

正交投影矩陣在第四章變換已經講過,就不再贅述,
投影紋理坐標
投影紋理貼圖技術能夠將紋理投射到任意形狀的幾何體上,又因為其原理與投影機的作業方式比較相似,由此得名,例如下圖中,右邊的骷髏頭紋理被投射到左邊場景中的多個幾何體上,

投影紋理貼圖的關鍵在于為每個像素生成對應的投影紋理坐標,從視覺上給人一種紋理被投射到幾何體上的感覺,
下圖是光源觀察的視野,其中點p是待渲染的一點,而紋理坐標(u, v)則指定了應當被投射到3D點p上的紋素,并且坐標(u, v)與投影到螢屏上的NDC坐標有特定聯系,我們可以將投影紋理坐標的生成程序分為如下步驟:
- 將3D空間中一點p投影到光源的投影視窗,并將其變換到NDC空間,
- 將投影坐標從NDC空間變換到紋理空間,以此將它們轉換為紋理坐標

而步驟2中的變換程序則取決于下面的坐標變換:
\[u=0.5x+0.5\\ v=-0.5y+0.5 \]
即從x, y∈[-1, 1]映射到u, v∈[0, 1],(y軸和v軸是相反的)
這種線性變換可以用矩陣表示:
\[\mathbf{T}=\begin{bmatrix} 0.5 & 0 & 0 & 0 \\ 0 & -0.5 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0.5 & 0.5 & 0 & 1 \\ \end{bmatrix}\\ \begin{bmatrix} x & y & 0 & 1 \end{bmatrix}\mathbf{T}=\begin{bmatrix} u & v & 0 & 1 \end{bmatrix} \]
那么物體上的一點p從區域坐標系到最終的紋理坐標點t的變換程序為:
\[\mathbf{p}\mathbf{W_{Obj}}\mathbf{V_{Light}}\mathbf{P_{Light}}\mathbf{T}=\mathbf{t} \]
這里補上了世界變換矩陣,是因為這一步容易在后面的代碼實踐中被漏掉,但此時的t還需要經過透視除法,才是我們最終需要的紋理坐標,
HLSL代碼
下面的HLSL代碼展示了頂點著色器計算投影紋理坐標的程序:
// 頂點著色器
VertexOutBasic VS(VertexPosNormalTex vIn)
{
VertexOutBasic vOut;
// ...
// 把頂點變換到光源的投影空間
vOut.ShadowPosH = mul(posW, g_ShadowTransform);
return vOut;
}
// 像素著色器
float4 PS(VertexOutBasic pIn) : SV_Target
{
// 透視除法
pIn.ShadowPosH.xyz /= pIn.ShadowPosH.w;
// NDC空間中的深度值
float depth = pIn.ShadowPosH.z;
// 通過投影紋理坐標來對紋理采樣
// 采樣出的r分量即為光源觀察該點時的深度值
float4 c = g_ShadowMap.Sample(g_Sam, pIn.ShadowPosH.xy);
// ...
}
視錐體之外的點
在渲染管線中,位于視錐體之外的幾何體是要被裁剪掉的,但是,在我們以光源設定的視角投影幾何體而為之生成投影紋理坐標時,并不需要執行裁剪操作——只需要簡單投影頂點即可,因此,位于視錐體之外的幾何體頂點會得到[0, 1]區間之外的投影紋理坐標,然后具體的采樣行為則需要依賴于我們設定的采樣器,
一般來說,我們并不希望對位于視錐體外的幾何體頂點進行貼圖,因為這并沒有任何意義,考慮到可視深度在NDC空間的最大值為1.0f,我們可以采用邊界深度值為1.0f的邊框尋址模式,
另一種做法則是結合聚光燈的策略,使聚光燈照射范圍之外的部分不受光照,亦即不在陰影的計算范圍內,
透視除法與投影的其他問題
來到正交投影,因為我們依然是要計算出NDC坐標,對于NDC空間范圍外的點,我們依然可以采用上面的尋址模式策略,但聚光燈的策略就不適用了,
此外,正交投影無需進行透視除法,因為正交投影后的坐標w值總是1.0f,但保留透視除法可以讓我們的這套著色器可以同時作業在正交投影和透視投影上,如果沒有透視除法,則只能在正交投影中作業,
演算法思路
- 從光源的視角將場景深度以“渲染到紋理”的形式繪制到名為陰影貼圖的深度緩沖區中
- 從玩家攝像機的視角渲染場景,計算出該點在光源視角下NDC坐標,其中z值為深度值,記為d(p)
- 上面算出的NDC坐標的xy分量變換為陰影貼圖的紋理坐標uv,然后進行深度值采樣,得到s(p)
- 當d(p) > s(p)時, 像素p位于陰影范圍之內;自然相反地,當d(p) <= s(p)時,像素p位于陰影范圍之外(至于為什么還有<,后面會提到)

偏移與走樣
陰影圖存盤的是距離光源最近的可視像素深度值,但是它的解析度有限,導致每一個陰影圖紋素都要表示場景中的一片區域,因此,陰影圖只是以光源視角針對場景深度進行的離散采樣,這將會導致所謂的陰影粉刺等影像走樣問題,如下圖所示(注意圖中地面上光影之間輪流交替的“階梯狀”條紋):

而下圖則簡單展示了為什么會發生陰影粉刺這種現象,由于陰影圖的解析度有限,所以每個陰影圖紋素要對應于長江中的一塊區域(而不是點對點的關系,一個坡面代表陰影圖中一個紋素的對應范圍),從觀察點E查看場景中的兩個點p1與p2,它們分別對應于兩個不同的螢屏像素,但是,從光源的觀察角度來看,它們卻都有著相同的陰影圖紋素(即s(p1)=s(p2)=s,由于解析度的原因),當我們在執行陰影圖檢測時,會得到d(p1) > s 及 d(p2) <= s這兩個測驗結果,這樣一來,p1將會被繪制為如同它在陰影中的顏色,p2將被渲染為好似它在陰影之外的顏色,從而導致陰影粉刺,

因此,我們可以通過偏移陰影圖中的深度值來防止出現錯誤的陰影效果,此時我們就可以保證d(p1) <= s 及 d(p2) <= s,但是尋找合適的深度偏移需要反復嘗試,

偏移量過大會導致名為peter-panning(彼得·潘,即小飛俠,他曾在一次逃跑時弄丟了自己的影子)的失真效果,使得陰影看起來與物體相分離,

然而,并沒有哪一種固定的偏移量可以正確地運用于所有幾何體的陰影繪制,特別是下圖那種(從光源的角度來看)有著極大斜率的三角形,這時候就需要選取更大的偏移量,但是,如果試圖通過一個過大的深度偏移量來處理所有的斜邊,則又會造成peter-panning問題,

因此,我們繪制陰影的方式就是先以光源視角度量多邊形斜面的斜率,并為斜率較大的多邊形應用更大的偏移量,而圖形硬體內部對此有相關技術的支持,我們通過名為斜率縮放偏移的光柵化狀態屬性就能夠輕松實作,
typedef struct D3D11_RASTERIZER_DESC {
// ...
INT DepthBias;
FLOAT DepthBiasClamp;
FLOAT SlopeScaledDepthBias;
BOOL DepthClipEnable;
// ...
} D3D11_RASTERIZER_DESC;
DepthBias:一個固定的應用偏移量,DepthBiasClamp:所允許的最大深度偏移量,以此來設定深度偏移量的上限,不難想象,及其陡峭的傾斜度會導致斜率縮放偏移量過大,從而造成peter-panning失真SlopeScaledDepthBias:根據多邊形的斜率來控制偏移程度的縮放因子,
注意,在將場景渲染至陰影貼圖時,便會應用該斜率縮放偏移量,這是由于我們希望以光源的視角基于多邊形的斜率而進行偏移操作,從而避免陰影失真,因此,我們就會對陰影圖中的數值進行偏移計算(即由硬體將像素的深度值與偏移值相加),在本Demo中采用的具體數值如下:
// [出自MSDN]
// 如果當前的深度緩沖區采用UNORM格式并且系結在輸出合并階段,或深度緩沖區還沒有被系結
// 則偏移量的計算程序如下:
//
// Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope;
//
// 這里的r是在深度緩沖區格式轉換為float32型別后,其深度值可取到大于0的最小可表示的值
// MaxDepthSlope則是像素在水平方向和豎直方向上的深度斜率的最大值
// [結束MSDN參考]
//
// 對于一個24位的深度緩沖區來說, r = 1 / 2^24
//
// 例如:DepthBias = 100000 ==> 實際的DepthBias = 100000/2^24 = .006
//
// 本Demo中的方向光始終與地面法線呈45度夾角,故取斜率為1.0f
// 以下資料極其依賴于實際場景,因此我們需要對特定場景反復嘗試才能找到最合適
rsDesc.DepthBias = 100000;
rsDesc.DepthBiasClamp = 0.0f;
rsDesc.SlopeScaledDepthBias = 1.0f
注意:深度偏移發生在光柵化期間(裁剪之后),因此不會對幾何體裁剪造成影響,
在RenderStates中我們添加了這樣一個光柵化狀態:
// 深度偏移模式
rasterizerDesc.FillMode = D3D11_FILL_SOLID;
rasterizerDesc.CullMode = D3D11_CULL_BACK;
rasterizerDesc.FrontCounterClockwise = false;
rasterizerDesc.DepthClipEnable = true;
rasterizerDesc.DepthBias = 100000;
rasterizerDesc.DepthBiasClamp = 0.0f;
rasterizerDesc.SlopeScaledDepthBias = 1.0f;
HR(device->CreateRasterizerState(&rasterizerDesc, RSDepth.GetAddressOf()));
MSDN檔案Depth Bias講述了該技術相關的全部規則,并且介紹了如何使用浮點深度緩沖區進行作業,
百分比漸近過濾(PCF)
在使用投影紋理坐標(u, v)對陰影圖進行采樣時,往往不會命中陰影圖中紋素的準確位置,而是通常位于陰影圖中的4個紋素之間,然而,我們不應該對深度值采用雙線性插值法,因為4個紋素之間的深度值不一定滿足線性過渡,插值出來的深度值跟實際的深度值有偏差,這樣可能會導致把像素錯誤標入陰影中這樣的錯誤結果(因此我們也不能為陰影圖生成mipmap),
出于這樣的原因,我們應該對采樣的結果進行插值,而不是對深度值進行插值,這種做法稱為——百分比漸近過濾,即我們以點過濾(MIN_MAG_MIP_POINT)的方式在坐標(u, v)、(u+△x, v)、(u, v+△x)、(u+△x, v+△x)處對紋理進行采樣,其中△x=1/SHADOW_MAP_SIZE(除以的是參考貼圖的寬高),由于是點采樣,這4個采樣點分別命中的是圍繞坐標(u, v)最近的4個陰影圖紋素s0、s1、s2、s3,如下圖所示,

接下來,我們會對這些采集的深度值進行陰影圖檢測,并對測驗的結果展開雙線性插值,
static const float SMAP_SIZE = 2048.0f;
static const float SMAP_DX = 1.0f / SMAP_SIZE;
// ...
//
// 采樣操作
//
// 對陰影圖進行采樣以獲取離光源最近的深度值
float s0 = g_ShadowMap.Sample(g_SamShadow, tex.xy).r;
float s1 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(SMAP_DX, 0)).r;
float s2 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(0, SMAP_DX)).r;
float s3 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(SMAP_DX, SMAP_DX)).r;
// 該像素的深度值是否小于等于陰影圖中的深度值
float r0 = (depth <= s0);
float r1 = (depth <= s1);
float r2 = (depth <= s2);
float r3 = (depth <= s3);
//
// 雙線性插值操作
//
// 變換到紋素空間
float2 texelPos = SMAP_SIZE * tex.xy;
// 確定插值系數(frac()回傳浮點數的小數部分)
float2 t = frac(texelPos);
// 對比較結果進行雙線性插值
return lerp(lerp(r0, r1, t.x), lerp(r2, r3, t.x), t.y);
若采用這種計算方法,則一個像素就可能區域處于陰影之中,而不是非0即1.例如,若有4個樣本,三個在陰影中,一個在陰影外,那么該像素有75%處于陰影之中,這就讓陰影內外的像素之間有了更加平滑的過渡,而不是棱角分明,

但這種過濾方法產生的陰影看起來仍然非常生硬,且鋸齒失真問題的最終處理效果還是不能令人十分滿意,PCF的主要缺點是需要4個紋理樣本,而紋理采樣本身就是現代GPU代價較高的操作之一,因為存盤器的帶寬與延遲并沒有隨著GPU計算能力的劇增而得到相近程度的巨大改良,幸運的是,Direct3D 11+版本的圖形硬體對PCF技術已經有了內部支持,上面的一大堆代碼可以用SampleCmpLevelZero函式來替代,
float percentage = g_ShadowMap.SampleCmpLevelZero(g_SamShadow, shadowPosH.xy, depth).r;
方法中的LevelZero部分意味著它只能在最高的mipmap層級中進行采樣,另外,該方法使用的并非一般的采樣器物件,而是比較采樣器,這使得硬體能夠執行陰影圖的比較測驗,并且需要在過濾采樣結果之前完成,對于PCF技術來說,我們需要使用的是D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT過濾器,并將比較函式設定為LESS_EQUAL(由于對深度值進行了偏移,所以也要用到LESS比較函式),
函式中傳入的depth將會出現在比較運算子的左邊,即:
depth <= sampleDepth
在RenderStates中我們添加了這樣一個采樣器:
ComPtr<ID3D11SamplerState> RenderStates::SSShadow = nullptr;
// 采樣器狀態:深度比較與Border模式
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_BORDER;
sampDesc.ComparisonFunc = D3D11_COMPARISON_LESS_EQUAL;
sampDesc.BorderColor[0] = { 1.0f };
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HR(device->CreateSamplerState(&sampDesc, SSShadow.GetAddressOf()));
注意:根據SDK檔案所述,只有
R32_FLOAT_X8X24_TYPELESS、R32_FLOAT,R24_UNORM_X8_TYPELESS、R16_UNORM格式才能用于比較過濾器,
在PCF的基礎上進行均值濾波
到目前為止,我們在本節中一直使用的是4-tap PCF核(輸入4個樣本來執行的PCF),PCF核越大,陰影的邊緣輪廓也就越豐滿、越平滑,當然,花費在SampleCmpLevelZero函式上的開銷也就越大,在本Demo中,我們是按3x3正方形的均值濾波方式來執行PCF,由于每次呼叫SampleCmpLevelZero函式實際所執行的都是4-tap PCF,所以一共采樣了36次,其中有4x4個獨立采樣點,此外,采用過大的濾波核還會導致之前所述的陰影粉刺問題,但本章不打算講述,有興趣可以回到龍書閱讀(過大的PCF核),
顯然,PCF技術一般來說只需在陰影的邊緣進行,因為陰影內外兩部分并不涉及混合操作(只有陰影邊緣才是漸變的),基于此,只要能對陰影邊緣的PCF設計相應的處理方案就好了,但這種做法一般要求我們所用的PCF核足夠大(5x5及更大)時才劃算(因為動態分支也有開銷),不過最終是要效率還是要畫質還是取決于你自己,
注意:實際工程中所用的PCF核不一定是方形的過濾柵格,不少文獻也指出,隨機的拾取點也可以作為PCF核,
考慮到在做比較時,如果處于陰影外的值為1,在陰影內的值為0,在采用SampleCmpLevelZero和均值濾波后,我們用范圍值0~1來表示處于陰影外的程度,隨著值的增加,該點也變得越亮,我們可以使用下面的函式來計算3x3正方形的均值濾波下的陰影因子:
float CalcShadowFactor(SamplerComparisonState samShadow, Texture2D shadowMap, float4 shadowPosH)
{
// 透視除法
shadowPosH.xyz /= shadowPosH.w;
// NDC空間的深度值
float depth = shadowPosH.z;
// 紋素在紋理坐標下的寬高
const float dx = SMAP_DX;
float percentLit = 0.0f;
const float2 offsets[9] =
{
float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx),
float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f),
float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx)
};
[unroll]
for (int i = 0; i < 9; ++i)
{
percentLit += shadowMap.SampleCmpLevelZero(samShadow,
shadowPosH.xy + offsets[i], depth).r;
}
return percentLit /= 9.0f;
}
然后在我們的光照模型中,只有第一個方向光才參與到陰影的計算,并且陰影因子將與直接光照(漫反射和鏡面反射光)項相乘,
// ...
float shadow[5] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
// 僅第一個方向光用于計算陰影
shadow[0] = CalcShadowFactor(g_SamShadow, g_ShadowMap, pIn.ShadowPosH);
[unroll]
for (i = 0; i < 5; ++i)
{
ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += shadow[i] * D;
spec += shadow[i] * S;
}
// ...
由于環境光是間接光,所以陰影因子不受影響,并且,陰影因子也不會對來自環境映射的反射光構成影響,
C++端代碼實作
在本章中,與陰影特效直接相關的類有BasicEffect、ShadowEffect,然后TextureRender類在本章還作為陰影圖使用,GameApp類承擔了實作程序,
改進TextureRender
既然陰影貼圖和RTT有著許多相似的地方,那何不把它也放到TextureRender里面共用呢?只要添加一個開關控制該RTT是否用作陰影貼圖即可,
class TextureRender
{
public:
template<class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
TextureRender() = default;
~TextureRender() = default;
// 不允許拷貝,允許移動
TextureRender(const TextureRender&) = delete;
TextureRender& operator=(const TextureRender&) = delete;
TextureRender(TextureRender&&) = default;
TextureRender& operator=(TextureRender&&) = default;
HRESULT InitResource(ID3D11Device* device,
int texWidth,
int texHeight,
bool shadowMap = false,
bool generateMips = false);
// 開始對當前紋理進行渲染
// 陰影貼圖無需提供背景色
void Begin(ID3D11DeviceContext* deviceContext, const FLOAT backgroundColor[4]);
// 結束對當前紋理的渲染,還原狀態
void End(ID3D11DeviceContext * deviceContext);
// 獲取渲染好的紋理的著色器資源視圖
// 陰影貼圖回傳的是深度緩沖區
// 參考數不增加,僅用于傳參
ID3D11ShaderResourceView* GetOutputTexture();
// 設定除錯物件名
void SetDebugObjectName(const std::string& name);
private:
ComPtr<ID3D11ShaderResourceView> m_pOutputTextureSRV; // 輸出的紋理(或陰影貼圖)對應的著色器資源視圖
ComPtr<ID3D11RenderTargetView> m_pOutputTextureRTV; // 輸出的紋理對應的渲染目標視圖
ComPtr<ID3D11DepthStencilView> m_pOutputTextureDSV; // 輸出紋理所用的深度/模板視圖(或陰影貼圖)
D3D11_VIEWPORT m_OutputViewPort = {}; // 輸出所用的視口
ComPtr<ID3D11RenderTargetView> m_pCacheRTV; // 臨時快取的后備緩沖區
ComPtr<ID3D11DepthStencilView> m_pCacheDSV; // 臨時快取的深度/模板緩沖區
D3D11_VIEWPORT m_CacheViewPort = {}; // 臨時快取的視口
bool m_GenerateMips = false; // 是否生成mipmap鏈
bool m_ShadowMap = false; // 是否為陰影貼圖
};
在作為RTT時,需要創建紋理與它的SRV和RTV、深度/模板緩沖區和它的DSV、視口
而作為陰影貼圖時,需要創建深度緩沖區與它的SRV和DSV、視口
下面的代碼只關注創建陰影貼圖的部分:
HRESULT TextureRender::InitResource(ID3D11Device* device, int texWidth, int texHeight, bool shadowMap, bool generateMips)
{
// 防止重復初始化造成記憶體泄漏
m_pOutputTextureSRV.Reset();
m_pOutputTextureRTV.Reset();
m_pOutputTextureDSV.Reset();
m_pCacheRTV.Reset();
m_pCacheDSV.Reset();
m_ShadowMap = shadowMap;
m_GenerateMips = false;
HRESULT hr;
// ...
// ******************
// 創建與紋理等寬高的深度/模板緩沖區或陰影貼圖,以及對應的視圖
//
CD3D11_TEXTURE2D_DESC texDesc((m_ShadowMap ? DXGI_FORMAT_R24G8_TYPELESS : DXGI_FORMAT_D24_UNORM_S8_UINT),
texWidth, texHeight, 1, 1,
D3D11_BIND_DEPTH_STENCIL | (m_ShadowMap ? D3D11_BIND_SHADER_RESOURCE : 0));
ComPtr<ID3D11Texture2D> depthTex;
hr = device->CreateTexture2D(&texDesc, nullptr, depthTex.GetAddressOf());
if (FAILED(hr))
return hr;
CD3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc(depthTex.Get(), D3D11_DSV_DIMENSION_TEXTURE2D, DXGI_FORMAT_D24_UNORM_S8_UINT);
hr = device->CreateDepthStencilView(depthTex.Get(), &dsvDesc,
m_pOutputTextureDSV.GetAddressOf());
if (FAILED(hr))
return hr;
if (m_ShadowMap)
{
// 陰影貼圖的SRV
CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(depthTex.Get(), D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R24_UNORM_X8_TYPELESS);
hr = device->CreateShaderResourceView(depthTex.Get(), &srvDesc,
m_pOutputTextureSRV.GetAddressOf());
if (FAILED(hr))
return hr;
}
// ******************
// 初始化視口
//
m_OutputViewPort.TopLeftX = 0.0f;
m_OutputViewPort.TopLeftY = 0.0f;
m_OutputViewPort.Width = static_cast<float>(texWidth);
m_OutputViewPort.Height = static_cast<float>(texHeight);
m_OutputViewPort.MinDepth = 0.0f;
m_OutputViewPort.MaxDepth = 1.0f;
return S_OK;
}
需要注意的是,在創建深度緩沖區時,如果還想為他創建SRV,就不能將DXGI格式定義成DXGI_FORMAT_D24_UNORM_S8_UINT這些帶D的型別,而應該是DXGI_FORMAT_R24G8_TYPELESS
然后在創建陰影貼圖的SRV時,則需要指定為DXGI_FORMAT_R24_UNORM_X8_TYPELESS
開始陰影貼圖的渲染前,不需要設定RTV,只需要系結DSV,
void TextureRender::Begin(ID3D11DeviceContext* deviceContext, const FLOAT backgroundColor[4])
{
// 快取渲染目標和深度模板視圖
deviceContext->OMGetRenderTargets(1, m_pCacheRTV.GetAddressOf(), m_pCacheDSV.GetAddressOf());
// 快取視口
UINT num_Viewports = 1;
deviceContext->RSGetViewports(&num_Viewports, &m_CacheViewPort);
// 清慷訓沖區
// ...
deviceContext->ClearDepthStencilView(m_pOutputTextureDSV.Get(), D3D11_CLEAR_DEPTH | (m_ShadowMap ? 0 : D3D11_CLEAR_STENCIL), 1.0f, 0);
// 設定渲染目標和深度模板視圖
deviceContext->OMSetRenderTargets((m_ShadowMap ? 0 : 1),
(m_ShadowMap ? nullptr : m_pOutputTextureRTV.GetAddressOf()),
m_pOutputTextureDSV.Get());
// 設定視口
deviceContext->RSSetViewports(1, &m_OutputViewPort);
}
渲染完成后,和往常一樣還原即可,
EffectHelper的引入
本章開始的代碼引入了EffectHelper來管理著色器所需的資源(我們可以無需手動創建并交給它來托管),并應用在了所有的Effect類當中,除了IEffect介面類,目前還引入了IEffectTransform介面類來統一變換的設定,
class IEffectTransform
{
public:
virtual void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W) = 0;
virtual void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V) = 0;
virtual void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P) = 0;
};
class IEffectTextureDiffuse
{
public:
virtual void SetTextureDiffuse(ID3D11ShaderResourceView* textureDiffuse) = 0;
};
然后特效類再根據自己的需要選擇介面并實作:
class BasicEffect : public IEffect, public IEffectTransform, public IEffectTextureDiffuse
{
// ...
};
隨著抽象類的增加,像GameObject這樣的類就可以對IEffect介面類物件查詢是否有某一特定介面類或具體類來執行額外的復雜操作,
void GameObject::Draw(ID3D11DeviceContext * deviceContext, IEffect * effect)
{
UINT strides = m_Model.vertexStride;
UINT offsets = 0;
for (auto& part : m_Model.modelParts)
{
// 設定頂點/索引緩沖區
deviceContext->IASetVertexBuffers(0, 1, part.vertexBuffer.GetAddressOf(), &strides, &offsets);
deviceContext->IASetIndexBuffer(part.indexBuffer.Get(), part.indexFormat, 0);
/// 更新資料并應用
IEffectTransform* pEffectTransform = dynamic_cast<IEffectTransform*>(effect);
if (pEffectTransform)
{
pEffectTransform->SetWorldMatrix(m_Transform.GetLocalToWorldMatrixXM());
}
IEffectTextureDiffuse* pEffectTextureDiffuse = dynamic_cast<IEffectTextureDiffuse*>(effect);
if (pEffectTextureDiffuse)
{
pEffectTextureDiffuse->SetTextureDiffuse(part.texDiffuse.Get());
}
BasicEffect* pBasicEffect = dynamic_cast<BasicEffect*>(effect);
if (pBasicEffect)
{
pBasicEffect->SetTextureNormalMap(part.texNormalMap.Get());
pBasicEffect->SetMaterial(part.material);
}
effect->Apply(deviceContext);
deviceContext->DrawIndexed(part.indexCount, 0, 0);
}
}
void GameObject::DrawInstanced(ID3D11DeviceContext* deviceContext, IEffect * effect, const std::vector<Transform> & data)
{
// ...
UINT strides[2] = { m_Model.vertexStride, sizeof(InstancedData) };
UINT offsets[2] = { 0, 0 };
ID3D11Buffer* buffers[2] = { nullptr, m_pInstancedBuffer.Get() };
for (auto& part : m_Model.modelParts)
{
buffers[0] = part.vertexBuffer.Get();
// 設定頂點/索引緩沖區
deviceContext->IASetVertexBuffers(0, 2, buffers, strides, offsets);
deviceContext->IASetIndexBuffer(part.indexBuffer.Get(), part.indexFormat, 0);
// 更新資料并應用
IEffectTextureDiffuse* pEffectTextureDiffuse = dynamic_cast<IEffectTextureDiffuse*>(effect);
if (pEffectTextureDiffuse)
{
pEffectTextureDiffuse->SetTextureDiffuse(part.texDiffuse.Get());
}
BasicEffect* pBasicEffect = dynamic_cast<BasicEffect*>(effect);
if (pBasicEffect)
{
pBasicEffect->SetTextureNormalMap(part.texNormalMap.Get());
pBasicEffect->SetMaterial(part.material);
}
effect->Apply(deviceContext);
deviceContext->DrawIndexedInstanced(part.indexCount, numInsts, 0, 0, 0);
}
}
構建陰影貼圖與更新
首先我們要在GameApp::InitResource中創建一副2048x2048的陰影貼圖:
m_pShadowMap = std::make_unique<TextureRender>();
HR(m_pShadowMap->InitResource(m_pd3dDevice.Get(), 2048, 2048, true));
在本Demo中,光照方向每幀都在變動,我們希望讓投影立方體與光照所屬的變換軸對齊,并且中心能夠坐落在原點,因此在GameApp::UpdateScene可以這么做:
//
// 投影區域為正方體,以原點為中心,以方向光為+Z朝向
//
XMMATRIX LightView = XMMatrixLookAtLH(dirVec * 20.0f * (-2.0f), g_XMZero, g_XMIdentityR1);
m_pShadowEffect->SetViewMatrix(LightView);
// 將NDC空間 [-1, +1]^2 變換到紋理坐標空間 [0, 1]^2
static XMMATRIX T(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f);
// S = V * P * T
m_pBasicEffect->SetShadowTransformMatrix(LightView * XMMatrixOrthographicLH(40.0f, 40.0f, 20.0f, 60.0f) * T);
繪制程序
在最終的繪制程序如下:
void GameApp::DrawScene()
{
assert(m_pd3dImmediateContext);
assert(m_pSwapChain);
m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Silver));
m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
// ******************
// 繪制到陰影貼圖
//
m_pShadowMap->Begin(m_pd3dImmediateContext.Get(), nullptr);
{
DrawScene(m_pShadowEffect.get());
}
m_pShadowMap->End(m_pd3dImmediateContext.Get());
// ******************
// 正常繪制場景
//
m_pBasicEffect->SetTextureShadowMap(m_pShadowMap->GetOutputTexture());
DrawScene(m_pBasicEffect.get(), m_EnableNormalMap);
// 繪制天空盒
m_pDesert->Draw(m_pd3dImmediateContext.Get(), *m_pSkyEffect, *m_pCamera);
// 解除深度緩沖區系結
m_pBasicEffect->SetTextureShadowMap(nullptr);
m_pBasicEffect->Apply(m_pd3dImmediateContext.Get());
// ******************
// 除錯繪制陰影貼圖
//
if (m_EnableDebug)
{
if (m_GrayMode)
{
m_pDebugEffect->SetRenderOneComponentGray(m_pd3dImmediateContext.Get(), 0);
}
else
{
m_pDebugEffect->SetRenderOneComponent(m_pd3dImmediateContext.Get(), 0);
}
m_DebugQuad.Draw(m_pd3dImmediateContext.Get(), m_pDebugEffect.get());
// 解除系結
m_pDebugEffect->SetTextureDiffuse(nullptr);
m_pDebugEffect->Apply(m_pd3dImmediateContext.Get());
}
// Direct2D 部分...
HR(m_pSwapChain->Present(0, 0));
}
void GameApp::DrawScene(BasicEffect* pBasicEffect, bool enableNormalMap)
{
// 地面和石柱
if (enableNormalMap)
{
pBasicEffect->SetRenderWithNormalMap(m_pd3dImmediateContext.Get(), IEffect::RenderObject);
m_GroundT.Draw(m_pd3dImmediateContext.Get(), pBasicEffect);
pBasicEffect->SetRenderWithNormalMap(m_pd3dImmediateContext.Get(), IEffect::RenderInstance);
m_CylinderT.DrawInstanced(m_pd3dImmediateContext.Get(), pBasicEffect, m_CylinderTransforms);
}
else
{
pBasicEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderObject);
m_Ground.Draw(m_pd3dImmediateContext.Get(), pBasicEffect);
pBasicEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderInstance);
m_Cylinder.DrawInstanced(m_pd3dImmediateContext.Get(), pBasicEffect, m_CylinderTransforms);
}
// 石球
pBasicEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderInstance);
m_Sphere.DrawInstanced(m_pd3dImmediateContext.Get(), pBasicEffect, m_SphereTransforms);
// 房屋
pBasicEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderObject);
m_House.Draw(m_pd3dImmediateContext.Get(), pBasicEffect);
}
void GameApp::DrawScene(ShadowEffect* pShadowEffect)
{
// 地面
pShadowEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderObject);
m_Ground.Draw(m_pd3dImmediateContext.Get(), pShadowEffect);
// 石柱
pShadowEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderInstance);
m_Cylinder.DrawInstanced(m_pd3dImmediateContext.Get(), pShadowEffect, m_CylinderTransforms);
// 石球
pShadowEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderInstance);
m_Sphere.DrawInstanced(m_pd3dImmediateContext.Get(), pShadowEffect, m_SphereTransforms);
// 房屋
pShadowEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), IEffect::RenderObject);
m_House.Draw(m_pd3dImmediateContext.Get(), pShadowEffect);
}
演示
本Demo提供了5種斜率下的方向光,對應主鍵盤數字鍵1-5,Q鍵開關法線貼圖,E鍵開關陰影貼圖的顯示,G鍵切換陰影貼圖的顯示模式,

透明物體的陰影繪制
但我們的例程還沒有處理透明物體的陰影繪制,如果我們直接在場景中繪制一顆樹(貼圖存在Alpha值為0的部分),可以看到下圖的陰影并不正確:

因此,我們需要在繪制陰影貼圖的時候增加一個像素著色器用以進行Alpha裁剪,把Alpha值低于0.1的紋素給剔除掉,不要讓其寫入到陰影貼圖:
Texture2D g_DiffuseMap : register(t0);
SamplerState g_Sam : register(s0);
struct VertexPosHTex
{
float4 PosH : SV_POSITION;
float2 Tex : TEXCOORD;
};
// 這僅僅用于Alpha幾何裁剪,以保證陰影的顯示正確,
// 對于不需要進行紋理采樣操作的幾何體可以直接將像素
// 著色器設為nullptr
void PS(VertexPosHTex pIn)
{
float4 diffuse = g_DiffuseMap.Sample(g_Sam, pIn.Tex);
// 不要將透明像素寫入深度貼圖
clip(diffuse.a - 0.1f);
}
我們只在繪制樹的時候使用帶有像素著色器的版本,其余物體照常繪制,并且因為我們的BasicEffect默認繪制就帶有Alpha裁剪,無需做這部分改動,最終效果如下:

練習題
- 嘗試4096x4096、1024x1024、512x512、256x256這幾種不同解析度的陰影貼圖
- 嘗試以單次點采樣陰影檢測來修改本演示程式(即不采用PCF),我們將欣賞到硬陰影與鋸齒狀的陰影邊緣
- 關閉斜率縮放偏移來觀察陰影粉刺
- 將斜率縮放偏移值放大10倍,觀察peter panning失真的效果
- 實作單點光源下的陰影(必要時可以考慮像CubeMap那樣使用6個正方形貼圖)
- 修改專案代碼,把繪制房屋改成繪制上圖中的樹(模型已給出),要求陰影顯示正確
DirectX11 With Windows SDK完整目錄
Github專案原始碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/33914.html
標籤:其他
下一篇:游戲開發優化篇之合并圖集
