前言
当使用加法/减法/乘法颜色混合,或者使用透明混合的时候,在经过深度测试时可能会引发一些问题。例如现在我们需要使用加法混合来绘制一系列对象,而这些对象彼此之间不会相互阻挡。若我们仍使用原来的深度测试,就必须保证某一像素下的所有片元需要按照从远到近的顺序来进行绘制,但这很难做到,尤其在绘制一些几何体的时候可能会出现在前面的像素片元挡住了后面的像素片元的情况。现在,我们有能力通过调整深度测试这一行为,来改变最终的显示结果。
在此之前需要额外了解的章节如下:
章节回顾 |
---|
11 混合状态与光栅化状态 |
12 深度/模板状态、反射绘制 |
13 动手实现一个简易Effects框架、阴影效果绘制 |
DirectX11 With Windows SDK完整目录
Github项目源码
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
交换律
加法/减法/乘法的混合运算是满足交换律的(对C0到Cn-1来说),这说明我们绘制像素的先后顺序应该是不会对结果产生影响的:
\[ \mathbf{B'}=\mathbf{B}+\mathbf{C_0}+\mathbf{C_1}+...+\mathbf{C_{n-1}} \\ \mathbf{B'}=\mathbf{B}-\mathbf{C_0}-\mathbf{C_1}-...-\mathbf{C_{n-1}} \\ \mathbf{B'}=\mathbf{B} \otimes \mathbf{C_0} \otimes \mathbf{C_1} \otimes ... \otimes \mathbf{C_{n-1}} \]
但是混合的先后顺序会对结果有影响,所以不满足交换律(无论alpha值如何):
\[ \mathbf{B'}= 0.5(0.5\mathbf{B} + 0.5\mathbf{C_0}) + 0.5\mathbf{C_1} = 0.25\mathbf{B} + 0.25\mathbf{C_0} + 0.5\mathbf{C_1} \\ \mathbf{B'}= 0.5(0.5\mathbf{B} + 0.5\mathbf{C_1}) + 0.5\mathbf{C_0} = 0.25\mathbf{B} + 0.25\mathbf{C_1} + 0.5\mathbf{C_0} \]
深度测试
关闭深度测试
在某一个阶段关闭深度测试后,若某一像素位置有新的像素片元出现,那么该像素片元就会直接不经过深度测试来到后面的混合阶段。此时混合没开的话,该像素片元就会直接取代后备缓冲区的对应像素,此时需要按从后到前的顺序来绘制物体才能保证正确的显示效果。但关闭深度测试的一个主要用途是绘制场景内的一系列透明物体。当然,前提是这一堆透明物体中间没有不透明物体在阻挡,否则不透明物体后面的透明物也会被绘制出来。
开启深度测试但关闭深度写入
相比上面的方式,这是一种更为合理的做法。我们只需要在渲染的时候先绘制不透明物体,然后就可以按任意的顺序来绘制透明物体。这是因为当绘制透明物体的时候,若它前面有不透明物体阻挡,则不会通过深度测试。所以十分适合用于处理透明物体和不透明物体相互交叉的情况。
利用深度测试和模板测试来绘制闪电特效与其镜面
然后在绘制的时候,每秒绘制60帧闪电动画,即1秒一个循环。切换下一帧的时候,只需要更换下一张纹理即可。
// 更新闪电动画
mBoltAnim.SetTexture(mBoltSRVs[currBoltFrame].Get());
if (frameTime > 1.0f / 60)
{
currBoltFrame = (currBoltFrame + 1) % 60;
frameTime -= 1.0f / 60;
}
frameTime += dt;
RenderStates类的变化
现在RenderStates
类有如下可用的状态:
class RenderStates
{
public:
template
using ComPtr = Microsoft::WRL::ComPtr;
static void InitAll(ID3D11Device * device);
// 使用ComPtr无需手工释放
public:
static ComPtr RSWireframe; // 光栅化器状态:线框模式
static ComPtr RSNoCull; // 光栅化器状态:无背面裁剪模式
static ComPtr RSCullClockWise; // 光栅化器状态:顺时针裁剪模式
static ComPtr SSLinearWrap; // 采样器状态:线性过滤
static ComPtr SSAnistropicWrap; // 采样器状态:各项异性过滤
static ComPtr BSNoColorWrite; // 混合状态:不写入颜色
static ComPtr BSTransparent; // 混合状态:透明混合
static ComPtr BSAlphaToCoverage; // 混合状态:Alpha-To-Coverage
static ComPtr BSAdditive; // 混合状态:加法混合
static ComPtr DSSWriteStencil; // 深度/模板状态:写入模板值
static ComPtr DSSDrawWithStencil; // 深度/模板状态:对指定模板值的区域进行绘制
static ComPtr DSSNoDoubleBlend; // 深度/模板状态:无二次混合区域
static ComPtr DSSNoDepthTest; // 深度/模板状态:关闭深度测试
static ComPtr DSSNoDepthWrite; // 深度/模板状态:仅深度测试,不写入深度值
static ComPtr DSSNoDepthTestWithStencil; // 深度/模板状态:关闭深度测试,对指定模板值的区域进行绘制
static ComPtr DSSNoDepthWriteWithStencil; // 深度/模板状态:仅深度测试,不写入深度值,对指定模板值的区域进行绘制
};
加法混合
加法混合模式的创建如下:
D3D11_BLEND_DESC blendDesc;
ZeroMemory(&blendDesc, sizeof(blendDesc));
auto& rtDesc = blendDesc.RenderTarget[0];
blendDesc.AlphaToCoverageEnable = false;
blendDesc.IndependentBlendEnable = false;
rtDesc.BlendEnable = true;
// 加法混合模式
// Color = SrcColor + DestColor
// Alpha = SrcAlpha
rtDesc.SrcBlend = D3D11_BLEND_ONE;
rtDesc.DestBlend = D3D11_BLEND_ONE;
rtDesc.BlendOp = D3D11_BLEND_OP_ADD;
rtDesc.SrcBlendAlpha = D3D11_BLEND_ONE;
rtDesc.DestBlendAlpha = D3D11_BLEND_ZERO;
rtDesc.BlendOpAlpha = D3D11_BLEND_OP_ADD;
HR(device->CreateBlendState(&blendDesc, BSAdditive.GetAddressOf()));
关闭深度测试
需要准备好默认情况下的绘制和指定模板值绘制两种情况:
D3D11_DEPTH_STENCIL_DESC dsDesc;
// 关闭深度测试的深度/模板状态
// 若绘制非透明物体,务必严格按照绘制顺序
// 绘制透明物体则不需要担心绘制顺序
// 而默认情况下模板测试就是关闭的
dsDesc.DepthEnable = false;
dsDesc.StencilEnable = false;
HR(device->CreateDepthStencilState(&dsDesc, DSSNoDepthTest.GetAddressOf()));
// 关闭深度测试
// 若绘制非透明物体,务必严格按照绘制顺序
// 绘制透明物体则不需要担心绘制顺序
// 对满足模板值条件的区域才进行绘制
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, DSSNoDepthTestWithStencil.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()));
// 进行深度测试,但不写入深度值的状态
// 若绘制非透明物体时,应使用默认状态
// 绘制透明物体时,使用该状态可以有效确保混合状态的进行
// 并且确保较前的非透明物体可以阻挡较后的一切物体
// 对满足模板值条件的区域才进行绘制
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, DSSNoDepthWriteWithStencil.GetAddressOf()));
BasicEffect类的变化
下面四个方法都是专门用于绘制闪电动画的,使用了加法混合。
BasicEffect::SetDrawBoltAnimNoDepthTest方法
该方法关闭了深度测试,用于绘制闪电动画(但默认并不是使用这个,你需要自行修改代码替换调用来查看区别)
void BasicEffect::SetDrawBoltAnimNoDepthTest(ID3D11DeviceContext * deviceContext)
{
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDepthTest.Get(), 0);
deviceContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}
BasicEffect::SetDrawBoltAnimNoDepthWrite方法
该方法允许深度测试,但关闭深度值写入,用于绘制闪电动画(在程序中默认使用这种模式)
void BasicEffect::SetDrawBoltAnimNoDepthWrite(ID3D11DeviceContext * deviceContext)
{
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);
deviceContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}
BasicEffect::SetDrawBoltAnimNoDepthTestWithStencil方法
该方法关闭了深度测试,用于绘制镜面区域的闪电动画(默认不使用这种模式)
void BasicEffect::SetDrawBoltAnimNoDepthTestWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDepthTestWithStencil.Get(), stencilRef);
deviceContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}
BasicEffect::SetDrawBoltAnimNoDepthWriteWithStencil方法
该方法开启深度测试,但不允许写入深度值,用于绘制镜面区域的闪电动画(默认使用这种模式)
void BasicEffect::SetDrawBoltAnimNoDepthWriteWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDepthWriteWithStencil.Get(), stencilRef);
deviceContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}
场景绘制
现在的场景绘制可以说算是比较复杂的了,需要同时处理透明物体、非透明物体的绘制,以及绘制镜面和阴影的效果。因此严格的按照正确顺序去绘制在这里就变得十分重要。
第1步: 镜面区域写入模板缓冲区
和之前一样,先标记好镜面区域:
// *********************
// 1. 给镜面反射区域写入值1到模板缓冲区
//
m_BasicEffect.SetWriteStencilOnly(m_pd3dImmediateContext.Get(), 1);
m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第2步:绘制不透明的反射物体
// ***********************
// 2. 绘制不透明的反射物体
//
// 开启反射绘制
m_BasicEffect.SetReflectionState(true); // 反射开启
m_BasicEffect.SetRenderDefaultWithStencil(m_pd3dImmediateContext.Get(), 1);
m_Walls[2].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[3].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[4].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Floor.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第3步:绘制不透明反射物体的阴影
// ***********************
// 3. 绘制不透明反射物体的阴影
//
m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true); // 反射开启,阴影开启
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 1);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
// 恢复到原来的状态
m_BasicEffect.SetShadowState(false);
m_WoodCrate.SetMaterial(m_WoodCrateMat);
第4步:绘制需要混合的反射闪电动画和透明物体
// ***********************
// 4. 绘制需要混合的反射闪电动画和透明物体
//
m_BasicEffect.SetDrawBoltAnimNoDepthWriteWithStencil(m_pd3dImmediateContext.Get(), 1);
m_BoltAnim.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_BasicEffect.SetReflectionState(false); // 反射关闭
m_BasicEffect.SetRenderAlphaBlendWithStencil(m_pd3dImmediateContext.Get(), 1);
m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第5步:绘制不透明的正常物体
// ************************
// 5. 绘制不透明的正常物体
//
m_BasicEffect.SetRenderDefault(m_pd3dImmediateContext.Get());
for (auto& wall : m_Walls)
wall.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Floor.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第6步:绘制不透明正常物体的阴影
// ************************
// 6. 绘制不透明正常物体的阴影
//
m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true); // 反射关闭,阴影开启
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 0);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_BasicEffect.SetShadowState(false); // 阴影关闭
m_WoodCrate.SetMaterial(m_WoodCrateMat);
第7步:绘制需要混合的闪电动画
// ************************
// 7. 绘制需要混合的闪电动画
m_BasicEffect.SetDrawBoltAnimNoDepthWrite(m_pd3dImmediateContext.Get());
m_BoltAnim.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
最终动画效果如下:
DirectX11 With Windows SDK完整目录
Github项目源码
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。