观察下图,我们可以透过水看到地形和木板箱,这就是混合(blending, 融合)技术。下图我们先绘制地形与木板箱,再运用混合技术将水面绘制到后台缓冲区,令水的像素数据与地形以及板条箱这两种像素数据在后台缓冲区内相混合。
本章研究混合技术,它使我们将当前要光栅化的像素(又名为源像素, source pixel)与之前已光栅化至后台缓冲区的像素(目标像素, destination pixel)相融合。blending技术可以用于渲染如水与玻璃之类的半透明物体。
为了便于讨论,我们将此处谈及的后台缓冲区视为渲染目标,而在本书的后面还会展示将物体渲染至"离屏"(off screen)的渲染目标之中。在这两种渲染目标中所运用混合技术其实并没有什么区别,而位于离屏渲染目标中的目标像素也是经此前光栅化处理后的像素数据而已。
1.混合方程
设
为像素着色器输出的当前正在光栅化的第i行、第j列像素(源像素)的颜色值;
再设
为目前在后台缓冲区中与之对应的第i行、第j列像素(目标像素)的颜色值。若不适用混合技术,
将直接覆写
(当然,前提是像素通过了深度/模板测试)。如果使用混合技术,则
与
将融合在一起得到新颜色值C后再覆写
。
D3D使用下列混合方程来使源像素颜色与目标像素颜色相融合:
:
源混合因子
:
目标混合因子
-- 我们可以调整混合因子具体的值以获取各种不同效果
:表示针对颜色向量而定义的
分量式乘法
:则表示在10.2节中定义的
二元运算符
上述方程只用于控制颜色的RGB分量,而alpha分量实则由类似于下面的方程来单独处理:
上述两组方程本质上都是相同的,但希望能独立地处理两者,以获取不同的混合变换效果。但alpha分量的混合需求远少于RGB分量的混合需求。主要因为我们往往不关系后台缓冲区中的alpha值。而仅在一些对目标alpha值有特定要求的算法之中,后台缓冲区内的alpha值才显得至关重要。
※C方程中F是粗体,A方程中F是斜体
2.混合运算
下列枚举类型成员用作混合方程中的二元运算符
:
typedef enum D3D12_BLEND_OP
{
D3D12_BLEND_OP_ADD = 1,
D3D12_BLEND_OP_SUBTRACT = 2,
D3D12_BLEND_OP_REV_SUBTRACT = 3,
D3D12_BLEND_OP_MIN = 4,
D3D12_BLEND_OP_MAX = 5
} D3D12_BLEND_OP;
注意:min和max忽略了混合因子
这些运算符也同样适用于alpha混合运算,而且能为RGB和alpha这两种运算分别指定不同的运算符。D3D从最近几版开始加入了一项新特性,通过逻辑运算符对源颜色和目标颜色进行混合,用以取代上述传统的混合方程。这些逻辑运算符:
typedef enum D3D12_LOGIC_OP
{
// 等式右侧的值为上一个枚举变量+1
D3D12_LOGIC_OP_CLEAR = 0,
D3D12_LOGIC_OP_SET = (D3D12_LOGIC_OP_CLEAR + 1),
D3D12_LOGIC_OP_COPY ...
D3D12_LOGIC_OP_COPY_INVERTED ...
D3D12_LOGIC_OP_NOOP ...
D3D12_LOGIC_OP_INVERT ...
D3D12_LOGIC_OP_AND ...
D3D12_LOGIC_OP_NAND ...
D3D12_LOGIC_OP_OR ...
D3D12_LOGIC_OP_NOR ...
D3D12_LOGIC_OP_XOR ...
D3D12_LOGIC_OP_EQUIV ...
D3D12_LOGIC_OP_AND_REVERSE ...
D3D12_LOGIC_OP_AND_INVERTED ...
D3D12_LOGIC_OP_OR_REVERSE ...
D3D12_LOGIC_OP_OR_INVERTED ...
} D3D12_LOGIC_OP;
注意:传统混合方程和逻辑运算符这两种手段,只能选择一种。另外,使用逻辑运算符混合技术,一定要选择它所支持的渲染目标格式 -- 这个格式应当为UINT的有关类型,否则会报错(见下图)。
3.混合因子
下面是基本的混合因子,将其应用与Fsrc与Fdst,关于其他更加高级的混合因子,读者请参考SDK文档中的D3D12_BLEND枚举类型。
上述混合因子都可用于RGB混合方程,但对于alpha混合方程来说,不能使用以_COLOR结尾的混合因子。
我们通过OMSetBlendFactor来设置混合因子:
void ID3D12GraphicsCommandList::OMSetBlendFactor(
const FLOAT BlendFactor[4]
);
// 若传入nullptr,则恢复值为(1,1,1,1)的默认混合因子
4.混合状态
前面讨论过混合运算符和混合因子,那么D3D如何来设置这些数值呢?就像其他D3D状态一样,混合状态亦是PSO的一部分。之前我们一直使用的默认的混合技术,就是没有启用混合技术。 -- 所以之前才区分了透明物体与非透明物体(opaque)的PSO。
D3D12_GRAPHICS_PIPELINE_STATE_DESC opaquePsoDesc;
ZeroMemory(&opaquePsoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
...
opaquePsoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
为了配置非默认混合状态,我们必须填写D3D12_BLEND_DESC结构体:
typedef struct D3D12_BLEND_DESC {
BOOL AlphaToCoverageEnable; // 默认值:False
BOOL IndependentBlendEnable; // 默认值:False
D3D12_RENDER_TARGET_BLEND_DESC RenderTarget[8];
} D3D12_BLEND_DESC;
// AlphaToCoverageEnable:
是否开启alpha-to-coverage功能,这是一种在渲染叶片或门等纹理时极其有用的一种多重采样技术
-- 另外,要使用此技术还需开启多重采样(即创建后台缓冲区与深度缓冲区时要启用多重采样)
// IndependentBlendEnable:
D3D最大可同时支持8个渲染目标。此标志设置为true则表明可以向每一个渲染目标执行不同的混合操作(不同的混合因子、混合运算以及设置不同的混合状态),否则,意味着所有渲染目标均使用D3D12_BLEND_DESC::RenderTarget数组中第一个元素所描述的方式进行混合
// RenderTarget:
8个元素数组,其中第i个元素表示如何针对第i个渲染目标进行混合处理,如果IndependentBlendEnable设置为false,则所有渲染目标都由第0个元素的设置进行混合运算
结构体D3D12_RENDER_TARGET_BLEND_DESC:
typedef struct D3D12_RENDER_TARGET_BLEND_DESC
{
BOOL BlendEnable; // 默认值:False
BOOL LogicOpEnable; // 默认值:False
D3D12_BLEND SrcBlend; // 默认值:D3D12_BLEND_ONE
D3D12_BLEND DescBlend; // 默认值:D3D12_BLEND_ZERO
D3D12_BLEND_OP BlendOp; // 默认值:D3D12_BLEND_OP_ADD
D3D12_BLEND SrcBlendAlpha; // 默认值:D3D12_BLEND_ONE
D3D12_BLEND DestBlendAlpha; // 默认值:D3D12_BLEND_ZERO
D3D12_BLEND_OP BlendOpAlpha; // 默认值:D3D12_BLEND_OP_ADD
D3D12_LOGIC_OP LogicOp; // 默认值:D3D12_LOGIC_OP_NOOP
UINT8 RenderTargetWriteMask; // 默认值:D3D12_COLOR_WRITE_ENABLE_ALL
} D3D12_RENDER_TARGET_BLEND_DESC;
// BlendEnable/LogicOpEnable:
true:启用常规混合功能/逻辑混合运算 -- 不能将 BlendEnable和LogicOpEnable 同时置为true
也就是说:常规运算和逻辑运算只能2选1
// SrcBlend/DestBlend/SrcBlendAlpha/DestBlendAlpha:
枚举类型D3D12_BLEND中的成员之一,用于指定RGB混合中的混合因子Fsrc/Fdst,和指定alpha混合中的混合因子Fsrc/Fdst
// BlendOp/BlendOpAlpha:
枚举类型D3D12_BLEND_OP中的成员之一,用于指定RGB/alpha混合运算符
// LogicOp:
枚举类型D3D12_LOGIC_OP中的成员之一,指定源颜色与目标颜色在混合时所用的逻辑运算符
// RenderTargetWriteMask:
typedef enum D3D12_COLOR_WRITE_ENABLE {
D3D12_COLOR_WRITE_ENABLE_RED = 1,
D3D12_COLOR_WRITE_ENABLE_GREEN = 2,
D3D12_COLOR_WRITE_ENABLE_BLUE = 4,
D3D12_COLOR_WRITE_ENABLE_ALPHA = 8,
D3D12_COLOR_WRITE_ALL =
(D3D12_COLOR_WRITE_ENABLE_RED | D3D12_COLOR_WRITE_ENABLE_GREEN |
D3D12_COLOR_WRITE_ENABLE_BLUE | D3D12_COLOR_WRITE_ENABLE_ALPHA )
} D3D12_COLOR_WRITE_ENABLE;
这些颜色控制着混合后的数据可被写入后台缓冲区中的哪些颜色通道,比如..alpha就只写入alpha通道,不写入RGB通道
当混合功能被禁止时,从PS返回的颜色数据将按没有设置上述写掩码来进行处理
混合运算是由开销的,它对每个像素进行额外的处理,只有在需要的情况下才使用此技术。
代码示例:创建和设置混合状态:
// 透明物体的PSO
D3D12_GRAPHICS_PIPELINE_STATE_DESC transparentPsoDesc = opaquePsoDesc;
D3D12_RENDER_TARGET_BLEND_DESC transparencyBlendDesc;
transparencyBlendDesc.BlendEnable = true;
transparencyBlendDesc.LogicOpEnable = false;
transparencyBlendDesc.SrcBlend = D3D12_BLEND_SRC_ALPHA;
transparencyBlendDesc.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
transparencyBlendDesc.BlendOp = D3D12_BLEND_OP_ADD;
transparencyBlendDesc.SrcBlendAlpha = D3D12_BLEND_ONE;
transparencyBlendDesc.DestBlendAlpha = D3D12_BLEND_ZERO;
transparencyBlendDesc.BlendOpAlpha = D3D12_BLEND_OP_ADD;
transparencyBlendDesc.LogicOp = D3D12_LOGIC_OP_NOOP;
transparencyBlendDesc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
transparentPsoDesc.BlendState.RenderTarget[0] = transparencyBlendDesc;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&transparentPsoDesc, IID_PPV_ARGS(&mPSOs["transparent"])));
如同其他PSO一般,我们应当在应用程序的初始化期间来创建它们,接着再根据需求以ID3D12GraphicsCommandList::SetPipelineState方法在不同的状态之间进行转换。
5.混合示例
我们将考查一些用于获取特效的混合因子组合,这些示例中,我们只关注RGB混合,而alpha混合的处理方法与之相似。
①禁止颜色的写操作:
希望使原始的目标像素保持不变,既不对它进行覆写,也不与当前光栅化的源像素执行混合。
比如说,不涉及后台缓冲区,只对深度/模板缓冲区进行写操作时,就把源像素的混合因子设置为D3D12_BLEND_ZERO,再将目标混合因子配置为D3D12_BLEND_ONE,再令混合运算符为D3D12_BLEND_OP_ADD即可。
当然也可以直接将D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask设置为0,即禁止任何向颜色通道执行的写操作。
②加法混合和减法混合:
想实现加法运算:直接将源和目标混合因子都设置为D3D12_BLEND_ONE,再将混合运算符置于D3D12_BLEND_OP_ADD。
相减同理:依旧两个ONE混合因子,然后将ADD改成SUBTRACT
③乘法混合:
如果希望源像素与对应目标像素相乘,那么应设源混合因子为D3D12_BLEND_ZERO、目标混合因子为D3D12_BLEND_SRC_COLOR,再将混合运算符置为D3D12_BLEND_OP_ADD。
④透明混合:
不透明度(opacity)与透明度(transparency)的关系很简单:T=1-A,其中A为不透明度,T为透明度。现在,假设我们希望基于源像素的不透明度,将源像素与目标像素进行混合。假设源alpha分量
为一种可用来控制源像素不透明度的百分比(alpha值越小越透明)。为了实现这个效果,我们设源混合因子为D3D12_BLEND_SRC_ALPHA、目标混合因子为D3D12_BLEND_INV_SRC_ALPHA,并将混合运算符置为D3D12_BLEND_OP_ADD。
例如,假设
,也就是说,源像素的不透明度为25%。由于源像素的透明度为75%,因此,也就是说在源像素与目标像素混合在一起的时候,我们便希望最终颜色由25%的源像素与75%的目标像素组合而成(目标像素会位于源像素的"后侧")。
⭐但是,使用混合方法时,我们应当考虑物体的绘制顺序,我们应当遵循以下规则:
首先要绘制无需混合的物体,接下来,再根据混合物体与摄像机的距离对它们进行排序。最后,由远及近的顺序通过混合的方式来绘制这些物体。
由后向前绘制的原因:每个物体都要与其后的所有物体执行混合运算。对于一个透明的物体,我们应当可以透过它看到其后面的场景。因此先将透明物体后面的所有对应像素都预先写入后台缓冲区,随后再将透明物体的源像素与其后场景的目标像素进行混合。
理论上需要对混合物体进行排序,但我们观察混合运算,发现有几种混合运算是满足交换律的(
),所以说对于这几种混合运算,无需考虑计算的顺序(也就是物体绘制的顺序)。具体来看后台缓冲区中某个像素点的颜色的计算公式:对该像素连续进行n次下面这种单一的混合运算,便无序考虑计算顺序
5.混合与深度缓冲区
举例:假设我们希望使用加法混合来渲染一个物体集合S,我们的目标是:只需将这些物体的颜色数据简单地累加即可。为此,我们不希望进行深度测试,这是因为,如果我们开启深度测试,但绘制顺序没有从后往前的顺序进行绘制,那么部分数据就会丢失,不能完成简单混合的效果。
我们可以通过禁止向深度缓冲区的写操作来禁止S中物体之间的深度测试。但是注意,我们只禁止了透明物体深度值的写入,但深度值的读取与深度值的检测仍然是开启的,这保证了非透明物体后面的所有物体(包括透明物体)不会遮挡前面的几何体。
6.alpha通道
alpha分量控制着像素的透明度(alpha值越小越透明)。而混合方程中所用的源颜色实则来自于像素着色器。在第9章中,我们将漫反射材质的alpha值作为纹理着色器的alpha输出。这样一来,我们就能利用漫反射图(diffuse map)中的alpha通道来控制混合过程中的透明度。
float4 PS(VertexOut pin) : SV_Target
{
float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo;
...
litColor.a = diffuseAlbedo.a;
return litColor;
}
我们往往可以通过常见的图像编辑软件中添加alpha通道,接着将图像保存为支持alpha通道的格式,如DDS。
7.裁剪像素
有时候,我们希望彻底禁止某个源像素参与后续的处理,可以通过HLSL的内置函数clip(x)来实现[此函数仅供像素着色器调用]。若x<0,则从后面的处理阶段中丢弃当前这一像素。
我们可以通过此函数处理铁丝网纹理的绘制,换句话说,用它来绘制透明与非透明相间的像素再好不过了,见下图:
在像素着色器PS中,我们将采集像素的alpha分量。如果该值极小接近于0,则表示此像素是完全透明的,那么我们将此像素从后续处理中淘汰掉。
float4 PS(VertexOut pin) : SV_Target
{
float4 diffuseAlbedo = gDiffuseMap.Sample(
gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo;
#ifdef ALPHA_TEST
// 若alpha<0.1则抛弃该像素,我们应该在PS中尽早执行此测试,让不满足像素快速推出PS并跳过后续相关处理
clip(diffuseAlbedo.a - 0.1f);
#endif
...
litColor.a = diffuseAlbedo.a;
return litColor;
}
alpha测试的开销不小,所以只有在必要的情况下才使用它。我们只有在定义了ALPHA_TEST宏的时候才会实行透明像素的筛选。
注意:通过混合操作也能实现相同的效果,但是使用clip函数更为有效。首先,它无序执行混合运算(即可以禁止混合操作)。其次,处理期间不用考虑绘制顺序。此外,从PS中抛弃像素,像素不用执行后续指令。
纹理过滤操作可能会使alpha通道的数据略受影响,因此在裁剪像素时应对判断值留出适当的余地(允许特定的误差)。例如,可以根据接近0的alpha值来裁剪像素,但不要按精确的0值进行处理。 -- 纹理过滤操作就是指点过滤、线性过滤、各向异性过滤等。
铁丝网演示程序的PSO代码:①使用clip来渲染铁丝网盒②铁丝网盒必须禁止背面剔除!
D3D12_GRAPHICS_PIPELINE_STATE_DESC alphaTestedPsoDesc = opaquePsoDesc;
alphaTestedPsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["alphaTestedPS"]->GetBufferPointer()),
mShaders["alphaTestedPS"]->GetBufferSize()
};
alphaTestedPsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&alphaTestedPsoDesc, IID_PPV_ARGS(&mPSOs["alphaTested"])));
8.雾
游戏中出现雾的好处:①模拟天气状况②放置远处景物在渲染上的失真:防止因为摄像机的移动,远处物体突然出现在视锥体的范围之内,使它像是突然“瞬移”到了场景之中一样 -- 即使场景处于晴朗的白天,我们不妨在远处设有少量的薄雾,或是在高山这种远处景物处设置云雾缭绕的感觉。
雾气随着深度函数值的增加而变浓。
实现雾化效果的流程:
指明雾的颜色、由摄像机到雾气的最近距离以及雾的分散范围(最近距离~完全覆盖物体)
将网格三角形上的点的颜色置为原色与雾色的加权平均值:
参数s范围[0,1],由摄像机到被雾气覆盖物体表面点之间的距离作为参数的函数来确定,参数s的定义如下:
dist:表面点p与摄像机位置E之间的距离
shader代码:
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif
#include "LightingUtil.hlsl"
Texture2D gDiffuseMap : register(t0);
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform;
};
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
float4 gAmbientLight;
// --------------------------------------------------------------------------
// 允许应用程序在每一帧都能改变雾效参数
float4 gFogColor;
float gFogStart;
float gFogRange;
// --------------------------------------------------------------------------
float2 cbPerObjectPad2;
Light gLights[MaxLights];
};
cbuffer cbMaterial : register(b2)
{
float4 gDiffuseAlbedo;
float3 gFresnelR0;
float gRoughness;
float4x4 gMatTransform;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 TexC : TEXCOORD;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout = (VertexOut)0.0f;
// Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosW = posW.xyz;
// Assumes nonuniform scaling; otherwise, need to use inverse-transpose of world matrix.
vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
// Transform to homogeneous clip space.
vout.PosH = mul(posW, gViewProj);
// Output vertex attributes for interpolation across triangle.
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, gMatTransform).xy;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo;
#ifdef ALPHA_TEST
clip(diffuseAlbedo.a - 0.1f);
#endif
pin.NormalW = normalize(pin.NormalW);
// --------------------------------------------------------------------------\
// 之前直接normalize(toEyeW),这里因为需要求distance所以不如直接除以distance
// 这其实是一种优化:因为之前normalize也会求出distance,会重复多求一次distance
float3 toEyeW = gEyePosW - pin.PosW;
float distToEye = length(toEyeW);
toEyeW /= distToEye;
// --------------------------------------------------------------------------
float4 ambient = gAmbientLight*diffuseAlbedo;
const float shininess = 1.0f - gRoughness;
Material mat = { diffuseAlbedo, gFresnelR0, shininess };
float3 shadowFactor = 1.0f;
float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
pin.NormalW, toEyeW, shadowFactor);
float4 litColor = ambient + directLight;
// --------------------------------------------------------------------------
#ifdef FOG
float fogAmount = saturate((distToEye - gFogStart) / gFogRange);
litColor = lerp(litColor, gFogColor, fogAmount); // lerp:HLSL插值函数
#endif
// --------------------------------------------------------------------------
litColor.a = diffuseAlbedo.a;
return litColor;
}
有一些场景可能不需要雾效,我们将该功能设为可选项。若要使用,则需在编译着色器时定义FOG宏。在演示程序中,我们通过向CompileShader函数提供下列D3D_SHADER_MACRO结果体来开启雾效:
const D3D_SHADER_MACRO defines[] =
{
"FOG", "1",
NULL, NULL
};
const D3D_SHADER_MACRO alphaTestDefines[] =
{
"FOG", "1",
"ALPHA_TEST", "1",
NULL, NULL
};
mShaders["standardVS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", nullptr, "VS", "vs_5_0");
mShaders["opaquePS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", defines, "PS", "ps_5_0");
mShaders["alphaTestedPS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", alphaTestDefines, "PS", "ps_5_0");
// 宏定义的语法: {Name, Defination}