Skip to content

Latest commit

 

History

History
332 lines (215 loc) · 21.3 KB

Chapter 10 Blending.md

File metadata and controls

332 lines (215 loc) · 21.3 KB

Chapter 10 Blending

观察下方图片10.1。 我们在这一帧中最开始渲染的是地形和木箱,因此地形和木箱的像素就被储存在后台缓冲中。 然后我们使用混合技术来绘制一个水面到后台缓冲,因此水的像素和地形以及木箱的像素在后台缓冲中得到了混合,看起来就是木箱和地形穿过了水面。 在本章中,我们尝试混合技术,这个技术能够就是混合(组合)现在正在光栅化的像素(我们称之为源)和之前已经完成光栅化并存储在后台缓冲中的像素(我们称之为目标)。 这个技术能够让我们去渲染透明的物体,例如水和玻璃。

目标

  • 理解如何使用混合,并且能够在Direct3D中使用它。
  • 学习Direct3D支持的不同的混合模式。
  • 发现如何通过使用Alpha去控制图元的透明度。
  • 学习如何使用HLSL中的Clip函数阻止一个像素绘制到后台缓冲中。

10.1 THE BLENDING EQUATION

我们设Csrc表示从我们的像素着色器(PixelShader)输出的第i,j个像素,也就是我们正在光栅化的像素,然后我们设Cdst表示现在在后台缓冲中第i,j个像素。 如果不使用混合技术的话,像素Csrc将会替代Cdst(前提是他能够通过深度和模板测试)。 但是在使用混合的情况下,CsrcCdst将会混合成一个新的颜色C,并且C将会替代Cdst作为后台缓冲里面的像素(你可以认为新的颜色C将会被写入到后台缓冲中去)。 Direct3D使用下面的这个方程来混合源和目标像素的颜色:

C = Csrc × Fsrc ^ Cdst × Fdst

F将会在下面介绍,主要是通过F来让我们有更多的方式去实现更多的效果。 × 符合表示的是向量的点积,^(他那个符合我打不出,我们暂且叫做混合操作)符号是一种由我们自主定义的运算,具体下面会介绍。

上面的方程是用于处理颜色分量中的RGB的,对于Alpha值我们单独使用下面的方程处理,这个方程和上面的是极为相似的:

A = AsrcFsrc ^ AdstFdst

这个方程基本上是一样的,但是这样分开处理的话就可以让^运算可以不同了。我们将RGB和Alpha分开处理的动机就是让我们能够单独处理RGB和Alpha值,而两者不互相影响。

10.2 BLEND OPERATIONTS

  • D3D12_BLEND_OP_ADD: C = Csrc × Fsrc + Cdst × Fdst
  • D3D12_BLEND_OP_SUBTRACT: C = Cdst × Fdst - Csrc × Fsrc
  • D3D12_BLEND_OP_REV_SUBTRACT: C = Csrc × Fsrc - Cdst × Fdst
  • D3D12_BLEND_OP_MIN: C = min(Csrc, Cdst)
  • D3D12_BLEND_OP_MAX: C = max(Csrc, Cdst)

Alpha的混合操作也是一样的。 当然你可以使用不同的操作来分别处理RGB和Alpha的混合。 举个例子来说,你可以在RGB的混合中使用+,然后在Alpha的混合中使用-

C = Csrc × Fsrc + Cdst × Fdst

A = AdstFdst - AsrcFsrc

最近在Direct3D中加入了一个新的特征(D3D12_LOGIC_OP),我们可以使用逻辑运算符来代替上面的混合操作。 具体的我就没必要放进去了,毕竟很简单理解。

但是你需要注意的是,如果你使用逻辑运算符代替混合操作,你需要注意的是逻辑运算符和传统运算符是不能同时使用的,你必须在这两种里面选择一种使用。 并且你使用逻辑运算符的话,你需要确保你的Render Target的格式支持(支持的格式应当是UINT的变种)。

10.3 BLEND FACTORS

通过改变Factors(因素),我们可以设置更多的不同的混合组合,从而来实现更多的不同的效果。 我们将会在下面解释一些混合组合,但是你还是要去体验一下他们的效果,从而能够有一个概念。 下面将会介绍一些基础的Factors。你可以去看SDK文档里面的D3D12_BLEND枚举了解到更多高级的Factors。 我们设Csrc = (rsrc, gsrc, bsrc)Asrc = asrc(这个RGBA值是由像素着色器输出的), Cdst = (rdst, gdst, bdst)Adst = adst(这个RGBA值是存储在后台缓冲中的)。

  • D3D12_BLEND_ZERO: F = (0, 0, 0, 0)
  • D3D12_BLEND_ONE: F = (1, 1, 1, 1)
  • D3D12_BLEND_SRC_COLOR: F = (rsrc, gsrc, bsrc)
  • D3D12_BLEND_INV_SRC_COLOR: Fsrc = (1 - rsrc, 1 - gsrc, 1 - bsrc)
  • D3D12_BLEND_SRC_ALPHA: F = (asrc, asrc, asrc, asrc)
  • D3D12_BLEND_INV_SRC_ALPHA: F = (1 - asrc, 1 - asrc, 1 - asrc, 1 - asrc)
  • D3D12_BLEND_DEST_ALPHA: F = (adst, adst, adst, adst)
  • D3D12_BLEND_INV_DEST_ALPHA: F = (1 - adst,1 - adst, 1 - adst, 1 - adst)
  • D3D12_BLEND_DEST_COLOR: F = (adst, adst, adst)
  • D3D12_BLEND_INV_DEST_COLOR: F = (1 - adst, 1 - adst, 1 - adst)
  • D3D12_BLEND_SRC_ALPHA_SAT: F = (a'src, a'src, a'src, a'src), a'src = clamp(asrc, 0, 1)
  • D3D12_BLEND_BLEND_FACTOR: F = (r, g, b, a)

最后一个枚举类型中的参数 (r ,g ,b ,a) 通过下面这个函数设置。

ID3D12GraphicsCommandList::OMSetBlendFactor

参数是一个Float[4],表示4个分量的值,如果设置为nullptr那么就默认全是1。

10.4 BLEND STATE

我们已经讨论过了混合操作符和混合因素,但是我们如何在Direct3D里面设置这些参数呢? 和其他的Direct3D的状态一样,混合状态也是PSO(渲染管线)的一个部分。 之前我们使用的都是默认的混合状态(禁用状态)。

我们如果要使用非默认的混合状态,我们必须填充D3D12_BLEND_DESC结构。

    struct D3D12_BLEND_DESC{
        bool AlphaToCoverageEnable; // false
        bool IndependentBlendEnable; // false
        D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[8];
    };
  • AlphaToCoverageEnable:设置成true开启alpha-to-coverage技术,这个技术是多重采样技术在渲染某些纹理 (这里的翻译无法保证正确性,因此就使用某些纹理代替) 的时候使用的。设置成false来关闭这个技术。alpha-to-coverage技术需要多重采样开启才能使用(换言之就是必须在创建后台缓冲和深度缓冲的时候开启多重采样)。

  • IndependentBlendEnable:Direct3D最多支持同时渲染8个Render Target。如果这个属性设置成true,那么就可以在渲染不同的Render Target的时候使用不同的混合参数(例如混合因素,混合操作符,混合是否开启等)。如果设置成false,那么所有的Render Target就会使用同样的混合方法(具体来说就是D3D12_BLEND_DESC::RenderTarget中的第一个元素作为所有的Render Target使用的混合方法)。对于现在来说,我们一次只使用一个Render Target

  • RenderTarget:第i个元素描述第i个Render Target使用的混合方法,如果IndependentBlendEnable设置成false,那么所有的Render Target就全部使用RenderTarget[0]这个混合方法去进行混合。

