DirectX11学习笔记八 平面镜 模版测试

  一开始看到平面镜效果我以为要在着色器里写光反射blablabla(我是菜鸟),后来发现,教程中实现平面镜的方法是把场景渲染两次,一次正常渲染,一次镜像渲染,然后设置一个模版将平面镜表面以外的镜像部分剔除掉,只显示镜子中的部分,就像unity里的LayerMask一样。
要做到上面的工作,需要弄明白两个问题

  • 如何剔除平面镜以外的镜像部分
  • 如何渲染镜像的部分

  对于问题1,这里就需要用到深度/模版测试接口ID3D11DepthStencilState

模板缓冲区(stencil buffer)是一种用来实现特殊效果的离屏(off-screen)缓冲区。模板缓冲的大小与后台缓冲及深度缓冲的大小相同,也就是说,模板缓冲的第ij个像素对应于后台缓冲和深度缓冲第ij个像素。我们在4.1.5节的“注意”中提到,当指定一个模板缓冲时,它总是与深度缓冲共享相同的内存空间。尤如名字所指出的,模板缓冲区的用法就像是模板一样,它可以挡住某些像素片段,不让它们存入后台缓冲。(译者注:比如喷油漆时使用的图案模板,先把模板贴在汽车上或者其他什么地方,然后开始喷油漆。在模板镂空的地方会有油漆喷到汽车上,而没有镂空的地方会挡住油漆。在喷完之后,揭下模板,图案就喷涂在汽车上了。例如,当实现一个镜像效果时,我们需要反射镜子对面的物体;不过,我们希望镜像只显示在镜子里面。我们可以使用模板缓冲区来控制镜像范围,阻止镜像绘制到镜子之外的区域)

  我们可以通过ID3D11DepthStencilState接口控制模板缓冲(和深度缓冲)。与混合一样,该接口也提供了一套灵活而强大的功能集合。要学习如何高效地使用模板缓冲区,最有效的方法是仔细研究现有的示例应用程序。当你弄懂了几个使用模板缓冲区的应用程序之后,就会对它有一个更清晰的认识,知道该如何用它来解决实际工作问题。
DirectX11学习笔记八 平面镜 模版测试_第1张图片
  深度测试、模板测试的执行是在混合操作之前执行的,共享同一个内存空间,具体的执行顺序为:
模板测试→深度测试→混合操作
  他们三个都发生在Output-Merger输出合并阶段。在启用模板功能之后,每个光栅化像素都要与下面的两个操作数进行模板测试:

if( StencilRef & StencilReadMask  ⊴  Value &StencilReadMask) 
    accept pixel 
