前言

深度/模板测试使用的是与后备缓冲区同等分辨率大小的缓冲区,每个元素的一部分连续位用于深度测试,其余的则用作模板测试。两个测试的目的都是为了能够根据深度/模板状态需求的设置来选择需要绘制的像素。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

深度/模板测试

深度测试、模板测试的执行是在混合操作之前执行的,具体的执行顺序为:模板测试→深度测试→混合操作。

深度测试

深度测试需要用到深度/模板缓冲区,对每个像素使用24位或32位来映射物体从世界到NDC坐标系下z的值(即深度,范围[0.0, 1.0])。0.0时达到摄像机的最近可视距离,而1.0时达到摄像机的最远可视距离。若某一像素位置接收到多个像素片元,只有z值最小的像素才会通过最终的深度测试。具体细化的话,就是现在有一个像素片元,已知它的深度值,然后需要跟深度缓冲区中的深度值进行比较,若小于深度缓冲区的深度值,则该像素片元将会覆盖后备缓冲区原来的像素片元,并更新深度缓冲区中对应位置的深度值。

模板测试

除了深度测试以为,我们还可以设定模板测试来阻挡某些特定的区域的像素通过后备缓冲区。而且模板测试在操作上自由度会比深度测试大。对于需要进行模板测试的像素,比较式如下:

(StencilRef & StencilReadMask) ⊴ (Value & StencilReadMask)

该表达式首先括号部分是两个操作数进行与运算,然后通过⊴(用户指定的运算符)对两个结果进行比较。若该表达式的值为真,则最终通过模板测试,并保留该像素进行后续的混合操作。

其中StencilReadMask则是应用程序所提供的掩码值。

深度/模板格式

深度/模板缓冲区是一个2D数组(纹理),它必须经由确定的数据格式创建:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:每个元素占64位,其中32位浮点数用于深度测试,8位无符号整数用于模板测试,剩余24位仅用于填充。

  2. 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;

深度状态设定

  1. DepthEnable:如果关闭了深度测试,则绘制的先后顺序就十分重要了。对于不透明的物体,必须按照从后到前的顺序进行绘制,否则最后绘制的内容会覆盖之前的内容,看起来就像在最前面那样。并且关闭深度测试会导致深度缓冲区的值会保持原样,不再进行更新,此时DepthWriteMask也不会使用。

  2. D3D11_DEPTH_WRITE_MASK枚举类型只有两种枚举值:

枚举值 含义
D3D11_DEPTH_WRITE_MASK_ZERO 不写入深度/模板缓冲区
D3D11_DEPTH_WRITE_MASK_ALL 允许写入深度/模板缓冲区

但即便设置了D3D11_DEPTH_WRITE_MASK_ZERO,如果DepthEnable开着的话仍会取原来的深度值进行深度比较,只是不会更新深度缓冲区。

  1. 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

模板状态设定

  1. StencilEnable:若要使用模板测试,则指定为true
  2. StencilReadMask:该掩码用于指定StencilRef和深度/模板缓冲区的模板值Value中的某些特定位,默认使用的是下面宏常量:

    #define D3D11_DEFAULT_STENCIL_READ_MASK (0xff)
  3. StencilWriteMask:该掩码指定待写入的模板值的哪些位要写入深度/模板缓冲区中,默认使用的是下面宏常量:

    #define D3D11_DEFAULT_STENCIL_WRITE_MASK (0xff)
  4. FrontFace:该结构体指定了不同测试结果下对模板值应做什么样的更新(对于正面朝向的三角形)
  5. 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;
  1. StencilFailOp:若模板测试不通过对深度/模板缓冲区的模板值部分的操作
  2. StencilDepthFailOp:若模板测试通过,但深度测试不通过对深度/模板缓冲区的模板值部分的操作
  3. StencilPassOp:若模板/深度测试通过对深度/模板缓冲区的模板值部分的操作
  4. 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. 如何计算出一个物体的所有顶点在任意平面的镜面的反射位置
  2. 在镜面位置只显示镜面本身和反射的物体的混合

若一个有平面镜的场景中包含透明和非透明物体,则实际的绘制顺序为:

  1. 只向镜面区域的模板缓冲区写入值1,而深度缓冲区和后备缓冲区的值都不应该写入
  2. 将需要绘制的镜面反射物体进行反射变换,然后仅在模板值为1的区域先绘制不透明的反射物体到后备缓冲区
  3. 在模板值为1的区域绘制透明的反射物体后,再绘制透明镜面到后备缓冲区
  4. 绘制正常的非透明物体到后备缓冲区
  5. 绘制透明物体到后备缓冲区

在3D场景中,要绘制镜面反射的物体,我们只需要将原本的物体(所有顶点位置)进行镜面反射矩阵的变换即可得到。但是反射的物体仅可以在物体一侧透过镜面看到,在镜面的另一边是无法看到反射的物体的。通过模板测试,我们可以在摄像机仅与镜面同侧的时候标定镜面区域,并绘制镜面反射的物体。

我们可以使用XMMatrixReflection函数来创建反射矩阵,提供的参数为平面向量\((\mathbf{n} ,d)\)

这里简单了解一下,平面可以表示为点法式:

\[\mathbf{n} \cdot \mathbf{p} + d = 0
\]

