图形学基础|景深效果(Depth of Field/DOF)

图形学基础|景深效果(Depth of Field/DOF)

文章目录

  • 图形学基础|景深效果(Depth of Field/DOF)
    • 一、前言
    • 二、景深效果
      • 2.1 物理原理
      • 2.2 弥散圆量化
    • 三、景深实现
      • 3.1 非物理的简单实现
      • 3.2 基于物理的景深效果
        • 3.2.1 CoC计算
        • 3.2.2 下采样和预滤波
        • 3.2.3 散景滤波
        • 3.2.4 后滤波
        • 3.2.5 混合
    • 参考博文

一、前言

景深(DOF) 是模拟摄像机镜头对焦特性的一种常见的后处理效果。

在现实生活中,相机只能对特定距离内的物体进行锐利的聚焦,离相机较近或较远的物体会有些失焦。

模糊不仅提供了一个关于物体距离的视觉提示,还引入了焦外成像(Bokeh,散景)。

Bokeh,亦称焦外,是一个摄影术语,一般表示在景深较浅的摄影成像中,落在景深以外的画面,会有逐渐产生松散模糊的效果。

景深示意图:

图形学基础|景深效果(Depth of Field/DOF)_第1张图片

散景示意图:

图形学基础|景深效果(Depth of Field/DOF)_第2张图片

二、景深效果

2.1 物理原理

景深,指的是相机对焦点前后相对清晰的成像范围。在景深之内的图像比较清楚,在这个范围之前或是之后的图像则比较模糊。

相机的最简单形式是完美的针孔相机,它有一个用于记录光的图像平面。在此平面之前是一个被称为光圈(aperture) 的小孔:仅允许一个光束通过。

相机前面的物体会向多个方向发射或反射光,从而产生大量光线。对于每个点,只有一条光线能够穿过孔并被记录。

如下图,记录了3个点。

图形学基础|景深效果(Depth of Field/DOF)_第3张图片

因为每个点只会产生一个光束,因此记录是产生的图像总是锐利的。

但单一的光束不够亮,需要很长的时间积累(这意味着摄像机需要一个长曝光时间)才能得到一张清晰的图像。

为减少曝光时间,光需要积累得足够快。唯一的方法是同时记录多个光线。这能通过增加光圈的半径来实现。

假设光圈为圆形,这意味着每一个点都会在图片平面上投影光束,而不是光线。所以我们会接收更多的光线,但不再是一个点的,而是一个圆盘的。

如下图,使用更大的光圈。

图形学基础|景深效果(Depth of Field/DOF)_第4张图片

为了重新聚焦光线,我们需要将光束还原为一个点,这可以通过在光圈前加一个镜头来实现。镜头可以弯曲光线,使其重新聚焦。这能形成一个亮且锐利的成像,但是只在一个范围内。

对于远处的点,聚焦不够,而对于近处的点,聚焦又过了。两者都让成像变为了光束,也就变模糊了,而此模糊投影正是弥散圆(circle of confusion),简称为CoC

图形学基础|景深效果(Depth of Field/DOF)_第5张图片

2.2 弥散圆量化

当成像平面不在焦点上时,光线无法聚焦在一点,就会朝四周分散开。散开的范围被称为Circle Of Confusion,简称COC。

如图所示,展示了不同半径的弥散圆。
图形学基础|景深效果(Depth of Field/DOF)_第6张图片

那么如何量化这个弥散圆的大小呢?

根据下图:

图形学基础|景深效果(Depth of Field/DOF)_第7张图片

镜头(光圈直径为 A A A)和成像平面位置( 成像平面距光圈的距离为 I I I)。

在距镜头距离为 P P P的点,刚好可以落在成像平面上。

根据凸透镜成像公式:

1 u + 1 v = 1 F \frac{1}{u} + \frac{1}{v} = \frac{1}{F} u1+v1=F1

其中, u u u表示物距, v v v表示像距, F F F表示焦距。

则有:

1 P + 1 I = 1 F \frac{1}{P} + \frac{1}{I} = \frac{1}{F} P1+I1=F1

在距镜头距离为 D D D的物体,在成像平面上形成直径为 C C C的弥散圆,其经凸透镜聚焦为投影点,假设其距离光圈的距离为 X X X

1 D + 1 X = 1 F \frac{1}{D} + \frac{1}{X} = \frac{1}{F} D1+X1=F1

如下图所示,根据相似三角形有:

C A = X − I X \frac{C}{A} = \frac{X-I}{X} AC=XXI

图形学基础|景深效果(Depth of Field/DOF)_第8张图片

C = A ⋅ X − I X = A ⋅ D F D − F − P F P − F D F D − F = ∣ A F ( P − D ) D ( P − F ) ∣ \begin{aligned} C &= A \cdot \frac{X-I}{X} \\ &= A \cdot \frac{\frac{DF}{D-F}-\frac{PF}{P-F}}{\frac{DF}{D-F}} \\ &= \left | A \frac{F(P-D)}{D(P-F)} \right | \end{aligned} C=AXXI=ADFDFDFDFPFPF=AD(PF)F(PD)

其中,图中的各个符号为:

C C C:弥散圆直径;

  • 在计算中通常使用用CoC表示弥散圆半径。

A A A:光圈直径;

F F F:焦距;

  • 焦距属于镜头的固有属性,不因其他因素的变化而变化,是平行光入射镜头后聚焦的位置与镜头中心的距离。

P P P :聚焦位置;

  • 指的是一个特定的物距,指在确定了镜头和成像平面位置之后,镜头前有一个刚好可以在成像面上聚焦的位置,我们称这个位置与镜头中心的距离为 P P P
  • 图上的Plane in focus直译可能会有些误导,其实图上的本意是站在一个空间的角度来看,能够在镜头前聚焦的位置应该是一个平行于镜头、距离固定的平面,平面上每个点反射的光都可以在成像面上汇聚成一个点

D D D:物距;

  • 物体距离镜头的距离,即要求这个距离的物体在镜头上的COC弥散圆。

I I I:成像面距离;

  • 成像面与镜头中心的距离。

P P P可以通过 I I I F F F确定,在镜头和成像面确定的情况下也可以称之为常量。

因此,我们就得到了一个以 C C C为因变量, D D D为自变量的函数,这个函数的图像(不加绝对值)是这样的:

图形学基础|景深效果(Depth of Field/DOF)_第9张图片

MaxBgdCoC,指背景最大弥散圆半径,可以通过左边的这个算式得到。

在函数图像中,z即为上面公式的 D D D,也就是自变量,纵轴即为CoC值,可以看到在P点,弥散圆半径为0。

前景后景随着距离增大,弥散圆半径逐渐增大,而且前景的变化趋势更为陡峭。

至此,我们就可以描述整个屏幕上每一个点成像的弥散范围了。

这也就是我们后续进行屏幕后处理计算的基础:获取屏幕上的点,以及它距离摄像机的距离,其他常量都可以通过参数进行调整。(成像面距离不要小于焦距,否则无法聚焦)。

三、景深实现

3.1 非物理的简单实现

这个部分比较简单,笔者就没有实现。

最简单的实现如下图所示,从摄像机位置开始,按照距离分为三个部分:

  • 近距离模糊;
  • 焦点范围清晰;
  • 远距离模糊;

图形学基础|景深效果(Depth of Field/DOF)_第10张图片

渲染的时候按深度(即距离)进行判断,在焦点范围内则是清晰的,否则就进行模糊处理。

整个过程共分为三个Pass:

  1. 将场景渲染到一个RenderTarget,作为清晰版;
  2. 将上一步得到的RenderTarget进行模糊处理,得到BluredRT(模糊版);
  3. 合成!跟据距离来判断是否应该模糊,如果不在焦点范围内则绘制BluredRT,否则就绘制RenderTarget。

示例Shader代码:

sampler RenderTarget;
sampler BluredRT;
// 焦点范围
float fNearDis;
float fFarDis;
float4 ps_main( float2 TexCoord : TEXCOORD0 ) : COLOR0
{
	float4 color = tex2D( RenderTarget, TexCoord );
	if( color.a > fNearDis && color.a < fFarDis )	
		return color;
	else
		return tex2D( BluredRT, TexCoord );
}

效果如下:

图形学基础|景深效果(Depth of Field/DOF)_第11张图片

可以看出,上图看起来很不自然。

原因就是DOF在清晰与模糊的交界处过渡太生硬。

可以通过增加两个过渡带实现一个简单的渐变。

图形学基础|景深效果(Depth of Field/DOF)_第12张图片

示例Shader代码:

sampler RenderTarget;
sampler BluredRT;
// 焦点范围
float fNearDis;
float fFarDis;
float fNearRange;
float fFarRange;
float4 ps_main( float2 TexCoord : TEXCOORD0 ) : COLOR0
{
	float4 sharp = tex2D( RenderTarget, TexCoord );
	float4 blur= tex2D( BluredRT, TexCoord );
   	// sharp.a存储的
	float percent = max(saturate(sharp.a-fNearDis)/fNearRange),saturate((sharp.a-(fFarDis-fFarRange))/fFarRange));
	return lerp( sharp, blur, percent );
}

效果如下:

图形学基础|景深效果(Depth of Field/DOF)_第13张图片

3.2 基于物理的景深效果

3.1 仅通过不加区分的模糊操作来实现景深DOF效果,效果比较一般。

比较符合物理的做法,是基于2.2推导的弥散圆Circle Of Confusion(COC),将某点的像素值均匀地沿着弥散圈扩散。

但在GPU中实现向外扩散的操作是非常困难的。

因而,通常将该过程反过来,计算某个像素点的颜色时,根据周围像素点弥散圈的大小从周围的像素点中搜集 (gather) 像素颜色。

动视暴雪在Siggraph2014上分享了他们的Scatter As Gather方法。

图形学基础|景深效果(Depth of Field/DOF)_第14张图片

搜集的过程,就是在周围的像素点中,寻找处于周围像素点弥散圈范围内的像素点,将其颜色按照一定的权重累加。

如图所示:

  • 左边,五个点:红色和蓝色处于近处,黄色和紫色点处于远处,绿色点是正确对焦处。

  • 右边,展示了每个像素点的弥散圈范围,弥散圈的范围越大,对其他像素点的影响越小,中间黑色的点的位置受到两个弥散圈的影响。

图形学基础|景深效果(Depth of Field/DOF)_第15张图片

整个景深后处理的过程大致是这样的:
图形学基础|景深效果(Depth of Field/DOF)_第16张图片
笔者参考Unity-Technologies/PostProcessing/DepthOfField和CatlikeCoding-DOF实现一个稍微有点基于物理的景深DOF效果。

先给出实现的效果图:

图形学基础|景深效果(Depth of Field/DOF)_第17张图片

笔者的实现共分为5个Pass,分别为:

  • CoC(弥散圆计算);
  • DownSample and Prefilter(下采样和预滤波);
  • Bokeh Filter(散景滤波);
  • Postfilter(后滤波);
  • Combine(混合);

实现基于DX12的ComputeShader。

3.2.1 CoC计算

为了实现的简单方便,CoC的公式并没有选择2.2中介绍的公式。而采用了下面的一种方法。

基于一个焦点距离(FoucusDistance)一个简单的焦点区域(FoucusRange):

正如CatlikeCoding-DOF中使用的:

图形学基础|景深效果(Depth of Field/DOF)_第18张图片

CoC值采用如下公式:

c o c = S c e n e D e p t h − F o u c u s D i s t a n c e F o u c u s R a n g e coc = \frac{SceneDepth - FoucusDistance}{FoucusRange} coc=FoucusRangeSceneDepthFoucusDistance

其中, S c e n e D e p t h SceneDepth SceneDepth表示像素在相机空间的深度。

如下图所示,横轴表示深度的变化,纵轴表示Coc的量化值(这里将其截断为-1到1之间的值)。

图形学基础|景深效果(Depth of Field/DOF)_第19张图片

具体的Shader代码如下:

RWTexture2D  CoCBuffer               : register(u0);
Texture2D    DepthBuffer             : register(t0);

cbuffer CB0 : register(b0)
{
    float   FoucusDistance;
    float   FoucusRange;
    float2  ClipSpaceNearFar;
};

[numthreads(8, 8, 1)]
void cs_FragCoC
(
    uint3 DTid  : SV_DispatchThreadID
)
{
    // screenPos
    const uint2 ScreenST = DTid.xy;
    // non-linear depth
    float Depth = DepthBuffer[ScreenST];
    // 转到相机空间的深度
    float SceneDepth = LinearEyeDepth(Depth, ClipSpaceNearFar.x, ClipSpaceNearFar.y);
    
    // compute simple coc 
    float coc = (SceneDepth - FoucusDistance) / FoucusRange;
    coc = clamp(-1, 1, coc);
    // 将[-1,1]转换为[0,1]
    CoCBuffer[ScreenST] = saturate(coc * 0.5f + 0.5f);
}

3.2.2 下采样和预滤波

我们将在半分辨率的情况下创建散景的效果。所以需要一个下采样的Pass。

在这个Pass中,我们将获取邻近四纹素中最显著的CoC值,而非进行平均(这样并没有意义)。\

对于颜色,进行了基于亮度和coc绝对值的加权。

最后,输出的四通道中rgb存储了下采样滤波后的颜色值,alpha通道存储了coc值。

注意,这里的coc值乘以了BokehRadius(散景半径),转换为了散景距离值。

图形学基础|景深效果(Depth of Field/DOF)_第20张图片

具体的Shader代码如下:

RWTexture2D PrefilterColor		: register(u0);
Texture2D	SceneColor			: register(t0);
Texture2D	CoCBuffer			: register(t1);

SamplerState LinearSampler : register(s0);

cbuffer CB0 : register(b0)
{
    float   BokehRadius;
};

void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 HalfPixelSize)
{
    float2 ScreenSize;
    PrefilterColor.GetDimensions(ScreenSize.x, ScreenSize.y);
    float2 InvScreenSize = rcp(ScreenSize);
    HalfPixelSize = 0.5 * InvScreenSize;
    UV = ScreenCoord * InvScreenSize + HalfPixelSize;
}

