前言
深度/模板測試使用的是與后備緩沖區同等分辨率大小的緩沖區,每個元素的一部分連續位用於深度測試,其余的則用作模板測試。兩個測試的目的都是為了能夠根據深度/模板狀態需求的設置來選擇需要繪制的像素。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
深度/模板測試
深度測試、模板測試的執行是在混合操作之前執行的,具體的執行順序為:模板測試→深度測試→混合操作。
深度測試
深度測試需要用到深度/模板緩沖區,對每個像素使用24位或32位來映射物體從世界到NDC坐標系下z的值(即深度,范圍[0.0, 1.0])。0.0時達到攝像機的最近可視距離,而1.0時達到攝像機的最遠可視距離。若某一像素位置接收到多個像素片元,只有z值最小的像素才會通過最終的深度測試。具體細化的話,就是現在有一個像素片元,已知它的深度值,然后需要跟深度緩沖區中的深度值進行比較,若小於深度緩沖區的深度值,則該像素片元將會覆蓋后備緩沖區原來的像素片元,並更新深度緩沖區中對應位置的深度值。
模板測試
除了深度測試以為,我們還可以設定模板測試來阻擋某些特定的區域的像素通過后備緩沖區。而且模板測試在操作上自由度會比深度測試大。對於需要進行模板測試的像素,比較式如下:
(StencilRef & StencilReadMask) ⊴ (Value & StencilReadMask)
該表達式首先括號部分是兩個操作數進行與運算,然后通過⊴(用戶指定的運算符)對兩個結果進行比較。若該表達式的值為真,則最終通過模板測試,並保留該像素進行后續的混合操作。
其中StencilReadMask
則是應用程序所提供的掩碼值。
深度/模板格式
深度/模板緩沖區是一個2D數組(紋理),它必須經由確定的數據格式創建:
-
DXGI_FORMAT_D32_FLOAT_S8X24_UINT
:每個元素占64位,其中32位浮點數用於深度測試,8位無符號整數用於模板測試,剩余24位僅用於填充。 -
DXGI_FORMAT_D24_UNORM_S8_UINT
:每個元素占32位,其中24位無符號整數映射到深度值[0.0, 1.0]的區間,8位無符號整數用於模板測試。
ID3D11DeviceContext::ClearDepthStencilView方法–深度/模板緩沖區內容清空
方法原型如下:
void ID3D11DeviceContext::ClearDepthStencilView(
ID3D11DepthStencilView *pDepthStencilView, // [In]深度模板視圖
UINT ClearFlags, // [In]使用D3D11_CLEAR_FLAG枚舉類型決定需要清空的部分
FLOAT Depth, // [In]使用Depth值填充所有元素的深度部分
UINT8 Stencil); // [In]使用Stencil值填充所有元素的模板部分
其中D3D11_CLEAR_FLAG
有如下枚舉值:
枚舉值 | 含義 |
---|---|
D3D11_CLEAR_DEPTH | 清空深度部分 |
D3D11_CLEAR_STENCIL | 清空模板部分 |
可以使用位運算或來同時清理。
通常深度值會默認設為1.0以確保任何在攝像機視野范圍內的物體都能被顯示出來
模板值則默認會設置為0
ID3D11Device::CreateDepthStencilState方法–創建深度/模板狀態
要創建深度/模板狀態ID3D11DepthStencilState
之前,首先需要填充D3D11_DEPTH_STENCIL_DESC
結構體:
typedef struct D3D11_DEPTH_STENCIL_DESC {
BOOL DepthEnable; // 是否開啟深度測試
D3D11_DEPTH_WRITE_MASK DepthWriteMask; // 深度值寫入掩碼
D3D11_COMPARISON_FUNC DepthFunc; // 深度比較函數
BOOL StencilEnable; // 是否開啟模板測試
UINT8 StencilReadMask; // 模板值讀取掩碼
UINT8 StencilWriteMask; // 模板值寫入掩碼
D3D11_DEPTH_STENCILOP_DESC FrontFace; // 對正面朝向的三角形進行深度/模板操作描述
D3D11_DEPTH_STENCILOP_DESC BackFace; // 對背面朝向的三角形進行深度/模板操作的描述
} D3D11_DEPTH_STENCIL_DESC;
深度狀態設定
-
DepthEnable
:如果關閉了深度測試,則繪制的先后順序就十分重要了。對於不透明的物體,必須按照從后到前的順序進行繪制,否則最后繪制的內容會覆蓋之前的內容,看起來就像在最前面那樣。並且關閉深度測試會導致深度緩沖區的值會保持原樣,不再進行更新,此時DepthWriteMask
也不會使用。 -
D3D11_DEPTH_WRITE_MASK
枚舉類型只有兩種枚舉值:
枚舉值 | 含義 |
---|---|
D3D11_DEPTH_WRITE_MASK_ZERO | 不寫入深度/模板緩沖區 |
D3D11_DEPTH_WRITE_MASK_ALL | 允許寫入深度/模板緩沖區 |
但即便設置了D3D11_DEPTH_WRITE_MASK_ZERO
,如果DepthEnable
開着的話仍會取原來的深度值進行深度比較,只是不會更新深度緩沖區。
DepthFunc
:指定D3D11_COMPARISON_FUNC
枚舉值來描述深度測試的比較操作,標准情況下是使用D3D11_COMPARISON_LESS
來進行深度測試,當然你也可以自定義測試的比較方式。
枚舉類型D3D11_COMPARISON_FUNC
的枚舉值如下:
枚舉值 | 含義 |
---|---|
D3D11_COMPARISON_NEVER = 1 | 該比較函數一定返回false |
D3D11_COMPARISON_LESS = 2 | 使用<來替換⊴ |
D3D11_COMPARISON_EQUAL = 3 | 使用==來替換⊴ |
D3D11_COMPARISON_LESS_EQUAL = 4 | 使用<=來替換⊴ |
D3D11_COMPARISON_GREATER = 5 | 使用>來替換⊴ |
D3D11_COMPARISON_NOT_EQUAL = 6 | 使用!=來替換⊴ |
D3D11_COMPARISON_GREATER_EQUAL = 7 | 使用>=來替換⊴ |
D3D11_COMPARISON_ALWAYS = 8 | 該比較函數一定返回true |
默認情況下,深度狀態的值如下:
DepthEnable = TRUE;
DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL
DepthFunc = D3D11_COMPARISION_LESS
模板狀態設定
StencilEnable
:若要使用模板測試,則指定為true
StencilReadMask
:該掩碼用於指定StencilRef
和深度/模板緩沖區的模板值Value
中的某些特定位,默認使用的是下面宏常量:
#define D3D11_DEFAULT_STENCIL_READ_MASK (0xff)
StencilWriteMask
:該掩碼指定待寫入的模板值的哪些位要寫入深度/模板緩沖區中,默認使用的是下面宏常量:
#define D3D11_DEFAULT_STENCIL_WRITE_MASK (0xff)
FrontFace
:該結構體指定了不同測試結果下對模板值應做什么樣的更新(對於正面朝向的三角形)BackFace
:該結構體指定了不同測試結果下對模板值應做什么樣的更新(對於背面朝向的三角形)
深度/模板操作描述結構體如下:
typedefstruct D3D11_DEPTH_STENCILOP_DESC {
D3D11_STENCIL_OP StencilFailOp;
D3D11_STENCIL_OP StencilDepthFailOp;
D3D11_STENCIL_OP StencilPassOp;
D3D11_COMPARISON_FUNC StencilFunc;
} D3D11_DEPTH_STENCILOP_DESC;
StencilFailOp
:若模板測試不通過對深度/模板緩沖區的模板值部分的操作StencilDepthFailOp
:若模板測試通過,但深度測試不通過對深度/模板緩沖區的模板值部分的操作StencilPassOp
:若模板/深度測試通過對深度/模板緩沖區的模板值部分的操作StencilFunc
:模板測試所用的比較函數
枚舉類型D3D11_STENCIL_OP
的枚舉值如下:
枚舉值 | 含義 |
---|---|
D3D11_STENCIL_OP_KEEP | 保持目標模板值不變 |
D3D11_STENCIL_OP_ZERO | 保持目標模板值為0 |
D3D11_STENCIL_OP_REPLACE | 使用StencilRef 的值替換模板模板值 |
D3D11_STENCIL_OP_INCR_SAT | 對目標模板值加1,超過255的話將值保持在255 |
D3D11_STENCIL_OP_DECR_SAT | 對目標模板值減1,低於0的話將保持在0 |
D3D11_STENCIL_OP_INVERT | 對目標模板值的每個位進行翻轉 |
D3D11_STENCIL_OP_INCR | 對目標模板值加1,超過255的話值將上溢變成0 |
D3D11_STENCIL_OP_DECR | 對目標模板值減1,低於0的話將下溢變成255 |
默認情況下,模板狀態的值如下:
StencilEnable = FALSE;
StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;
FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
填充完上面一堆結構體信息后,就終於可以創建深度模板狀態了:
HRESULT ID3D11Device::CreateDepthStencilState(
const D3D11_DEPTH_STENCIL_DESC *pDepthStencilDesc, // [In]深度/模板狀態描述
ID3D11DepthStencilState **ppDepthStencilState // [Out]輸出深度/模板狀態
);
ID3D11DeviceContext::OMSetDepthStencilState方法–輸出合並階段設置深度/模板狀態
創建好深度/模板狀態后,我們就可以將它綁定到渲染管線上:
void ID3D11DeviceContext::OMSetDepthStencilState(
ID3D11DepthStencilState *pDepthStencilState, // [In]深度/模板狀態,使用nullptr的話則是默認深度/模板狀態
UINT StencilRef); // [In]提供的模板值
如果要恢復到默認狀況,可以這樣調用:
md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
利用模板測試繪制平面鏡
要實現鏡面反射的效果,我們需要解決兩個問題:
- 如何計算出一個物體的所有頂點在任意平面的鏡面的反射位置
- 在鏡面位置只顯示鏡面本身和反射的物體的混合
若一個有平面鏡的場景中包含透明和非透明物體,則實際的繪制順序為:
- 只向鏡面區域的模板緩沖區寫入值1,而深度緩沖區和后備緩沖區的值都不應該寫入
- 將需要繪制的鏡面反射物體進行反射變換,然后僅在模板值為1的區域先繪制不透明的反射物體到后備緩沖區
- 在模板值為1的區域繪制透明的反射物體后,再繪制透明鏡面到后備緩沖區
- 繪制正常的非透明物體到后備緩沖區
- 繪制透明物體到后備緩沖區
在3D場景中,要繪制鏡面反射的物體,我們只需要將原本的物體(所有頂點位置)進行鏡面反射矩陣的變換即可得到。但是反射的物體僅可以在物體一側透過鏡面看到,在鏡面的另一邊是無法看到反射的物體的。通過模板測試,我們可以在攝像機僅與鏡面同側的時候標定鏡面區域,並繪制鏡面反射的物體。
我們可以使用XMMatrixReflection
函數來創建反射矩陣,提供的參數為平面向量\((\mathbf{n} ,d)\)
這里簡單了解一下,平面可以表示為點法式:
n為平面法向量,p為平面一點,進行叉乘運算。
d是一個有向距離值
上面的式子展開后就是我們高數見到的平面方程:
這相當於我
例如(0.0f, 0.0f, -1.0f, 10.0f)
可以表示z = 10
的平面
HLSL代碼的變化
在Basic.hlsli
中,添加了一個常量緩沖區用來控制反射開關,它的更新頻率僅次於每次繪制更新的緩沖區。並且由於鏡面是固定的,這里將鏡面反射矩陣放在不會變化的常量緩沖區上:
cbuffer CBChangesEveryDrawing : register(b0)
{
matrix g_World;
matrix g_WorldInvTranspose;
Material g_Material;
}
cbuffer CBDrawingStates : register(b1)
{
int g_IsReflection;
float3 g_Pad1;
}
cbuffer CBChangesEveryFrame : register(b2)
{
matrix g_View;
float3 g_EyePosW;
}
cbuffer CBChangesOnResize : register(b3)
{
matrix g_Proj;
}
cbuffer CBChangesRarely : register(b4)
{
matrix g_Reflection;
DirectionalLight g_DirLight[10];
PointLight g_PointLight[10];
SpotLight g_SpotLight[10];
int g_NumDirLight;
int g_NumPointLight;
int g_NumSpotLight;
float g_Pad2;
}
所以現在目前已經使用了5個常量緩沖區,可以說在管理上會非常復雜,其中頂點着色器需要用到所有的常量緩沖區,而像素着色器需要用到除了CBChangesOnResize
外的所有常量緩沖區。
然后3D頂點着色器添加了是否需要乘上反射矩陣的判定:
// Basic_VS_3D.hlsl
#include "Basic.hlsli"
// 頂點着色器(3D)
VertexPosHWNormalTex VS_3D(VertexPosNormalTex vIn)
{
VertexPosHWNormalTex vOut;
matrix viewProj = mul(g_View, g_Proj);
float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);
float3 normalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
// 若當前在繪制反射物體,先進行反射操作
[flatten]
if (g_IsReflection)
{
posW = mul(posW, g_Reflection);
normalW = mul(normalW, (float3x3) g_Reflection);
}
vOut.PosH = mul(posW, viewProj);
vOut.PosW = posW.xyz;
vOut.NormalW = normalW;
vOut.Tex = vIn.Tex;
return vOut;
}
對於像素着色器來說,由於點光燈和聚光燈都可以看做是物體,所以也應該進行鏡面反射矩陣變換(主要反射光的方向和位置):
// Basic_PS_3D.hlsl
#include "Basic.hlsli"
// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
// 提前進行裁剪,對不符合要求的像素可以避免后續運算
float4 texColor = g_Tex.Sample(g_SamLinear, pIn.Tex);
clip(texColor.a - 0.1f);
// 標准化法向量
pIn.NormalW = normalize(pIn.NormalW);
// 頂點指向眼睛的向量
float3 toEyeW = normalize(g_EyePosW - pIn.PosW);
// 初始化為0
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
int i;
[unroll]
for (i = 0; i < 5; ++i)
{
DirectionalLight dirLight = g_DirLight[i];
[flatten]
if (g_IsReflection)
{
dirLight.Direction = mul(dirLight.Direction, (float3x3) (g_Reflection));
}
ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
// 若當前在繪制反射物體,需要對光照進行反射矩陣變換
PointLight pointLight;
[unroll]
for (i = 0; i < 5; ++i)
{
pointLight = g_PointLight[i];
[flatten]
if (g_IsReflection)
{
pointLight.Position = (float3) mul(float4(pointLight.Position, 1.0f), g_Reflection);
}
ComputePointLight(g_Material, pointLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
SpotLight spotLight;
// 若當前在繪制反射物體,需要對光照進行反射矩陣變換
[unroll]
for (i = 0; i < 5; ++i)
{
spotLight = g_SpotLight[i];
[flatten]
if (g_IsReflection)
{
spotLight.Position = (float3) mul(float4(spotLight.Position, 1.0f), g_Reflection);
spotLight.Direction = mul(spotLight.Direction, (float3x3) g_Reflection);
}
ComputeSpotLight(g_Material, spotLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
float4 litColor = texColor * (ambient + diffuse) + spec;
litColor.a = texColor.a * g_Material.Diffuse.a;
return litColor;
}
RenderStates類的變化
RenderStates
類變化如下:
class RenderStates
{
public:
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
static void InitAll(ID3D11Device * device);
// 使用ComPtr無需手工釋放
public:
static ComPtr<ID3D11RasterizerState> RSWireframe; // 光柵化器狀態:線框模式
static ComPtr<ID3D11RasterizerState> RSNoCull; // 光柵化器狀態:無背面裁剪模式
static ComPtr<ID3D11RasterizerState> RSCullClockWise; // 光柵化器狀態:順時針裁剪模式
static ComPtr<ID3D11SamplerState> SSLinear; // 采樣器狀態:線性過濾
static ComPtr<ID3D11SamplerState> SSAnistropic; // 采樣器狀態:各項異性過濾
static ComPtr<ID3D11BlendState> BSNoColorWrite; // 混合狀態:不寫入顏色
static ComPtr<ID3D11BlendState> BSTransparent; // 混合狀態:透明混合
static ComPtr<ID3D11BlendState> BSAlphaToCoverage; // 混合狀態:Alpha-To-Coverage
static ComPtr<ID3D11DepthStencilState> DSSMarkMirror; // 深度/模板狀態:標記鏡面區域
static ComPtr<ID3D11DepthStencilState> DSSDrawReflection; // 深度/模板狀態:繪制反射區域
static ComPtr<ID3D11DepthStencilState> DSSNoDoubleBlend; // 深度/模板狀態:無二次混合區域
static ComPtr<ID3D11DepthStencilState> DSSNoDepthTest; // 深度/模板狀態:關閉深度測試
static ComPtr<ID3D11DepthStencilState> DSSNoDepthWrite; // 深度/模板狀態:僅深度測試,不寫入深度值
};
新增的渲染狀態的定義如下:
void RenderStates::InitAll(ID3D11Device * device)
{
// 先前初始化過的話就沒必要重來了
if (IsInit())
return;
// ***********初始化光柵化器狀態***********
D3D11_RASTERIZER_DESC rasterizerDesc;
ZeroMemory(&rasterizerDesc, sizeof(rasterizerDesc));
// ...
// 順時針剔除模式
rasterizerDesc.FillMode = D3D11_FILL_SOLID;
rasterizerDesc.CullMode = D3D11_CULL_BACK;
rasterizerDesc.FrontCounterClockwise = true;
rasterizerDesc.DepthClipEnable = true;
HR(device->CreateRasterizerState(&rasterizerDesc, &RSCullClockWise));
// ***********初始化采樣器狀態***********
// ...
// ***********初始化混合狀態***********
// ...
// ***********初始化深度/模板狀態***********
D3D11_DEPTH_STENCIL_DESC dsDesc;
// 鏡面標記深度/模板狀態
// 這里不寫入深度信息
// 無論是正面還是背面,原來指定的區域的模板值都會被寫入StencilRef
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
dsDesc.StencilEnable = true;
dsDesc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
dsDesc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;
dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
// 對於背面的幾何體我們是不進行渲染的,所以這里的設置無關緊要
dsDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
HR(device->CreateDepthStencilState(&dsDesc, DSSMarkMirror.GetAddressOf()));
// 反射繪制深度/模板狀態
// 由於要繪制反射鏡面,需要更新深度
// 僅當鏡面標記模板值和當前設置模板值相等時才會進行繪制
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
dsDesc.StencilEnable = true;
dsDesc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
dsDesc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;
dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// 對於背面的幾何體我們是不進行渲染的,所以這里的設置無關緊要
dsDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
HR(device->CreateDepthStencilState(&dsDesc, DSSDrawReflection.GetAddressOf()));
// 無二次混合深度/模板狀態
// 允許默認深度測試
// 通過自遞增使得原來StencilRef的值只能使用一次,實現僅一次混合
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
dsDesc.StencilEnable = true;
dsDesc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
dsDesc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;
dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// 對於背面的幾何體我們是不進行渲染的,所以這里的設置無關緊要
dsDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
HR(device->CreateDepthStencilState(&dsDesc, DSSNoDoubleBlend.GetAddressOf()));
// 關閉深度測試的深度/模板狀態
// 若繪制非透明物體,務必嚴格按照繪制順序
// 繪制透明物體則不需要擔心繪制順序
// 而默認情況下模板測試就是關閉的
dsDesc.DepthEnable = false;
dsDesc.StencilEnable = false;
HR(device->CreateDepthStencilState(&dsDesc, DSSNoDepthTest.GetAddressOf()));
// 進行深度測試,但不寫入深度值的狀態
// 若繪制非透明物體時,應使用默認狀態
// 繪制透明物體時,使用該狀態可以有效確保混合狀態的進行
// 並且確保較前的非透明物體可以阻擋較后的一切物體
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
dsDesc.StencilEnable = false;
HR(device->CreateDepthStencilState(&dsDesc, DSSNoDepthWrite.GetAddressOf()));
}
場景繪制
現在場景內有四面牆,一個平面鏡,一面地板,一個籬笆盒和水面。
開始繪制前,我們需要清空深度/模板緩沖區和渲染目標視圖:
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black));
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
第1步: 鏡面區域寫入模板緩沖區
這一步通過對鏡面所在區域寫入模板值1來標定鏡面繪制區域。
// *********************
// 1. 給鏡面反射區域寫入值1到模板緩沖區
//
// 裁剪掉背面三角形
// 標記鏡面區域的模板值為1
// 不寫入像素顏色
m_pd3dImmediateContext->RSSetState(nullptr);
m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSMarkMirror.Get(), 1);
m_pd3dImmediateContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);
m_Mirror.Draw(m_pd3dImmediateContext.Get());
通過VS圖形調試器可以看到模板值為1的區域
第2步:繪制不透明的鏡面反射物體
理論上會有三面牆和地板可能會透過鏡面看到,這里都需要繪制,但要注意在對頂點位置做反射變換時,原來平面向外的法向量變成了平面向內部,因此還需要額外對法向量做反射變換(龍書缺少了對法向量的反射變換)。並且原來按順時針排布的三角形頂點也變成了逆時針排布。所以需要對順時針排布的頂點做裁剪處理。
在做模板測試的時候,我們僅對模板值為1的像素點通過測試,這樣保證限定繪制區域在鏡面上。
// ***********************
// 2. 繪制不透明的反射物體
//
// 開啟反射繪制
m_CBStates.isReflection = true;
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(CBDrawingStates), &m_CBStates, sizeof(CBDrawingStates));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);
// 繪制不透明物體,需要順時針裁剪
// 僅對模板值為1的鏡面區域繪制
m_pd3dImmediateContext->RSSetState(RenderStates::RSCullClockWise.Get());
m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawReflection.Get(), 1);
m_pd3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
m_Walls[2].Draw(m_pd3dImmediateContext.Get());
m_Walls[3].Draw(m_pd3dImmediateContext.Get());
m_Walls[4].Draw(m_pd3dImmediateContext.Get());
m_Floor.Draw(m_pd3dImmediateContext.Get());
到這時候繪制效果如下:
第3步:繪制透明的鏡面反射物體
這一步需要繪制的透明反射物體有籬笆盒以及水面,繪制了這些透明物體后就可以連同鏡面一起混合繪制了。其中籬笆盒要優於水面先行繪制:
// ***********************
// 3. 繪制透明的反射物體
//
// 關閉順逆時針裁剪
// 僅對模板值為1的鏡面區域繪制
// 透明混合
m_pd3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawReflection.Get(), 1);
m_pd3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
m_WireFence.Draw(m_pd3dImmediateContext.Get());
m_Water.Draw(m_pd3dImmediateContext.Get());
m_Mirror.Draw(m_pd3dImmediateContext.Get());
// 關閉反射繪制
m_CBStates.isReflection = false;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(CBDrawingStates), &m_CBStates, sizeof(CBDrawingStates));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);
繪制完后效果如下:
第4步:繪制不透明的正常物體
這一步僅有牆體和地板需要繪制:
// ************************
// 4. 繪制不透明的正常物體
//
m_pd3dImmediateContext->RSSetState(nullptr);
m_pd3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
m_pd3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
for (auto& wall : m_Walls)
wall.Draw(m_pd3dImmediateContext.Get());
m_Floor.Draw(m_pd3dImmediateContext.Get());
第5步:繪制透明的正常物體
// ***********************
// 5. 繪制透明的正常物體
//
// 關閉順逆時針裁剪
// 透明混合
m_pd3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
m_pd3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
m_pd3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
m_WireFence.Draw(m_pd3dImmediateContext.Get());
m_Water.Draw(m_pd3dImmediateContext.Get());
完成所有繪制后,顯示效果如下:
先繪制鏡面場景還是繪制主場景?
一開始我是根據龍書的順序先繪制主場景,再繪制鏡面場景的。但是在繪制帶有透明物體的場景時,會得到下面的結果:
可以看到鏡面下面的部分有黑邊,是因為在繪制主場景的時候,黑色背景和水面產生了混合,並且改寫了深度值,導致在繪制鏡面后面的物體(主要是地板部分)時水面以下的部分沒有通過深度測試,地板也就沒有被繪制。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。