我们从地形和板条箱开始渲染场景,先绘制地形,再绘制板条箱,使地形和板条箱的像素依次存入后台缓冲区。然后使用混合(blending),将水体绘制到后台缓冲区,使水体像素和后台缓冲区中的地形、板条箱像素融为一体。通过一方式,我们可以透过水体看到地形和板条箱。本章我们主要讲解混合技术,它可以将当前的光栅化像素(也称为源像素)与后台缓冲区中的像素(也称为目标像素)混合(融合)在一起。该技术通常用于渲染半透明物体,比如水和玻璃。
在输出合并(output merger,简称OM)阶段中,某些像素片段会被丢弃(例如,未能通过深度测试或模板测试)。未丢弃的像素片段会被写入后台缓冲区。混合(blending)工作是在该阶段中完成的,一个像素可以与后台缓冲区中的当前像素进行混合,并以混合后的值作为该像素的最终颜色。某些特殊效果,比如透明度,就是通过混合来实现的。
透明效果在DirectX中时需要PS和api同时设置才能实现的,如果在c++的api中不开启混合,那么即使在PS中设置了颜色的a值,最终渲染时a也会被丢弃。
在PS里的a值计算很好理解,就是类似
float4 litColor = texColor * (ambient + diffuse) + spec;
litColor.a = texColor.a * g_Material.Diffuse.a;
return litColor;
将最后计算出来的纹理透明度和材质透明度做乘法。
但是实际上,某个像素的颜色算出后会存到后备缓冲区中,通过z深度与其他像素排序,并计算透明度叠加效果,比如可以透过透明的玻璃看到小明,但小明身后的墙壁是看不到的(墙壁没通过深度测试,直接被丢弃),例如,我们设Csrc = 玻璃,Cdst = 小明,C = 最终颜色。设Csrc为当前正在进行光栅化处理的第ij个像素(源像素)的颜色,Cdst为后台缓冲区中的第ij个像素(目标像素)的颜色。当不使用混合时,Csrc会覆盖Cdst的值(假设该值已通过深度/模板测试)并成为后台缓冲区中的第ij个像素的新颜色。但是当使用混合时,Csrc和Cdst会被组合为一个新颜色C并覆盖Cdst的值(即,混合颜色C会成为后台缓冲区中的第ij个像素的新颜色)
还需要在API中设置前后两个像素叠加的方式。所以渲染次序也需要修改,我们先渲染不透明的物体,而对于透明的物体,则按照从远到近的次序渲染。对于本次实例,我们先渲染的篱笆盒,其次是水,因为篱笆盒有一部分在水下,如果反过来,则先渲染水再渲染篱笆盒,因为篱笆盒在水下的部分深度比水面大,无法通过深度测试而被丢弃。所以在绘制透明物体前,要么关闭深度测试,要么对物体到摄像机的先后顺序进行排序,并按从后到前的顺序进行绘制。
下面就要引入混合方程的概念,这些都是API帮我们处理的,不需要修改hlsl。
混合方程
首先Csrc和Cdst都是float3,即rgb
基本方程为
C = Csrc * Fsrc op1 Cdst * Fdst
A = Asrc * Fsrc op2 Adst * Fdst
其中,*表示分量乘法rgb都要算;op则表示一个未知运算符,这个运算符具体是什么,通过api来设定;Fsrc和Fdst,混合因子,跟op一样,也要去api设定。
注意:我们很少混合alpha分量,多数情况下,我们只是对RGB分量进行混合。尤其是在本书中,我们从不混合源alpha值和目标alpha值,虽然alpha值会参与我们的RGB混合操作。这主要是因为我们暂时用不到后台缓冲区中的alpha值。只有当某些算法需要使用目标alpha值时,后台缓冲区中的alpha值才有用。以小明为例,窗户为透明,需要将玻璃的颜色混合到玻璃后的小明身上,使小明出现一种被玻璃阻挡的感觉,但透明度不需要混合,因为小明不是透明的,也不需要透明,其A值永远是1。
渲染状态
RenderStates实际上就是将光栅化阶段描述、混合阶段描述和采样器等等需要额外创建的资源的创建代码与GameApp分隔开,整理下流程。
class RenderStates
{
public:
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
static bool IsInit();
static void InitAll(ID3D11Device * device);
// 使用ComPtr无需手工释放
public:
static ComPtr<ID3D11RasterizerState> RSWireframe; // 光栅化器状态:线框模式
static ComPtr<ID3D11RasterizerState> RSNoCull; // 光栅化器状态:无背面裁剪模式
static ComPtr<ID3D11SamplerState> SSLinearWrap; // 采样器状态:线性过滤
static ComPtr<ID3D11SamplerState> SSAnistropicWrap; // 采样器状态:各项异性过滤
static ComPtr<ID3D11BlendState> BSNoColorWrite; // 混合状态:不写入颜
static ComPtr<ID3D11BlendState> BSTransparent; // 混合状态:透明混合
static ComPtr<ID3D11BlendState> BSAlphaToCoverage; // 混合状态:Alpha-To-Coverage
};
这里要引入两个新的类
ID3D11RasterizerState
参考官方文档,用来描述光栅化阶段的操作,创建的套路跟缓冲区,采样器什么的差不多,也是需要一个D3D11_RASTERIZER_DESC的描述对象,不如直接看代码。
D3D11_RASTERIZER_DESC rasterizerDesc;
ZeroMemory(&rasterizerDesc, sizeof(rasterizerDesc));
// 线框模式
rasterizerDesc.FillMode = D3D11_FILL_WIREFRAME;
rasterizerDesc.CullMode = D3D11_CULL_NONE;
rasterizerDesc.FrontCounterClockwise = false;
rasterizerDesc.DepthClipEnable = true;
HR(device->CreateRasterizerState(&rasterizerDesc, RSWireframe.GetAddressOf()));
// 无背面剔除模式
rasterizerDesc.FillMode = D3D11_FILL_SOLID;
rasterizerDesc.CullMode = D3D11_CULL_NONE;
rasterizerDesc.FrontCounterClockwise = false;
rasterizerDesc.DepthClipEnable = true;
HR(device->CreateRasterizerState(&rasterizerDesc, RSNoCull.GetAddressOf()));
D3D11_RASTERIZER_DESC的定义
typedef struct D3D11_RASTERIZER_DESC
{
D3D11_FILL_MODE FillMode; // 填充模式
D3D11_CULL_MODE CullMode; // 裁剪模式
BOOL FrontCounterClockwise; //false表示顶点序列按顺时针排列的为正面,反之则反之
INT DepthBias; // https://docs.microsoft.com/zh-cn/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage-depth-bias
FLOAT DepthBiasClamp; // 网址同上
FLOAT SlopeScaledDepthBias; // 网址同上
BOOL DepthClipEnable; // 是否开启深度测试
BOOL ScissorEnable; // 是否开启矩形裁剪
BOOL MultisampleEnable; // 设置是否开启四重采样
BOOL AntialiasedLineEnable; // 设置是否开启反走样,仅multisampleEnable为false有效
} D3D11_RASTERIZER_DESC;
FillMode | 描述 |
---|---|
D3D11_FILL_WIREFRAME | 线框模式 |
D3D11_FILL_SOLID | 填充模式 |
CullMode | 描述 |
---|---|
D3D11_CULL_NONE | 总是渲染三角形 |
D3D11_CULL_FRONT | 不渲染正面对着摄像机的三角形 |
D3D11_CULL_BACK | 不渲染背面对着摄像机的三角形(默认) |
创建并绑定完对应的ID3D11RasterizerState,最后去draw前绑定
m_pd3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
绑定到渲染管线就可以了。
ID3D11BlendState
参考官方文档,创建混合状态需要用到D3D11_BLEND_DESC
// 调用 ******************
// 初始化混合状态
//
D3D11_BLEND_DESC blendDesc;
ZeroMemory(&blendDesc, sizeof(blendDesc));
auto& rtDesc = blendDesc.RenderTarget[0];
// Alpha-To-Coverage模式
blendDesc.AlphaToCoverageEnable = true;
blendDesc.IndependentBlendEnable = false;
rtDesc.BlendEnable = false;
rtDesc.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
HR(device->CreateBlendState(&blendDesc, BSAlphaToCoverage.GetAddressOf()));
// 透明混合模式
// Color = SrcAlpha * SrcColor + (1 - SrcAlpha) * DestColor
// Alpha = SrcAlpha
blendDesc.AlphaToCoverageEnable = false;
blendDesc.IndependentBlendEnable = false;
rtDesc.BlendEnable = true;
rtDesc.SrcBlend = D3D11_BLEND_SRC_ALPHA;
rtDesc.DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
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, BSTransparent.GetAddressOf()));
// 无颜色写入混合模式
// Color = DestColor
// Alpha = DestAlpha
rtDesc.SrcBlend = D3D11_BLEND_ZERO;
rtDesc.DestBlend = D3D11_BLEND_ONE;
rtDesc.BlendOp = D3D11_BLEND_OP_ADD;
rtDesc.SrcBlendAlpha = D3D11_BLEND_ZERO;
rtDesc.DestBlendAlpha = D3D11_BLEND_ONE;
rtDesc.BlendOpAlpha = D3D11_BLEND_OP_ADD;
HR(device->CreateBlendState(&blendDesc, BSNoColorWrite.GetAddressOf()));
D3D11_BLEND_DESC的定义
typedef struct D3D11_BLEND_DESC
{
BOOL AlphaToCoverageEnable; // 是否使用alpha-to-coverage来作为渲染像素的多重采样技术,默认关闭,它是一种多重采样技术,在渲染植物的叶子或铁丝网纹理时非常有用 默认值:False https://blog.csdn.net/butwang/article/details/6257076
//使用alpha-to-coverage需要开启多重采样(即,后台和深度缓冲创建时需要开启多重采样)
BOOL IndependentBlendEnable; // Direct3D 11支持同时绘制到8个渲染目标。当这个标志设置为true时,表示可以在不同的渲染目标上进行不同的混合处理(不同的混合因子、混合操作,混合开启/关闭等)。如果设置为false,则表示所有渲染目标都使用D3D11_BLEND_DESC::RenderTarget数组中第一个元素的混合状态,多重渲染目标用于高级的算法,目前为止,我们一次只使用一个渲染目标。 默认值:False
D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[ 8 ]; // 包含8个D3D11_RENDER_TARGET_BLEND_DESC元素的数组,第i个元素描述了第i个多重渲染目标的混合方式。如果IndependentBlendEnable设置为false,则所有的渲染目标都使用RenderTarget[0]进行混合
} D3D11_BLEND_DESC;
typedef struct D3D11_RENDER_TARGET_BLEND_DESC
{
BOOL BlendEnable; // 是否使用混合 默认值:False
D3D11_BLEND SrcBlend; // Fsrc 默认值:D3D11_BLEND_ONE
D3D11_BLEND DestBlend; // Fdst 默认值Default:D3D11_BLEND_ZERO
D3D11_BLEND_OP BlendOp; // 颜色混合运算符 默认值:D3D11_BLEND_OP_ADD
D3D11_BLEND SrcBlendAlpha; // Asrc 默认值:D3D11_BLEND_ONE
D3D11_BLEND DestBlendAlpha; // Adst 默认值:D3D11_BLEND_ZERO
D3D11_BLEND_OP BlendOpAlpha; // Alpha混合运算符 默认值:D3D11_BLEND_OP_ADD
UINT8 RenderTargetWriteMask; // D3D11_COLOR_WRITE_ENABLE枚举类型来指定可以写入的颜色 默认值:D3D11_COLOR_WRITE_ENABLE_ALL
} D3D11_RENDER_TARGET_BLEND_DESC;
翻译翻译这些枚举的含义,原混合方程为
C = Csrc * Fsrc op1 Cdst * Fdst
混合系数Fsrc和Fdst用D3D11_BLEND定义
D3D11_BLEND | 描述 |
---|---|
D3D11_BLEND_ZERO | F = 0 |
D3D11_BLEND_ONE | F = 1 |
D3D11_BLEND_SRC_COLOR | F = Csrc |
D3D11_BLEND_INV_SRC_COLOR | F = 1 - Csrc |
D3D11_BLEND_SRC_ALPHA | Asrc |
D3D11_BLEND_INV_SRC_ALPHA | F = 1 − Asrc |
D3D11_BLEND_DEST_ALPHA | F = Adst |
D3D11_BLEND_INV_DEST_ALPHA | F = 1 – Adst |
D3D11_BLEND_DEST_COLOR | F = Cdst |
D3D11_BLEND_INV_DEST_COLOR | F = 1 – Cdst |
D3D11_BLEND_SRC_ALPHA_SAT | F = sat(Asrc),sat()表示clamp(0,1) |
D3D11_BLEND_BLEND_FACTOR | F 的值来自于ID3D11DeviceContext::OMSetBlendState 方法的BlendFactor参数 |
D3D11_BLEND_INV_BLEND_FACTOR | F 的值来自于ID3D11DeviceContext::OMSetBlendState 方法的BlendFactor参数,并设为1 - BlendFactor |
D3D11_BLEND_SRC1_COLOR | |
D3D11_BLEND_INV_SRC1_COLOR | |
D3D11_BLEND_SRC1_ALPHA | |
D3D11_BLEND_INV_SRC1_ALPHA |
混合操作op用D3D11_BLEND_OP定义
D3D11_BLEND_OP | 描述 |
---|---|
D3D11_BLEND_OP_ADD | C = Csrc ⊗ Fsrc + Cdst ⊗ Fdst |
D3D11_BLEND_OP_SUBTRACT | C = Cdst ⊗ Fdst - Csrc ⊗ Fsrc |
D3D11_BLEND_OP_REV_SUBTRACT | C = Csrc ⊗ Fsrc - Cdst ⊗ Fdst |
D3D11_BLEND_OP_MIN | C = min(Csrc , Cdst) |
D3D11_BLEND_OP_MAX | C = max(Csrc , Cdst) |
okay,后头再看一眼当前渲染水体使用的透明混合方程。
rtDesc.SrcBlend = D3D11_BLEND_SRC_ALPHA;
rtDesc.DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
rtDesc.BlendOp = D3D11_BLEND_OP_ADD;
rtDesc.SrcBlendAlpha = D3D11_BLEND_ONE;
rtDesc.DestBlendAlpha = D3D11_BLEND_ZERO;
rtDesc.BlendOpAlpha = D3D11_BLEND_OP_ADD;
翻译出来就是
-
对于颜色C
Fsrc = Asrc
Fdst = 1 - Asrc
op = +
所以颜色C的混合公式就是
C = Csrc * Asrc + Cdst * (1 - Asrc); 标准的透明混合公式 -
对于透明度A
Fsrc = 1
Fdst = 0
op = +
所以透明度A的混合公式就是
A = Asrc * 1 + Adst * 0; 换句话说,就是只保留水体的透明度,丢弃水面下的物体的透明度。
最终的Alpha值是多少并不影响前面的运算,因此可以设为任意值,这里设为源像素Alpha值:
定义完之后调用即可
m_pd3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
```
void ID3D11DeviceContext::OMSetBlendState(
ID3D11BlendState *pBlendState, //混合状态对象的指针。
const FLOAT BlendFactor, //用于描述RGBA颜色向量的浮点数组。当混合因子指定为D3D11_BLEND_BLEND_FACTOR或D3D11_BLEND_INV_BLEND_FACTOR时,Direct3D将以该颜色向量作为混合系数。
UINT SampleMask); //多重采样最多可以支持32个采样源。这个32位整数用于启用和禁用采样源。例如,当第5个二进制位设为0时,表示屏蔽第5个采样源。当然,如果实际使用的多重采样源数量少于5个,那么屏蔽第5个采样源是没有什么实际意义的。当应用程序只使用一个采样源时,该参数只有第1个二进制位有效(参见练习1)。通常,该参数以0xffffffff作为默认值,表示不屏蔽任何采样源。
篱笆盒
读取之后,在像素着色器中用clip函数抛弃掉几乎透明的部分。
// 提前进行裁剪,对不符合要求的像素可以避免后续运算
float4 texColor = g_Tex.Sample(g_SamLinear, pIn.Tex);
clip(texColor.a - 0.1f);//为避免精度误差,接近0的都算0
此外,为了能透过方块的某个面看到面后面的铁丝网,需要设置光栅化状态的CULLMODE设为D3D11_CULL_NONE,取消背面剔除,让背面朝摄像机的面也能渲染。