[numthreads(8, 8, 1)]
void cs_FragPrefilter(uint3 DTid : SV_DispatchThreadID)
{
    float2 HalfPixelSize, UV;
    GetSampleUV(DTid.xy, UV, HalfPixelSize);

    // screenPos
    float2 uv0 = UV - HalfPixelSize;
    float2 uv1 = UV + HalfPixelSize;
    float2 uv2 = UV + float2(HalfPixelSize.x, -HalfPixelSize.y);
    float2 uv3 = UV + float2(-HalfPixelSize.x, HalfPixelSize.y);

    // Sample source colors
    float3 c0 = SceneColor.SampleLevel(LinearSampler, uv0, 0).xyz;
    float3 c1 = SceneColor.SampleLevel(LinearSampler, uv1, 0).xyz;
    float3 c2 = SceneColor.SampleLevel(LinearSampler, uv2, 0).xyz;
    float3 c3 = SceneColor.SampleLevel(LinearSampler, uv3, 0).xyz;

    float3 result = (c0 + c1 + c2 + c3) * 0.25;

    // Sample CoCs
    // convert [0,1] to [-1,1]
    float coc0 = CoCBuffer.SampleLevel(LinearSampler, uv0, 0).r * 2 - 1;
    float coc1 = CoCBuffer.SampleLevel(LinearSampler, uv1, 0).r * 2 - 1;
    float coc2 = CoCBuffer.SampleLevel(LinearSampler, uv2, 0).r * 2 - 1;
    float coc3 = CoCBuffer.SampleLevel(LinearSampler, uv3, 0).r * 2 - 1;

    // Apply CoC and luma weights to reduce bleeding and flickering
    float w0 = abs(coc0) / (Max3(c0.r, c0.g, c0.b) + 1.0);
    float w1 = abs(coc1) / (Max3(c1.r, c1.g, c1.b) + 1.0);
    float w2 = abs(coc2) / (Max3(c2.r, c2.g, c2.b) + 1.0);
    float w3 = abs(coc3) / (Max3(c3.r, c3.g, c3.b) + 1.0);

    // Weighted average of the color samples
    half3 avg = c0 * w0 + c1 * w1 + c2 * w2 + c3 * w3;
    avg /= max(w0 + w1 + w2 + w3, 1e-4);

    // Select the largest CoC value
    float cocMin = min(min(min(coc0, coc1), coc2), coc3);
    float cocMax = max(max(max(coc0, coc1), coc2), coc3);
    float coc = (-cocMin >= cocMax ? cocMin : cocMax) * BokehRadius;

    // Premultiply CoC again
    avg *= smoothstep(0, HalfPixelSize.y * 4, abs(coc));

    // alpha通道存储了coc值
    PrefilterColor[DTid.xy] = float4(avg, coc);
}

3.2.3 散景滤波

在半分辨率预过滤后,我们需要创建散景图。采用的方法即前面所介绍的Gather的方法。

通过圆盘采样(DiskKernel)来查看采样点的Coc是否覆盖了当前中心点。

并且分别累积前散景和后散景。

圆盘采样提供了好几个,不同的采样数量,所形成环不同。

笔者这里选择了KERNEL_LARGE

#if defined(KERNEL_LARGE)
// rings = 4
// points per ring = 7
static const int kSampleCount = 43;
static const float2 kDiskKernel[kSampleCount] = {
    float2(0,0),
    float2(0.36363637,0),
    float2(0.22672357,0.28430238),
    float2(-0.08091671,0.35451925),
    float2(-0.32762504,0.15777594),
    float2(-0.32762504,-0.15777591),
    float2(-0.08091656,-0.35451928),
    float2(0.22672352,-0.2843024),
    float2(0.6818182,0),
    float2(0.614297,0.29582983),
    float2(0.42510667,0.5330669),
    float2(0.15171885,0.6647236),
    float2(-0.15171883,0.6647236),
    float2(-0.4251068,0.53306687),
    float2(-0.614297,0.29582986),
    float2(-0.6818182,0),
    float2(-0.614297,-0.29582983),
    float2(-0.42510656,-0.53306705),
    float2(-0.15171856,-0.66472363),
    float2(0.1517192,-0.6647235),
    float2(0.4251066,-0.53306705),
    float2(0.614297,-0.29582983),
    float2(1,0),
    float2(0.9555728,0.2947552),
    float2(0.82623875,0.5633201),
    float2(0.6234898,0.7818315),
    float2(0.36534098,0.93087375),
    float2(0.07473,0.9972038),
    float2(-0.22252095,0.9749279),
    float2(-0.50000006,0.8660254),
    float2(-0.73305196,0.6801727),
    float2(-0.90096885,0.43388382),
    float2(-0.98883086,0.14904208),
    float2(-0.9888308,-0.14904249),
    float2(-0.90096885,-0.43388376),
    float2(-0.73305184,-0.6801728),
    float2(-0.4999999,-0.86602545),
    float2(-0.222521,-0.9749279),
    float2(0.07473029,-0.99720377),
    float2(0.36534148,-0.9308736),
    float2(0.6234897,-0.7818316),
    float2(0.8262388,-0.56332),
    float2(0.9555729,-0.29475483),
};