else
    reject pixel`

  1.左操作数(StencilRef & StencilReadMask)由应用程序指定的一个模板参考值(StencilRef)和一个模板掩码(StencilReadMask)进行按位与运算得到。
  2.右操作数(Value &StencilReadMask)由当前像素在模板缓冲区中的对应值(Value)和一个模板掩码(StencilReadMask)进行按位与运算得到。
如果像素无法通过模板测试,则直接丢弃,不参与深度测试;反之,则继续进行深度测试。
运算符⊴可以是D3D11_COMPARISON_FUNC枚举类型定义的任何一个函数:

D3D11_COMPARISON_FUNC 描述
D3D11_COMPARISON_NEVER 始终返回false
D3D11_COMPARISON_LESS <
D3D11_COMPARISON_EQUAL ==
D3D11_COMPARISON_LESS_EQUAL <=
D3D11_COMPARISON_GREATER >
D3D11_COMPARISON_NOT_EQUAL !=
D3D11_COMPARISON_GREATER_EQUAL >=
D3D11_COMPARISON_ALWAYS 始终返回true

创建模版/深度缓冲区接口

创建该接口需要先定义描述
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. 如果不开启深度测试,则绘制顺序影响像素的先后,就像unity的UGUI那样,先渲染的被后渲染的盖住。
  2. D3D11_DEPTH_WRITE_MASK 深度值写入掩码有两种描述
D3D11_DEPTH_WRITE_MASK 描述
D3D11_DEPTH_WRITE_MASK_ZERO 关闭对深度缓冲区的写入
D3D11_DEPTH_WRITE_MASK_ALL 开启对深度缓冲区的写入
  1. DepthFunc定义两种像素的深度值比较的办法,跟模版值比较的办法一样,一般情况都用Less,即现实中的透视关系。
  2. 是否开启模版测试
  3. 模版值读取掩码,对应模版函数中的StencilReadMask,默认掩码不屏蔽任何二进制位
  4. 模版值写入掩码,当更新模板缓冲区时,我们可以通过掩码来屏蔽某些二进制位,不让它们存入模板缓冲区。例如,当你希望屏蔽前4位数据时,可将掩码设为0x0f。默认掩码不屏蔽任何二进制位:
  5. D3D11_DEPTH_STENCILOP_DESC就是描述模版测试操作的
D3D11_DEPTH_STENCILOP_DESC 描述
StencilFailOp 模版测试失败时的操作
StencilDepthFailOp 模版测试通过而深度测试不通过的操作
StencilPassOp 模版/深度测试通过时的操作
StencilFunc 模版测试所用的比较函数

  StencilFunc类型为D3D11_COMPARISON_FUNC ,跟深度测试的比较符号操作是一样的。
  上面三个Op的数据类型为D3D11_STENCIL_OP

D3D11_STENCIL_OP 描述
D3D11_STENCIL_OP_KEEP 保留存在的模版数据
D3D11_STENCIL_OP_ZERO 将模版数据设为0
D3D11_STENCIL_OP_REPLACE 将模版数据替换为StencilRef,StencilRef 由ID3D11DeviceContext::OMSetDepthStencilState(ID3D11DepthStencilState,StencilRef)定义
D3D11_STENCIL_OP_INCR_SAT 得到Clamp(模版值+1,255)
D3D11_STENCIL_OP_DECR_SAT 得到Clamp(0,模版值-1)
D3D11_STENCIL_OP_INVERT 将模版值按位反转
D3D11_STENCIL_OP_INCR 对目标模板值加1,超过255的话值将上溢变成0
D3D11_STENCIL_OP_DECR 对目标模板值减1,低于0的话将下溢变成255

  填充完模版/深度缓冲区描述后,就可以创建了

HRESULT CreateDepthStencilState(
  const D3D11_DEPTH_STENCIL_DESC *pDepthStencilDesc,  //深度/模版缓冲区描述
  ID3D11DepthStencilState **ppDepthStencilState  //要接收创建出来的缓冲区的句柄(深度/模版测试接口)
);

清除和创建深度/模版缓冲区

  在一帧开始前先清除模版缓冲区

void ClearDepthStencilView(
  ID3D11DepthStencilView *pDepthStencilView,  //之前创建的深度模版视图
  UINT                   ClearFlags,  //要清除的类型,一共就两种
  FLOAT                  Depth,  //深度缓冲区的替换值
  UINT8                  Stencil  //
);
m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

  在draw前绑定模版缓冲区,即可

void OMSetDepthStencilState(
  ID3D11DepthStencilState *pDepthStencilState,
  UINT                    StencilRef  //模版函数中的StencilRef
);
m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), 1);

实现平面镜效果

  • 第一步,将平面镜像素在内存中相应的位置的模版值设为1,丢弃掉平面镜的像素
	// 裁剪掉背面三角形
	// 标记镜面区域的模板值为1
	// 不写入像素颜色
	m_pd3dImmediateContext->RSSetState(nullptr);
	m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSWriteStencil.Get(), 1);
	m_pd3dImmediateContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);
	m_Mirror.Draw(m_pd3dImmediateContext.Get());

   其中渲染状态的定义

   // 镜面标记深度/模板状态
   // 无论是正面还是背面,原来指定的区域的模板值都会被写入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;  //模版/深度测试通过就将模版值设为StencilRef
   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, DSSWriteStencil.GetAddressOf()));

   // 无颜色写入混合模式
   // Color = DestColor
   // Alpha = DestAlpha
   blendDesc.AlphaToCoverageEnable = false;  //混合全部关闭,屏蔽掉像素着色器的返回值
   rtDesc.BlendEnable = false;
   HR(device->CreateBlendState(&blendDesc, BSNoColorWrite.GetAddressOf()));

   这样我们就将平面镜画成一个长方形的模版为1的区域,并且屏蔽掉了颜色

  • 第二步,画正常场景和镜像场景
    其中,两者是要考虑绘制顺序的
    两种办法,1绘制顺序:不透明正常场景 不透明镜像场景 透明镜像场景 透明正常场景
         2绘制顺序:不透明镜像场景 透明镜像场景 不透明正常场景 透明正常场景 其中镜子要作为透明镜像场景渲染,原博客的顺序
      那么问题来了,如何渲染镜像场景?
    首先我们在初始化时可以得到镜子的坐标,0.0f, 3.0f, 10.0f,且正方向对着-Z轴,也就是法线为(0,0,-1),镜子所在平面方程为0X+0Y+(-1)Z+10=0,那么反射矩阵为
m_CBRarely.reflection = XMMatrixTranspose(XMMatrixReflect(XMVectorSet(0.0f, 0.0f, -1.0f, 10.0f)));`

   在cpp中向着色器传入反射矩阵和是否启动反射的bool值,来判断是否对法线和光照方向位置乘上一个反射矩阵。

#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;
}
#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;
}

   如何将镜像场景进行模版测试?

	D3D11_DEPTH_STENCIL_DESC dsDesc;
   // 反射绘制深度/模板状态
   // 由于要绘制反射镜面,需要更新深度
   // 仅当镜面标记模板值和当前设置模板值相等时才会进行绘制
   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, DSSDrawWithStencil.GetAddressOf()));

  换成模版函数就是

if( StencilRef ==  1) 
    accept pixel 
else
    reject pixel`

  前面是将模版测试设为永远通过且通过时将模版值设为1,那么我们渲染镜像场景时将StencilRef设为1就可以,渲染正常场景时关闭模版测试即可。另外注意,不透明的镜像场景由于镜像导致顶点顺序反转,逆时针变顺时针,所以需要将逆时针剔除变成顺时针剔除,或者不剔除。透明场景不剔除。
  场景绘制完整代码

m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black));
	m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	
	
	// ******************
	// 1. 给镜面反射区域写入值1到模板缓冲区
	// 

	// 裁剪掉背面三角形
	// 标记镜面区域的模板值为1
	// 不写入像素颜色
	m_pd3dImmediateContext->RSSetState(nullptr);
	m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSWriteStencil.Get(), 1);
	m_pd3dImmediateContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);


	m_Mirror.Draw(m_pd3dImmediateContext.Get());

	// ******************
	// 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::DSSDrawWithStencil.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. 绘制透明的反射物体
	//

	// 关闭顺逆时针裁剪
	// 仅对模板值为1的镜面区域绘制
	// 透明混合
	m_pd3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
	m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.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. 绘制不透明的正常物体
	//

	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. 绘制透明的正常物体
	//

	// 关闭顺逆时针裁剪
	// 透明混合
	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());

从下一节开始我想脱离教程源码,自己写demo了

你可能感兴趣的:(DirectX)