DirectX11 With Windows SDK--12 深度/模板状态、平面镜反射绘制

前言

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

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,以及有什么问题也可以在这里汇报。

posted @ 2018-07-28 13:04  X_Jun  阅读(5008)  评论(1编辑  收藏  举报
levels of contents