补充提一句关于渲染管线可以绑定的状态主要有如下四种:
光栅化状态(光栅化阶段)采样器状态(像素着色阶段)混合状态(输出合并阶段)深度/模板状态(输出合并阶段)对于两个相同位置的像素点,规定Csrc为源像素的颜色(从像素着色器输出的像素),Cdst为目标像素的颜色(已经存在于后备缓冲区上的像素)。在Direct3D中使用下面的混合等式来将源像素色和目标像素色进行混合。
ID3D11Device::CreateBlendState方法–创建混合状态 在创建混合状态前,需要填充D3D11_BLEND_DESC结构体:
typedef struct D3D11_BLEND_DESC { BOOL AlphaToCoverageEnable; // 默认关闭,这里 BOOL IndependentBlendEnable; // 是否每个渲染目标都有独立的混合混合描述,关闭的话都使用索引为0的描述信息 D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[ 8 ]; } D3D11_BLEND_DESC; typedef struct D3D11_RENDER_TARGET_BLEND_DESC { BOOL BlendEnable; // 是否开启混合 D3D11_BLEND SrcBlend; // 源颜色混合因子 D3D11_BLEND DestBlend; // 目标颜色混合因子 D3D11_BLEND_OP BlendOp; // 颜色混合运算符 D3D11_BLEND SrcBlendAlpha; // 源Alpha混合因子 D3D11_BLEND DestBlendAlpha; // 目标Alpha混合因子 D3D11_BLEND_OP BlendOpAlpha; // Alpha混合运算符 UINT8 RenderTargetWriteMask; // D3D11_COLOR_WRITE_ENABLE枚举类型来指定可以写入的颜色 } D3D11_RENDER_TARGET_BLEND_DESC;混合运算符的设置 对于运算符 ⊞ 的含义,可以使用下面的枚举类型D3D11_BLEND_OP来描述: 你可以分开指定运算颜色和Alpha通道的运算符。
混合因子的设置 对于混合公式,我们可以按需要设置混合因子。混合因子使用枚举类型D3D11_BLEND类型进行描述: 其中sat函数将值限定在[0.0, 1.0]之间。
可写入的颜色 枚举类型RenderTargetWriteMask有如下枚举值:
若你想指定红色和ALPHA通道可以写入,可以用位运算与结合起来,即D3D11_COLOR_WRITE_ENABLE_RED | D3D11_COLOR_WRITE_ENABLE_ALPHA
ID3D11Device::CreateBlendState含义如下:
HRESULT ID3D11Device::CreateBlendState( const D3D11_BLEND_DESC *pBlendStateDesc, // [In]混合状态描述 ID3D11BlendState **ppBlendState); // [Out]输出混合状态ID3D11DeviceContext::OMSetBlendState方法–输出合并阶段设置混合状态 方法如下:
void ID3D11DeviceContext::OMSetBlendState( ID3D11BlendState *pBlendState, // [In]混合状态,如果要使用默认混合状态则提供nullptr const FLOAT [4] BlendFactor, // [In]混合因子,如不需要可以为nullptr UINT SampleMask); // [In]采样掩码,默认为0xffffffff默认混合状态如下:
AlphaToCoverageEnable = false; IndependentBlendEnable = false; RenderTarget[0].BlendEnable = false; RenderTarget[0].SrcBlend = D3D11_BLEND_ONE RenderTarget[0].DestBlend = D3D11_BLEND_ZERO RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL采样掩码的设置主要是针对多重采样的操作,若采样掩码的第i位为0,则对应第i次采样将不进行,但这得在实际上进行不小于i次的采样时才会起作用。通常情况下设为0xffffffff来允许所有采样操作。
深度测试、模板测试的执行是在混合操作之前执行的,具体的执行顺序为:模板测试→深度测试→混合操作。
深度测试 深度测试需要用到深度/模板缓冲区,对每个像素使用24位或32位来映射物体从世界到NDC坐标系下z的值(即深度,范围[0.0, 1.0])。0.0时达到摄像机的最近可视距离,而1.0时达到摄像机的最远可视距离。若某一像素位置接收到多个像素片元,只有z值最小的像素才会通过最终的深度测试。具体细化的话,就是现在有一个像素片元,已知它的深度值,然后需要跟深度缓冲区中的深度值进行比较,若小于深度缓冲区的深度值,则该像素片元将会覆盖后备缓冲区原来的像素片元,并更新深度缓冲区中对应位置的深度值。
模板测试 除了深度测试以为,我们还可以设定模板测试来阻挡某些特定的区域的像素通过后备缓冲区。而且模板测试在操作上自由度会比深度测试大。对于需要进行模板测试的像素,比较式如下: (StencilRef & StencilReadMask) ⊴ (Value & StencilReadMask)
该表达式首先括号部分是两个操作数进行与运算,然后通过⊴(用户指定的运算符)对两个结果进行比较。若该表达式的值为真,则最终通过模板测试,并保留该像素进行后续的混合操作。
其中StencilReadMask则是应用程序所提供的掩码值。
深度/模板格式 深度/模板缓冲区是一个2D数组(纹理),它必须经由确定的数据格式创建:
DXGI_FORMAT_D32_FLOAT_S8X24_UINT:每个元素占64位,其中32位浮点数用于深度测试,8位无符号整数用于模板测试,剩余24位仅用于填充。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有如下枚举值: 可以使用位运算或来同时清理。
通常深度值会默认设为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;默认情况下,深度状态的值如下:
DepthEnable = TRUE; DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL DepthFunc = D3D11_COMPARISION_LESS深度/模板操作描述结构体如下:
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;StencilFailOp:若模板测试不通过对深度/模板缓冲区的模板值部分的操作
StencilDepthFailOp:若模板测试通过,但深度测试不通过对深度/模板缓冲区的模板值部分的操作
StencilPassOp:若模板/深度测试通过对深度/模板缓冲区的模板值部分的操作
StencilFunc:模板测试所用的比较函数
枚举类型D3D11_STENCIL_OP的枚举值如下: 默认情况下,模板状态的值如下:
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,而深度缓冲区和后备缓冲区的值都不应该写入将需要绘制的镜面反射物体进行反射变换,然后仅在模板值为1的区域先绘制不透明的反射物体到后备缓冲区在模板值为1的区域绘制透明的反射物体后,再绘制透明镜面到后备缓冲区绘制正常的非透明物体到后备缓冲区绘制透明物体到后备缓冲区在3D场景中,要绘制镜面反射的物体,我们只需要将原本的物体(所有顶点位置)进行镜面反射矩阵的变换即可得到。但是反射的物体仅可以在物体一侧透过镜面看到,在镜面的另一边是无法看到反射的物体的。通过模板测试,我们可以在摄像机仅与镜面同侧的时候标定镜面区域,并绘制镜面反射的物体。 我们可以使用XMMatrixReflection函数来创建反射矩阵,提供的参数为平面向量( n , d )。
管线状态类头文件伪代码及实现
class RenderStates { public: static ID3D11RasterizerState RSWireframe; // 光栅化器状态:线框模式 static ID3D11RasterizerState RSNoCull; // 光栅化器状态:无背面裁剪模式 static ID3D11RasterizerState RSCullClockWise; // 光栅化器状态:顺时针裁剪模式 static ID3D11SamplerState SSLinear; // 采样器状态:线性过滤 static ID3D11SamplerState SSAnistropic; // 采样器状态:各项异性过滤 static ID3D11BlendState BSNoColorWrite; // 混合状态:不写入颜色 static ID3D11BlendState BSTransparent; // 混合状态:透明混合 static ID3D11BlendState BSAlphaToCoverage; // 混合状态:Alpha-To-Coverage static ID3D11DepthStencilState DSSMarkMirror; // 深度/模板状态:标记镜面区域 static ID3D11DepthStencilState DSSDrawReflection; // 深度/模板状态:绘制反射区域 static ID3D11DepthStencilState DSSNoDoubleBlend; // 深度/模板状态:无二次混合区域 static ID3D11DepthStencilState DSSNoDepthTest; // 深度/模板状态:关闭深度测试 static ID3D11DepthStencilState DSSNoDepthWrite; // 深度/模板状态:仅深度测试,不写入深度值 }; void RenderStates::InitAll(ID3D11Device * device) { // ***********初始化光栅化器状态*********** 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 // 不写入像素颜色 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());此时,可以看到模板写入平面镜,模板值为1的渲染图效果如下:
理论上会有三面墙和地板可能会透过镜面看到,这里都需要绘制,但要注意在对顶点位置做反射变换时,原来平面向外的法向量变成了平面向内部,因此还需要额外对法向量做反射变换(龙书缺少了对法向量的反射变换)。并且原来按顺时针排布的三角形顶点也变成了逆时针排布。所以需要对顺时针排布的顶点做裁剪处理。 在做模板测试的时候,我们仅对模板值为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. 绘制透明的反射物体 // // 关闭顺逆时针裁剪 // 仅对模板值为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. 绘制不透明的正常物体 // 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());此时,绘制效果如下:
完成所有绘制后,显示效果如下: