一开始看到平面镜效果我以为要在着色器里写光反射blablabla(我是菜鸟),后来发现,教程中实现平面镜的方法是把场景渲染两次,一次正常渲染,一次镜像渲染,然后设置一个模版将平面镜表面以外的镜像部分剔除掉,只显示镜子中的部分,就像unity里的LayerMask一样。
要做到上面的工作,需要弄明白两个问题
对于问题1,这里就需要用到深度/模版测试接口ID3D11DepthStencilState
。
模板缓冲区(stencil buffer)是一种用来实现特殊效果的离屏(off-screen)缓冲区。模板缓冲的大小与后台缓冲及深度缓冲的大小相同,也就是说,模板缓冲的第ij个像素对应于后台缓冲和深度缓冲第ij个像素。我们在4.1.5节的“注意”中提到,当指定一个模板缓冲时,它总是与深度缓冲共享相同的内存空间。尤如名字所指出的,模板缓冲区的用法就像是模板一样,它可以挡住某些像素片段,不让它们存入后台缓冲。(译者注:比如喷油漆时使用的图案模板,先把模板贴在汽车上或者其他什么地方,然后开始喷油漆。在模板镂空的地方会有油漆喷到汽车上,而没有镂空的地方会挡住油漆。在喷完之后,揭下模板,图案就喷涂在汽车上了。例如,当实现一个镜像效果时,我们需要反射镜子对面的物体;不过,我们希望镜像只显示在镜子里面。我们可以使用模板缓冲区来控制镜像范围,阻止镜像绘制到镜子之外的区域)
我们可以通过ID3D11DepthStencilState接口控制模板缓冲(和深度缓冲)。与混合一样,该接口也提供了一套灵活而强大的功能集合。要学习如何高效地使用模板缓冲区,最有效的方法是仔细研究现有的示例应用程序。当你弄懂了几个使用模板缓冲区的应用程序之后,就会对它有一个更清晰的认识,知道该如何用它来解决实际工作问题。
深度测试、模板测试的执行是在混合操作之前执行的,共享同一个内存空间,具体的执行顺序为:
模板测试→深度测试→混合操作
他们三个都发生在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;
D3D11_DEPTH_WRITE_MASK | 描述 |
---|---|
D3D11_DEPTH_WRITE_MASK_ZERO | 关闭对深度缓冲区的写入 |
D3D11_DEPTH_WRITE_MASK_ALL | 开启对深度缓冲区的写入 |
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
// 不写入像素颜色
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的区域,并且屏蔽掉了颜色
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了