#endif

具体的Shader代码如下:

#define KERNEL_LARGE
#include "DiskKernels.hlsl"
RWTexture2D BokehColor			: register(u0);
Texture2D	PrefilterColor		: register(t0);

SamplerState LinearSampler : register(s0);

cbuffer CB0 : register(b0)
{
    float  BokehRadius;
};

//------------------------------------------------------- HELP FUNCTIONS

void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 PixelSize)
{
    float2 ScreenSize;
    BokehColor.GetDimensions(ScreenSize.x, ScreenSize.y);
    float2 InvScreenSize = rcp(ScreenSize);
    PixelSize = InvScreenSize;
    UV = ScreenCoord * InvScreenSize + 0.5 * InvScreenSize;
}

//------------------------------------------------------- ENTRY POINT
// Bokeh filter with disk-shaped kernels

[numthreads(8, 8, 1)]
void cs_FragBokehFilter(uint3 DTid : SV_DispatchThreadID)
{
    float2 PixelSize, UV;
    GetSampleUV(DTid.xy, UV, PixelSize);

    float4 center = PrefilterColor.SampleLevel(LinearSampler, UV, 0);

    float4 bgAcc = 0.0; // Background: far field bokeh
    float4 fgAcc = 0.0; // Foreground: near field bokeh
    for (int k = 0; k < kSampleCount; ++k)
    {
        float2 offset = kDiskKernel[k] * BokehRadius;
        float dist = length(offset);
        offset *= PixelSize;
        float4 samp = PrefilterColor.SampleLevel(LinearSampler, UV + offset, 0);

        // BG: Compare CoC of the current sample and the center sample and select smaller one.
        float bgCoC = max(min(center.a, samp.a), 0.0);

        // Compare the CoC to the sample distance. Add a small margin to smooth out.
        const float margin = PixelSize.y * 2;
        float bgWeight = saturate((bgCoC - dist + margin) / margin);
        // Foregound's coc is negative
        float fgWeight = saturate((-samp.a - dist + margin) / margin);

        // Cut influence from focused areas because they're darkened by CoC premultiplying. This is only needed for near field.
        // 减少聚焦区域的影响,它们因CoC预乘而变暗。这仅适用于近场。
        fgWeight *= step(PixelSize.y, -samp.a);

        // Accumulation
        bgAcc += half4(samp.rgb, 1.0) * bgWeight;
        fgAcc += half4(samp.rgb, 1.0) * fgWeight;
    }

    // Get the weighted average.
    bgAcc.rgb /= bgAcc.a + (bgAcc.a == 0.0); // zero-div guard
    fgAcc.rgb /= fgAcc.a + (fgAcc.a == 0.0);

    // FG: Normalize the total of the weights.
    // 归一化前散景权重
    fgAcc.a *= 3.14159265359 / kSampleCount;

    // Alpha premultiplying
    float alpha = saturate(fgAcc.a);
    // 前散景和后散景融合
    float3 rgb = lerp(bgAcc.rgb, fgAcc.rgb, alpha);
    // alpha存储的是前散景的权重
    BokehColor[DTid.xy] = float4(rgb, alpha);
}

3.2.4 后滤波

在创建散景效果之后再添加一个额外的模糊pass,这是一个后处理pass。采用3x3被称为帐篷滤波 (tent filter) 的卷积核。

图形学基础|景深效果(Depth of Field/DOF)_第21张图片

图形学基础|景深效果(Depth of Field/DOF)_第22张图片

通过一个半纹素偏移的方形滤波器,基于GPU的双线性插值,实现了一个小高斯模糊。

具体的Shader代码如下:

RWTexture2D PostfilterColor		: register(u0);
Texture2D	BokehColor		    : register(t0);

SamplerState LinearSampler : register(s0);

