什么是混合?
混合可以使得我们将要光栅化的像素(源像素)和先前已光栅化至后台缓冲区的像素进行融合
为什么需要混合?
混合主要用于渲染半透明物体,如水
混合的基本操作
混合的基本操作是over操作和under操作
over操作是一种混合操作,用于应对半透明物体在不透明物体前或半透明物体和半透明物体间的情况,它的顺序是从后向前——从源像素到目标像素一层一层混合。
公式为:\(c_0 = \alpha_s c_s + (1 - \alpha_s)c_d\),其中\(\alpha_s\)为源物体的alpha值,\(c_s\)为源物体的颜色值,\(c_d\)为目标物体的颜色值,\(c_0\)为目标输出值
under操作和over操作类似,但顺序相反,它是从前往后。公式为:\(c_0 = \alpha_d c_d + (1 - \alpha_d)\alpha_s cs \\ a_0 = \alpha_s(1-\alpha_d) + \alpha_d\)
区别:
- over操作无需计算blend后的透明度,因为over操作需要的是源物体的透明度;但under操作则需要源物体和目标物体的透明度
- over不能对两个半透明物体叠加,因此它只能从前向后叠加,而under可以对两个半透明物体叠加
深度写入和深度测试
深度测试
:通过模板测试的片元会进入深度测试,GPU会把该片元的深度和已经存在于深度缓冲区中的深度值进行比较,没通过测试的片元会被舍弃。通过深度测试的片元才有资格进入帧缓冲区,随后渲染在屏幕上
深度写入
:过深度测试的值来取代深度的最小值
D3D中的混合方程
颜色混合方程:\(C = C_{src} \bigotimes F_{src} \boxplus C_{dst} \bigotimes F_{dst}\),其中\(\bigotimes\)意为分量式乘法,\(\boxplus\)是D3D12_BLEND_OP结构体中定义的二元运算符,F为混合因子
alpha混合方程和颜色混合方程类似,只是将alpha分量替换颜色方程中的C。公式:\(A = A_{src} F_{src} \boxplus A_{dst} F_{dst}\)
混合运算
D3D定义了混合方程的二元运算符\(\boxplus\)结构体:
typedef enum D3D12_BLEND_OP {
D3D12_BLEND_OP_ADD = 1, //src + dst
D3D12_BLEND_OP_SUBTRACT = 2, //dst - src
D3D12_BLEND_OP_REV_SUBTRACT = 3, //src - dst
D3D12_BLEND_OP_MIN = 4, //求两者最小值
D3D12_BLEND_OP_MAX = 5 //求两者最大值
} ;
其中,求两者最小值/最大值时,会忽略混合因子
后来D3D推出以逻辑运算符对源颜色和目标颜色进行混合
的方式来取代混合方程:
typedef enum D3D12_LOGIC_OP {
D3D12_LOGIC_OP_CLEAR = 0,
D3D12_LOGIC_OP_SET = {D3D12_LOGIC_OP_CLEAR + 1}, //每个枚举比前一个枚举大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
} ;
但是这两者不可一起使用,只可任选其一。且若使用逻辑运算符混合。必须选择它所支持的渲染目标格式——UINT的有关类型
混合因子
混合因子就是混合方程中的F,通过为源混合因子和目标混合因子设置混合运算符,即可实现各式各样的混合效果。以下列举的是D3D中提供的混合因子,设\(C_{src} = (r_s, g_s. b_s), A_{src} = a_s,C_{dst} = (r_d, g_d, b_d),A_{dst} = a_d,F为颜色方程的混合因子F_{src}/F_{dst},f为alpha方程的混合因子f_{src}/f_{dst}\):
typedef enum D3D12_BLEND {
D3D12_BLEND_ZERO = 1, //F = (0,0,0), f = 0
D3D12_BLEND_ONE = 2, //F = (1,1,1), f = 1
D3D12_BLEND_SRC_COLOR = 3, //Fsrc = (rs, gs, bs)
D3D12_BLEND_INV_SRC_COLOR = 4, //Fsrc = (1 - rs, 1- gs, 1 - bs)
D3D12_BLEND_SRC_ALPHA = 5, //Fsrc = (as, as, as, as),f = as
D3D12_BLEND_INV_SRC_ALPHA = 6,
D3D12_BLEND_DEST_ALPHA = 7,
D3D12_BLEND_INV_DEST_ALPHA = 8,
D3D12_BLEND_DEST_COLOR = 9,
D3D12_BLEND_INV_DEST_COLOR = 10,
D3D12_BLEND_SRC_ALPHA_SAT = 11,
D3D12_BLEND_BLEND_FACTOR = 14, //F = (r, g, b), f = a
D3D12_BLEND_INV_BLEND_FACTOR = 15,
D3D12_BLEND_SRC1_COLOR = 16,
D3D12_BLEND_INV_SRC1_COLOR = 17,
D3D12_BLEND_SRC1_ALPHA = 18,
D3D12_BLEND_INV_SRC1_ALPHA = 19,
D3D12_BLEND_ALPHA_FACTOR = 20,
D3D12_BLEND_INV_ALPHA_FACTOR = 21
} ;
D3D12_BLEND_BLEND_FACTOR中的颜色(r,g,b,a)可用作方法ID3D12GraphicsCommandList::OMSetBlendFactor()的参数,该方法可直接指定当前管线使用的混合因子值,但在改变混合状态前,该值不会生效。且若该方法的参数传入的是nullptr,则为默认混合因子(1,1,1,1)
void OMSetBlendFactor(
[in, optional] const FLOAT [4] BlendFactor
);
D3D12_BLEND枚举中所有混合因子都可用于颜色混合方程,但alpha混合方程不可使用以_COLOR结尾的混合因子
AlphaToCoverage
-
什么是AlphaToCoverage?
我们知道开启MSAA后,在光栅化时会基于采样点的位置来计算图元的覆盖情况,只有被覆盖的采样点的颜色,才会被更新为当前图元的颜色,采样完成后,最终每个像素的颜色是它对应的采样点的颜色平均值(一个像素对应多个采样点)
而AlphaToCoverage会在MSAA的基础上,额外考虑alpha值,基于alpha修改图元颜色在一个像素点中所占权重:将输出寄存器SV_Target0的alpha分量从像素着色器转换到一个n步的coverage mask(给定n个样本的渲染目标),和原始流程下MSAA得到的结果做AND运算,基于alpha值来确定哪个采样点会更新。若alpha小于1,则最终会被更新颜色的采样点个数可能会变少(因为coverage mask的计算和具体硬件相关)
对于以上图片中的红圈框起来的像素点,绘制的一个三角形覆盖了该像素点里的两个采样器,若不开AlphaToCoverage则两个采样点的颜色值都会更新,若开启AlphaToCoverage则大概率只更新一个(概率与coverage mask有关)
-
为什么需要AlphaToCoverage?
对于叶子这类密集的物体仅仅使用alpha test,会造成很严重的失真,尤其是边缘
使用AlphaToCoverage可以使其更为柔和
-
存在的问题
- 从上图可以看出,这种方法并不完美,叶子有些毛茸茸的且有些模糊
混合状态
目前为止,我们已经知晓混合方程里各值得含义,接下来就是用D3D来设置这些数值:混合状态属于PSO,因此我们通过PSO设置混合状态
在之前的实例中,我们使用的是默认的混合状态——没有启动混合
D3D12_GRAPHICS_PIPELINE_STATE_DESC opaquePsoDesc;
//...
opaquePsoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
为了配置混合状态,需要填写混合状态描述符:
typedef struct D3D12_BLEND_DESC {
BOOL AlphaToCoverageEnable; //指定在将像素设为渲染目标时是否使用AlphaToCoverage
//指定是否可以向每一个渲染目标执行不同的混合操作,若为false则所有渲染目标全部使用第三个参数RenderTarget[0]所描述的方式进行混合.最多同时支持8个渲染目标
BOOL IndependentBlendEnable;
D3D12_RENDER_TARGET_BLEND_DESC RenderTarget[8]; //第i个元素描述如何针对第i个渲染目标进行混合处理
} D3D12_BLEND_DESC;
其中,D3D12_RENDER_TARGET_BLEND_DESC结构体定义如下:
typedef struct D3D12_RENDER_TARGET_BLEND_DESC {
BOOL BlendEnable; //true 启用常规混合;false 禁用常规混合
BOOL LogicOpEnable; //true 启用逻辑混合;false 禁用逻辑混合
D3D12_BLEND SrcBlend; //混合因子,指定RGB混合中的源混合因子
D3D12_BLEND DestBlend; //混合因子,指定RGB混合中的目标混合因子
D3D12_BLEND_OP BlendOp; //二元运算符,指定RGB混合的运算符
D3D12_BLEND SrcBlendAlpha; //混合因子,指定alpha混合中的源混合因子
D3D12_BLEND DestBlendAlpha; //混合因子,指定alpha混合中的目标混合因子
D3D12_BLEND_OP BlendOpAlpha; //二元运算符,指定alpha混合中的运算符
D3D12_LOGIC_OP LogicOp; //二元逻辑运算符,指定RGB混合中使用的逻辑运算符
UINT8 RenderTargetWriteMask; //组合标志,控制混合后的数据可被写入后台缓冲区中的哪些颜色通道
} D3D12_RENDER_TARGET_BLEND_DESC;
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_ENABLE_ALL
} ;
随后填写并创建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"])));
混合因子的舞台
我们已经清楚如何创建和设置blend状态,现在来看看混合因子能实现哪些效果。由于RGB和alpha混合处理方法类似,因此以下只涉及RGB的混合组合
不对目标像素进行blend
若不希望对后台缓冲区的数据进行改变,也就是后台缓冲区的像素依旧为目标像素。有两种实现方法
- 只需将源blend因子\(F_{src}\)设为D3D12_BLEND_ZERO,目标blend因子\(F_{dst}\)设为D3D12_BLEND_ONE,混合运算符为D3D12_BLEND_OP_ADD,即:\(C = C_{src} \bigotimes(0,0,0) + C_{dst} \bigotimes(1,1,1) = C_{dst}\)
- 将D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask设为0,也就是禁止写入任何颜色
加减乘法混合
- 加减法blend
通过源像素和目标像素进行加法/减法blend,可以让生成的图像变亮/变暗
若希望源像素和目标像素实现加减法blend,需设\(F_{src}\) 和 \(F_{dst}\)为D3D12_BLEND_ONE,若混合运算符为D3D12_BLEND_OP_ADD,会提升颜色值,从而使得生成的图像更为明亮;若混合运算符为D3D12_BLEND_OP_SUBTRACT,会使得生成图像更为暗淡e。方程如下:加法为\(C = C_{src} \bigotimes (1,1,1) + C_{dst} \bigotimes (1,1,1) = C_{src} + C_{dst}\),减法为\(C = C_{dst} \bigotimes(1,1,1) - C_{src} \bigotimes(1,1,1) = C_{dst} - C_{src}\)
-
乘法混合
若需要将源像素和目标像素相乘,需要设源混合因子为D3D12_BLEND_ZERO,目标混合因子为D3D12_BLEND_SRC_COLOR,混合运算符为D3D12_BLEND_OP_ADD,blend方程如下:
\(C = C_{src} \bigotimes(0,0,0) + C_{dst} \bigotimes C_{src} = C_{dst} \bigotimes C_{src}\)
透明blend
通过以源像素的alpha(不透明度),将源像素与目标像素进行混合,可以实现透明blend的效果。设源混合因子为D3D12_BLEND_SRC_ALPHA,
目标混合因子为D3D12_BLEND_INV_SRC_ALPHA,混合运算符为D3D12_BLEND_OP_ADD,混合方程如下:\(C = C_{src} \bigotimes (a_s, a_s, a_s) + C_{dst} \bigotimes (1 - a_s, 1 - a_s, 1 - a_s) = a_sC_{src} + (1 - a_s)C_{dst}\)
此方法较为特殊,需要考虑物体的绘制顺序,规则如下:
- 先绘制无需混合处理的物体
- 关闭深度写入但保留深度测试,再根据需要混合的物体的z值将它们进行sort,最后按画家算法的思路进行blend然后绘制
为什么需要考虑物体的绘制顺序且第二步关闭深度写入?
因为,进行混合后的物体属于半透明或全透明的物体,若假设一个不透明物体的z值大于透明物体,且它们在像素点上有部分重叠,若此时进行深度测试进行替换,会导致不透明物体的颜色值直接被丢弃,从而造成错误的结果
为什么先渲染不透明物体,再渲染半透明物体?
假设先渲染半透明物体,由于此时深度写入是关闭的,无法写入z值,当渲染完半透明物体后开始渲染不透明物体时,此时最小z值是空值,该不透明物体会直接通过测试,将不透明物体的颜色覆盖掉原本渲染的半透明物体的颜色,从而造成错误结果。但是,若是先渲染不透明物体,则可以先进行深度写入,确定它们之间的顺序,当渲染完所有不透明物体后便关闭深度写入,然后以画家算法的思路进行渲染,且由于半透明物体的混合颜色需要依赖z值大于它的不透明物体的颜色值
裁剪像素
-
为什么需要裁剪像素?
禁止某个源像素参与后续处理,实现绘制透明和非透明相间的像素
下图中,左边铁丝网纹理含有alpha通道,而右边黑色且含有alpha通道的像素将被clip函数丢弃,如此只会绘制出铁丝网
-
D3D中如何实现裁剪?
HLSL的内置函数clip(x)来实现,此函数仅用于片元着色器,若x < 0则丢弃这一像素
通过blend操作也能实现像素裁剪,但clip函数更为有效。原因如下
- 无需进行blend运算
- 通过提前从片元着色器中丢弃像素,可以使其跳过片元着色器的剩余指令
大部分时候进行像素裁剪,记得禁用背面剔除,否则会穿帮
-
示例
float4 PS(VertexOut pin) : SV_Target { float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo; #ifdef ALPHA_TEST // 若alpha < 0.1则抛弃该像素 //此项需尽早执行,可优化性能 clip(diffuseAlbedo.a - 0.1f); #endif }
示例——实现雾
-
雾有什么好处?
也许你可能认为雾仅仅用于模拟现实世界中的天气,但它的用处不止于此。如,浓雾可以掩盖远景的失真,以及发生物体突然出现(popping)的情况
-
什么是突然出现?
原本一个物体还处于摄像机视锥体的远平面之外,这时移动摄像机,使其突然出现在视锥体范围内,现在该物体是可见的,但如此会使得该物体看起来像素突然出现在渲染场景中
-
-
实现雾的流程
-
指明雾的颜色,有相机到雾气的最近距离及雾到相机的最近距离至能完全覆盖物体的这段范围
-
将网格三角形的顶点的颜色赋值为原色与雾色的加权平均值(线性插值):\(foggedColor = litColor + s(fogColor - litColor)\),参数s的范围为[0,1],该参数意为相机位置和被雾覆盖物体表面点的距离,s公式如下:$ s = saturate(\frac{dist(p,E) - fogStart}{fogRange})\(,其中,\)dist(p,E)\(表示物体表面点\)p\(和相机位置\)E\(的距离的距离函数,\)saturate()\(会将参数限制于区间[0,1]内——\)saturate(x) = \left {\begin {array} {rcl} x & 0 \leq x \leq 1 \ 0 & x < 0 \ 1 & x > 1 \end {array} \right.$
根据距离函数dist()绘制而绘制的s图像如下
-
hlsl实现雾
-
流程如下
- 计算距离,并在像素层级间进行插值
- 求光照颜色
-
示例
cbuffer cbPass : register(b1) { //... // Allow application to change fog parameters once per frame. // For example, we may only use fog for certain times of day. float4 gFogColor; float gFogStart; float gFogRange; } float4 PS(VertexOut pin) : SV_Target { //... float4 litColor = ambient + directLight; #ifdef FOG float fogAmount = saturate((distToEye - gFogStart) / gFogRange); litColor = lerp(litColor, gFogColor, fogAmount); #endif //... }
将雾效设为可选项。使用D3D_SHADER_MACRO struct,第一个参数指定宏的名字,宏的含义
const D3D_SHADER_MACRO defines[] = { "FOG", "1", NULL, NULL };
reference
https://zhuanlan.zhihu.com/p/388513281
Directx12 3D 游戏开发实战
标签:像素,DX,alpha,D3D12,混合,BLEND,OP From: https://www.cnblogs.com/chenglixue/p/17154979.html