n为平面法向量,p为平面一点,进行叉乘运算。

d是一个有向距离值

上面的式子展开后就是我们高数见到的平面方程:

\[Ax + By + Cz + D = 0
\]

这相当于我

例如(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完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

DirectX11 With Windows SDK--12 深度/模板状态、平面镜反射绘制的更多相关文章

  1. DirectX11 With Windows SDK--09 纹理映射与采样器状态

    前言 在之前的DirectX SDK中,纹理的读取使用的是D3DX11CreateShaderResourceViewFromFile函数,现在在Windows SDK中已经没有这些函数,我们需要找到 ...

  2. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  3. DirectX11 With Windows SDK--13 动手实现一个简易Effects框架、阴影效果绘制

    前言 到现在为止,所有的教程项目都没有使用Effects11框架类来管理资源.因为在D3DCompile API (#47)版本中,如果你尝试编译fx_5_0的效果文件,会收到这样的警告: X4717 ...

  4. DirectX11 With Windows SDK--14 深度测试

    前言 当使用加法/减法/乘法颜色混合,或者使用透明混合的时候,在经过深度测试时可能会引发一些问题.例如现在我们需要使用加法混合来绘制一系列对象,而这些对象彼此之间不会相互阻挡.若我们仍使用原来的深度测 ...

  5. DirectX11 With Windows SDK--00 目录

    前言 (更新于 2019/4/10) 从第一次接触DirectX 11到现在已经有将近两年的时间了.还记得前年暑假被要求学习DirectX 11,在用龙书的源码配置项目运行环境的时候都花了好几天的时间 ...

  6. DirectX11 With Windows SDK--29 计算着色器:内存模型、线程同步;实现顺序无关透明度(OIT)

    前言 由于透明混合在不同的绘制顺序下结果会不同,这就要求绘制前要对物体进行排序,然后再从后往前渲染.但即便是仅渲染一个物体(如上一章的水波),也会出现透明绘制顺序不对的情况,普通的绘制是无法避免的.如 ...

  7. DirectX11 With Windows SDK--07 添加光照与常用几何模型、光栅化状态

    原文:DirectX11 With Windows SDK--07 添加光照与常用几何模型.光栅化状态 前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的 ...

  8. DirectX11 With Windows SDK--11 混合状态与光栅化状态

    前言 虽然这一部分的内容主要偏向于混合(Blending),但这里还需提及一下,关于渲染管线可以绑定的状态主要有如下四种: 光栅化状态(光栅化阶段) 采样器状态(像素着色阶段) 混合状态(输出合并阶段 ...

  9. DirectX11 With Windows SDK--11 混合状态

    原文:DirectX11 With Windows SDK--11 混合状态 前言 这一章会着重讲述混合状态,在下一章则会讲述深度/模板状态 DirectX11 With Windows SDK完整目 ...

随机推荐

  1. Server 2008 R2多用户远程桌面连接授权,解决120天过期问题

    在工作中,我们往往需要远程服务器,经常会遇到以下这两个麻烦事. 一.远程桌面的连接数限制,超出系统就会提示超过连接数. 二.远程桌面连接时,同一个用户不能同时远程2个桌面连接. ----------- ...

  2. jquery/js记录点击事件,单击次数加一,双击清零

    目的:点击按钮,点击后在网页上显示点击次数,双击清零 实现:js或者jquery 代码如下: <%@ page language="java" contentType=&qu ...

  3. Django组件--分页器(有用)

    一.分页器对象 from django.core.paginator import Paginator,EmptyPage book_list = Book.objects.all() #假设有100 ...

  4. ESP8266当中继

    WiFi推原理(转) http://jb.tongxinmao.com/Article/Detail/id/412 https://www.anywlan.com/thread-409913-1-1. ...

  5. Redis数据持久化、数据备份、数据的故障恢复

    1.redis持久化的意义----redis故障恢复 在实际的生产环境中,很可能会遇到redis突然挂掉的情况,比如redis的进程死掉了.电缆被施工队挖了(支付宝例子)等等,总之一定会遇到各种奇葩的 ...

  6. 故障公告:docker swarm集群“群龙无首”造成部分站点无法访问

    今天傍晚 17:38-18:18 左右,由于 docker swarm 集群出现 "The swarm does not have a leader" 问题,造成博问.闪存.园子. ...

  7. C#中UDP数据的发送、接收

    Visual C# UDP数据的发送、接收包使用的主要类及其用法: 用Visual C# UDP协议的实现,最为常用,也是最为关键的类就是UdpClient,UdpClient位于命名空间System ...

  8. 关于H5页面中生成图片的两种方式!

    前言: 我们在做项目过程中,经常会遇到自定义生成一张图片并可以长按保存.长按保存图片在微信等浏览器中默认就有,那么对于生成图片的有哪些方式呢? 方法一: 利用canvas绘制图形,然后生成图片 代码如 ...

  9. springboot打成jar后文件读取问题

    springboot打成的jar包里面不能用File去获取文件对象,只能用流的方式去读取. 获取方式: InputStream resourceAsStream  = 类名.class.getClas ...

  10. Python——collections模块(扩展数据类型)

    1.namedtuple:利用坐标.空间坐标,扑克牌等指定空间位置 # namedtuple('名字',[list列表属性])from collections import namedtuple Po ...