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

前言

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

DirectX11 With Windows SDK完整目录git

Github项目源码github

欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。数组

深度/模板测试

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

深度测试

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

模板测试

除了深度测试觉得,咱们还能够设定模板测试来阻挡某些特定的区域的像素经过后备缓冲区。并且模板测试在操做上自由度会比深度测试大。对于须要进行模板测试的像素,比较式以下:
(StencilRef & StencilReadMask) ⊴ (Value & StencilReadMask)测试

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

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

深度/模板格式

深度/模板缓冲区是一个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场景中,要绘制镜面反射的物体,咱们只须要将本来的物体(全部顶点位置)进行镜面反射矩阵的变换便可获得。可是反射的物体仅能够在物体一侧透过镜面看到,在镜面的另外一边是没法看到反射的物体的。经过模板测试,咱们能够在摄像机仅与镜面同侧的时候标定镜面区域,并绘制镜面反射的物体。

image

咱们可使用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步:绘制不透明的镜面反射物体

理论上会有三面墙和地板可能会透过镜面看到,这里都须要绘制,但要注意在对顶点位置作反射变换时,原来平面向外的法向量变成了平面向内部,所以还须要额外对法向量作反射变换(龙书缺乏了对法向量的反射变换)。而且原来按顺时针排布的三角形顶点也变成了逆时针排布。因此须要对顺时针排布的顶点作裁剪处理。

image

在作模板测试的时候,咱们仅对模板值为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,以及有什么问题也能够在这里汇报。

相关文章
相关标签/搜索