D3D12_RENDER_TARGET_BLEND_DESC结构如下所示:

    struct D3D12_RENDER_TARGET_BLEND_DESC{
        bool BlendEnable; // false
        bool LogicOpEnable; // false
        D3D12_BLEND SrcBlend; // D3D12_BLEND_ONE
        D3D12_BLEND DestBlend; // 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
    };
  • BlendEnable: 设置成true就开启混合否则就是关闭,注意的是LogicOpEnableBlendEnable不能同时开启,你必须使用其中的一个来进行混合。
  • LogicOpEnable: 设置成true就开启使用逻辑运算符的混合,然后和BlendEnable不能同时使用。
  • SrcBlend: RGB混合中的Fsrc
  • DestBlend: RGB混合中的Fdst
  • BlendOp: RGB混合中使用的操作符。
  • SrcBlendAlpha: Alpha混合中的Fsrc
  • DestBlendAlpha: Alpha混合中的Fdst
  • BlendOpAlpha: Alpha混合中使用的操作符。
  • LogicOp: 混合中使用的逻辑运算符。
  • RenderTargetWriteMask: 用于控制混合结束后哪些颜色(R,G,B,A)可以写入到后台缓冲中去。举个例子来说就是如果我们想禁止将RGB写入到后台缓冲中去的话,我们就可以设置成D3D12_COLOR_WRITE_ENABLE_ALPHA。如果混合是关闭的,那么就没有任何限制输出到后台缓冲中去。

Notice: 由于混合需要处理每一个像素,所以他的开销很大。你最好只在需要的时候才打开它。

10.5 EXAMPLES

在这个部分,我们看一些混合操作的特效例子。当然我们只看关于RGB混合的例子。

10.5.1 No Color Write

如果我们只是想单纯的留下目标像素,即源像素不会和目标像素进行混合以及覆盖,那么就可以使用这个方法。 举个例子来说就是,将目标像素输出到Depth/Stencil Buffer中去。方程在下面:

C = Csrc × (0, 0, 0) + Cdst × (1, 1, 1)

C = Cdst

另外一个方法就是将RenderTargetWriteMask设置成0。 这样的话就禁止了所有的颜色输出到Back Buffer中。

10.5.2 Adding/Subtracting

如果我们想将源像素和目标像素加起来(参见10.2)。方程如下:

C = Csrc × (1, 1, 1) + Cdst × (1, 1, 1)

C = Csrc + Cdst

我们当然也可以相减啊(参见10.3)。方程如下:

C = Csrc × (1, 1, 1) - Cdst × (1, 1, 1)

C = Csrc - Cdst

10.5.3 Multiplying

如果我们想将源像素和目标像素相乘(参见10.4)。方程如下:

C = Csrc × (0, 0, 0) x Cdst × Csrc

C = Csrc x Cdst

10.5.4 Transparency

现在我们假设Alpha分量用来控制源像素的不透明度(0就是0%,0.4就是40%)。 我们设不透明度为A,透明度为T。那么透明度和不透明度的关系就是T = 1 - A。 比如不透明度是0.4,那么透明度就是0.6。现在我们想将源像素和目标像素在保留源像素的不透明度的情况下,将目标像素透明。方程如下:

C = Csrc × (asrc, asrc, asrc) + Cdst × (1 - asrc, 1 - asrc, 1 - asrc)

C = asrc × Csrc + (1 - asrc) × Cdst

举个例子就是,我们假设asrc = 0.25,就是不透明度为25%。 当源像素和目标像素混合的时候,我们希望保留 25% 的源像素,75% 的目标像素(这里目标像素在源像素的前面,其实就是说决定目标像素的物体在决定源像素的物体前面)。方程如下:

C = Csrc × (asrc, asrc, asrc) + Cdst × (1 - asrc, 1 - asrc, 1 - asrc)