//------------------------------------------------------- HELP FUNCTIONS
void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 HalfPixelSize)
{
    float2 ScreenSize;
    PostfilterColor.GetDimensions(ScreenSize.x, ScreenSize.y);
    float2 InvScreenSize = rcp(ScreenSize);
    HalfPixelSize = 0.5 * InvScreenSize;
    UV = ScreenCoord * InvScreenSize + HalfPixelSize;
}

//------------------------------------------------------- ENTRY POINT
// tent filter
/**
*   1 2 1
*   2 4 2
*   1 2 1
*/

[numthreads(8, 8, 1)]
void cs_FragPostfilter(uint3 DTid : SV_DispatchThreadID)
{
    // 9 tap tent filter with 4 bilinear samples
    float2 HalfPixelSize, UV;
    GetSampleUV(DTid.xy, UV, HalfPixelSize);

    float2 uv0 = UV - HalfPixelSize;
    float2 uv1 = UV + HalfPixelSize;
    float2 uv2 = UV + float2(HalfPixelSize.x, -HalfPixelSize.y);
    float2 uv3 = UV + float2(-HalfPixelSize.x, HalfPixelSize.y);

    float4 acc = 0;
    acc += BokehColor.SampleLevel(LinearSampler, uv0, 0);
    acc += BokehColor.SampleLevel(LinearSampler, uv1, 0);
    acc += BokehColor.SampleLevel(LinearSampler, uv2, 0);
    acc += BokehColor.SampleLevel(LinearSampler, uv3, 0);

    PostfilterColor[DTid.xy] = acc / 4.0f;
}

3.2.5 混合

使用后滤波的散景图和原始的场景图进行混合。

基于coc值进行非线性的插值。

具体的Shader代码如下:

RWTexture2D CombineColor		: register(u0);
Texture2D	TempSceneColor		: register(t0);
Texture2D	CoCBuffer		    : register(t1);
Texture2D	FilterColor		    : register(t2);

SamplerState LinearSampler : register(s0);

cbuffer CB0 : register(b0)
{
    float  BokehRadius;
};

//------------------------------------------------------- HELP FUNCTIONS
void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 PixelSize)
{
    float2 ScreenSize;
    CombineColor.GetDimensions(ScreenSize.x, ScreenSize.y);
    float2 InvScreenSize = rcp(ScreenSize);
    PixelSize = InvScreenSize;
    UV = ScreenCoord * InvScreenSize + 0.5 * PixelSize;
}

//------------------------------------------------------- ENTRY POINT
[numthreads(8, 8, 1)]
void cs_FragCombine(uint3 DTid : SV_DispatchThreadID)
{
    // screenPos
    float2 PixelSize, UV;
    GetSampleUV(DTid.xy, UV, PixelSize);
    float3 source = TempSceneColor.SampleLevel(LinearSampler, UV, 0);
    // 采样当前片段的CoC值
    float coc = CoCBuffer.SampleLevel(LinearSampler, UV, 0);
    // 转换为散景距离
    coc = (coc - 0.5) * 2.0 * BokehRadius;
    // 采样散景
    float4 dof = FilterColor.SampleLevel(LinearSampler, UV, 0);
    // Convert CoC to far field alpha value.
    // 对CoC正值进行插值,用于获取后散景
    float ffa = smoothstep(PixelSize.y * 2.0, PixelSize.y * 4.0, coc);
    // 非线性插值
    float3 color = lerp(source, dof.rgb, ffa + dof.a - ffa * dof.a);
    CombineColor[DTid.xy] = color;
}

参考博文

  • 基于物理的景深效果
  • DOF的改进算法
  • 渲染中的景深(Depth of Field/DOF)
  • CatlikeCoding-DOF
  • 景深效果(译)
  • Unity-DOF-Shader
  • UE4景深后处理效果学习笔记(一)基本原理
  • UE4景深后处理效果学习笔记(二)效果实现

你可能感兴趣的:([图形学基础])