C = 0.25 × Csrc + 0.75 × Cdst

通过使用混合的方法,我们就可以绘制如10.1的物体了。这里你就需要在混合的时候注意一些东西,否则你就会绘制的时候出现问题。 我们必须遵守如下规则:

首先绘制那些不需要混合的物体。 然后将需要混合的物体按照他们和摄像机的距离排序。 然后按照从后往前的顺序绘制物体。

从后往前绘制物体的原因我不晓得如何用中文描述了,这个我觉得很显然啊。所以下面是我的BB

对于一个物体来说,我们要将他进行混合的话,我们保留的是后面的像素,因此我们需要先绘制保留的像素,然后在绘制其他的像素。

对于10.5.1,绘制的顺序已经没有意义了,反正不会输出。对于10.5.210.5.3我们仍然先绘制不需要混合的物体,然后绘制需要混合的物体。 这是因为我们需要在开始混合之前将所有的不进行混合的像素确定下来。在这里我们并不需要排序需要混合的物体。因为这个运算是满足交换律的。我们设源像素为B

B' = B + C0 + ... + Cn-1

B' = B - C0 + ... - Cn-1

B' = B ^ C0 + ... ^ Cn-1

10.5.5 Blending and the Depth Buffer

当我们进行前面说的几种混合(不包括第一种)的时候,在深度测试的时候可能会有一点问题。 这里我们以加法混合作为例子,其他的混合是差不多的思路。

原文说了那么多其实就是想告诉你如果你开启了深度测试的话,你不按照从后往前的顺序绘制的话,那么就可能有像素因为深度测试而丢弃,从而没有累加到混合的方程中去,导致最后的颜色有点小问题。 因此我就不直接翻译原文了(原文里面有一部分单词语法没理解,我英语是个渣,但是意思是这个意思没错)。

10.6 ALPHA CHANNELS

10.5.4中我们使用Alpha分量在RGB混合中去控制物体的不透明度。 用于混合的源像素的颜色来自像素着色器。 回顾上一个章节,我们返回Diffuse材质的Alpha值作为像素着色器输出的Alpha值。 因此Diffuse MapAlpha通道的值就是用于透明度的。

你可以使用图片编辑软件(例如PS)给任何图片加入Alpha通道,然后将图片存储为支持Alpha通道的图片格式,例如DDS

10.7 CLIPPING PIXELS

有时候我们想从正在处理的源像素中完全剔除一部分像素。 我们可以通过时候HLSL内置的函数Clip(x)(这个函数只能在像素着色器中使用)。 这个函数在x < 0的时候将会丢弃现在正在处理的像素,即这个像素不会被绘制到后台缓冲中去,也不会进行之后的一系列处理。 这个方法通常用于绘制电线或者篱笆贴图。参见10.6。我们可以通过这样的方法绘制的时候一些地方不透明一些地方透明。

float4 PS(VertexOut pIn) : SV_TARGET
{
    clip(pIn.color.a - x); //如果当前像素的Alpha小于x,那么就剔除他。
    return pIn.color; //返回颜色。
}

注意的是clip开销也是有的,所以建议只在需要使用的时候使用。

我们可以使用混合做到同样的效果,但是这种方法相比较来说更为有效。 对于一个被剔除的像素来说,之后的关于他的操作会被跳过(例如混合,深度测试什么的)。

图片10.7显示了一个混合例子。 他使用透明混合来绘制半透明的水,使用剔除测试(Clip Test)来绘制栅栏盒。 值得注意的是,由于我们可以透过盒子看到盒子的背面,所以我们要关闭背面剔除。

10.8 FOG

为了在游戏中模仿准确的天气环境,我们需要能够实现一个雾的特效。 参见图片10.8。可以显然看出雾的效果。 雾可以遮掩原处的物体,并且阻止PoppingPopping就是当一个物体原本在可视范围(Far Plane)外的时候,然后因为摄像机的移动导致这个物体移动到了可视范围(Frustum)内,因此这个物体就可以被看见。 这样物体突然出现,会有一种比较奇怪的感觉。 但是如果使用雾层的话,那么远处的物体即使出现也会因为雾的效果模糊,从而不会显得那么突然。 注意即使你的场景发生在晴天,你也最好在远处保持一层雾层,因为即使在晴天,远处的物体的出现和消失也是一个和距离有关的函数。我们可以使用雾来模仿这一个大气环境现象。

我们实现雾的方法:我们需要确定雾的颜色,雾的起始位置距离摄像机多远,雾的范围(这个范围从雾的起始点开始到完全遮掩物体)。 在三角形上面的一个点的颜色是一个权重的平均值:

foggedColor = litColor + s(fogColor - litColor)

foggedColor = (1 - s) × litColor + s × fogColor

参数s的范围在 [0,1] 之间他是一个关于摄像机到物体某一个面上面的一个点的距离的函数。 随着距离的逐渐增大,这个点会逐渐被雾遮住。s的定义:

s = saturate((dist(p,E) - fogStart) / fogRange)

dist(p,E) 表示的是摄像机到点的距离。saturate 将参数保持在 [0,1] 范围内(大于1就是1,小于0就是0,否则就是原本的值)。(看图都能看明白...)

图片10.10就绘制了这个函数s。我们可以看到当dist(p, E) <= fogStart的时候,s = 0,并且最终的颜色就等于原本的颜色了(foggedColor = litColor)。

换句话来说,雾并没有修改距离摄像机小于fogStart的顶点的颜色。 雾只会对那些距离摄像机大于fogStart的物体起作用。

我们设fogEnd = fogStart + fogRange。当dist(p, E) >= fogEnd,s = 1的时候最终的颜色就是雾的颜色了(foggedColor = fogColor)。

换句话来说,雾完全遮住了距离摄像机大于等于FogEnd的点的颜色,所以你只能看见雾的颜色。

FogStart < dist(p, E) < fogEnd的时候,我们可以看到s随着 dist(p, E) 的增长而线性增长。 这意味着,当距离越来越远的时候,雾的颜色在最后的颜色中占的比重越来越大,原本的颜色占的比重越来越小了。 当然,随着距离越来越远,雾造成的模糊越来越剧烈。

...省略下面的代码部分。

有些场景并不想使用雾,我们可以定义一个宏定义来在编译着色器的时候决定是否开启雾效。 这样的话,如果我们不想使用雾效的话,就不会产生额外的运算开销了。具体参见D3D_SHADER_MACRO

10.9 SUMMARY

  • 混合是一项能够让我们将正在处理的像素(Source Pixel)和已经处理好的像素(Destination Pixel)进行混合的技术。
  • 混合是有一个混合方程的,注意的是RGBAlpha混合是独立的。
  • Fsrc, Fdst... 称作混合因素(Blend Factors),他提供了方法让我们自己定义混合方程。对于Alpha混合是不能使用带有 _COLOR 关键字的参数。
  • Alpha值来自于漫反射材质。在我们的框架中,漫反射材质定义为一张纹理,并且纹理的Alpha通道存储了Alpha信息。
  • 源像素可以通过使用HLSL函数Clip(x)来将其丢弃,从而不对其进行进一步处理。这个函数只能在像素着色器里使用,如果x < 0的话,那么现在处理的像素就会被丢弃。
  • 使用雾来模拟各种各样的天气和大气远景。在我们的线性雾模型中,我们定义一个雾的颜色,一个雾的起始点(距离摄像机的距离),一个雾的范围。一个面上的点的颜色将使用方程来计算他的颜色。

终于写完了。虽然感觉和原版差距好大。但是我觉得应该能够看懂吧?2333。 感觉原版的书及其啰嗦啊,虽然很厉害就是了。