URP Bokeh DOF 分析

URP Bokeh DOF 分析

“眼中只有你!”


阅读注意:
  • 本文的URP 版本为 10.8.1

1 景深与散景

景深是指在摄像机镜头前方的一段范围内,我们获得的图像清晰度是可以接受的。

我们以下面的简化模型为例:
URP Bokeh DOF 分析

理想状态下,光线从物体处通过透镜完美地聚焦在成像平面上,此时我们就能看到一个锐利的成像。但更多的时候,由于物体过远或过近,光线都无法汇聚到成像平面上,形成了一个模糊圆或弥散圆(circle of confusion,即CoC)。
URP Bokeh DOF 分析

弥散圆在一定范围内,清晰度都是可以被人眼所接受的。那么这段范围所对应的物体距离范围,就是景深。
URP Bokeh DOF 分析

在景深范围外,图像呈现模糊状态。
URP Bokeh DOF 分析

散景是指透镜渲染失焦部分的方式,不同镜头的孔径形状会产生不同的失焦图像。
URP Bokeh DOF 分析

不过,本文只会涉及光圈和快门叶片数量对散景的影响。

2 基本方向

来自ATI实验室的一篇论文Real-Time Depth of Field Simulation提供了一种思路。大致就是,在第一遍渲染场景时,计算每个点的弥散圆大小。在第二遍时通过一个滤波内核来过滤图像,这个内核的大小由当前弥散圆的大小控制。
URP Bokeh DOF 分析

当弥散圆大小为0 时,内核大小缩放到一个像素内,所有采样点均来自于该点,该处的图像是锐利的。当弥散圆大小不为1时,内核扩大到周围像素,此时图像是模糊的,并具有散景效果。

虽然本文和上述策略并不完全相同,但通过周围采样积累制造散景的方向是一致的。

在URP中算法分为了五个Pass。

  • 计算弥散圆
  • 降采样以及预处理颜色
  • 制造散景模糊
  • 后置滤波
  • 合成图像

3 实现细节

URP中可以设置的参数如下:

URP Bokeh DOF 分析

其中:

  • Focus Distance:对焦距离
  • Focal Length:焦距,单位mm
  • Aperture:光圈比值(f-stop或f-number),通过焦距除以Aperture可获得光圈直径。
  • Blade Count:形成光圈的叶片数量
  • Blade Curvature:叶片曲率,数值为1时,叶片完全呈现圆形
  • Blade Rotation:控制叶片的旋转角度

3.1 弥散圆的计算

我们首先要解决的弥散圆的计算,我们以下图为例:
URP Bokeh DOF 分析

其中物距URP Bokeh DOF 分析是我们的对焦距离(Focus Distance),它对应的像距是URP Bokeh DOF 分析,完美地聚焦在像平面上;物距URP Bokeh DOF 分析对应像距URP Bokeh DOF 分析,在像平面上形成一个弥散圆,弥散圆的直径为URP Bokeh DOF 分析URP Bokeh DOF 分析为焦距,URP Bokeh DOF 分析是光圈直径。它们之间的关系有:

凸透镜成像公式:
URP Bokeh DOF 分析
光圈直径URP Bokeh DOF 分析、焦距URP Bokeh DOF 分析f-stop之间的关系:
URP Bokeh DOF 分析
图中右侧存在明显的相似三角形(我们把URP Bokeh DOF 分析看做变量,他可能比URP Bokeh DOF 分析大,也可能比URP Bokeh DOF 分析小):
URP Bokeh DOF 分析
然后我们根据透镜公式进行推导:
URP Bokeh DOF 分析
那么,我们就可以计算弥散圆直径:
URP Bokeh DOF 分析
事实上,我们将结果的左半边看作最大弥散圆直径URP Bokeh DOF 分析
URP Bokeh DOF 分析
这个URP Bokeh DOF 分析实际上就是平行光线入射产生的最大弥散圆:
URP Bokeh DOF 分析

这让我们能够提前计算一部分,事实上URP也是这么做的:

// PostProcessPass.cs

// "A Lens and Aperture Camera Model for Synthetic Image Generation" [Potmesil81]
float F = m_DepthOfField.focalLength.value / 1000f; // 统一单位
float A = m_DepthOfField.focalLength.value / m_DepthOfField.aperture.value;
float P = m_DepthOfField.focusDistance.value;
float maxCoC = (A * F) / (P - F);

我们来看看片元着色器:

half FragCoC(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);
    float depth = LOAD_TEXTURE2D_X(_CameraDepthTexture, _SourceSize.xy * uv).x;
    float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams);
	// 弥散圆的计算公式
    half coc = (1.0 - FocusDist / linearEyeDepth) * MaxCoC;
    // 弥散圆大小限制在[-1,1]
    half nearCoC = clamp(coc, -1.0, 0.0);   // 近景CoC
    half farCoC = saturate(coc);			// 远景CoC

    return saturate((farCoC + nearCoC + 1.0) * 0.5);
}

这里值得注意的是,我们以对焦距离为分界线,将弥散圆分为了近景和远景,还将大小限制在了-1到1。这里的CoC更像是一个比例关系,真正的弥散圆大小还需要乘上一个根据屏幕大小实际调整的值。

这个阶段会输出一张CoC材质_FullCoCTexture

注意:虽然我们计算的是弥散圆直径大小,但后续更多地是把它当作半径处理!

3.2 降采样与创建遮罩

我们的基本思路是需要在每个点的周围进行大量采样以获得散景效果。所以,考虑到性能因素,我们需要降低材质的分辨率。默认的Bilt只是对临近像素求平均值,为了更好的效果,我们会自定义一个过滤器。

另外,我们的散景效果应该只会出现在聚焦区域外,所以本阶段生成的材质还应具有遮罩的作用。

这里除了预过滤我们的CoC材质,还要预处理屏幕源纹理颜色。

half4 FragPrefilter(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);

    #if SHADER_TARGET >= 45 && defined(PLATFORM_SUPPORT_GATHER)

    // Sample source colors
    half4 cr = GATHER_RED_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv);
    half4 cg = GATHER_GREEN_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv);
    half4 cb = GATHER_BLUE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv);

    half3 c0 = half3(cr.x, cg.x, cb.x);
    half3 c1 = half3(cr.y, cg.y, cb.y);
    half3 c2 = half3(cr.z, cg.z, cb.z);
    half3 c3 = half3(cr.w, cg.w, cb.w);

    // Sample CoCs
    half4 cocs = GATHER_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv) * 2.0 - 1.0;
    half coc0 = cocs.x;
    half coc1 = cocs.y;
    half coc2 = cocs.z;
    half coc3 = cocs.w;

    #else

    float3 duv = _SourceSize.zwz * float3(0.5, 0.5, -0.5);
    float2 uv0 = uv - duv.xy;
    float2 uv1 = uv - duv.zy;
    float2 uv2 = uv + duv.zy;
    float2 uv3 = uv + duv.xy;

    // Sample source colors
    half3 c0 = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv0).xyz;
    half3 c1 = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv1).xyz;
    half3 c2 = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv2).xyz;
    half3 c3 = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv3).xyz;

    // Sample CoCs
    half coc0 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv0).x * 2.0 - 1.0;
    half coc1 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv1).x * 2.0 - 1.0;
    half coc2 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv2).x * 2.0 - 1.0;
    half coc3 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv3).x * 2.0 - 1.0;

    #endif
  • _FullCoCTexture:弥散圆CoC材质
  • _SourceTex:屏幕源材质

我们首先收集需要进行双线性过滤的数据。这里分为了两种方式,如果平台支持Gather,直接调指令就行了,不清楚的小伙伴可以去看看GatherRed等具体解释。如果不支持Gather,那只能用土方法,直接计算出周围四个点的纹理坐标,采样获取对应的值。

那么,具体如何过滤呢?

#if COC_LUMA_WEIGHTING

    // Apply CoC and luma weights to reduce bleeding and flickering
    half w0 = abs(coc0) / (Max3(c0.x, c0.y, c0.z) + 1.0);
    half w1 = abs(coc1) / (Max3(c1.x, c1.y, c1.z) + 1.0);
    half w2 = abs(coc2) / (Max3(c2.x, c2.y, c2.z) + 1.0);
    half w3 = abs(coc3) / (Max3(c3.x, c3.y, c3.z) + 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-5);

    #else

    half3 avg = (c0 + c1 + c2 + c3) / 4.0;

    #endif

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

对于源纹理,我们可以对其进行一个简单的平均。但在面对HDR颜色时,一个非!常明亮的样本可以大幅度地改变结果,并在移动过程中突然出现或消失,从而导致闪烁(flickering)。借用HDR中的一个图,闪烁的效果如下:
URP Bokeh DOF 分析

为了更好的效果,我们需要基于亮度进行一个加权平均。根据大佬在Tone mapping中给出的权重公式:
URP Bokeh DOF 分析
其中亮度URP Bokeh DOF 分析由RGB通道中的最大值代替。

不过,代码中的权重还乘上了一个URP Bokeh DOF 分析,这是为了降低在聚焦区域中的颜色权重,毕竟散景效果在对焦距离附近最弱。

对于弥散圆大小,我们直接取四个像素中最大的URP Bokeh DOF 分析值,并乘上散景半径URP Bokeh DOF 分析,转换为真正的距离值。

这个散景半径由以下函数计算:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static float GetMaxBokehRadiusInPixels(float viewportHeight)
{ 
    // Estimate the maximum radius of bokeh (empirically derived from the ring count)
    const float kRadiusInPixels = 14f;
    return Mathf.Min(0.05f, kRadiusInPixels / viewportHeight);
}

可以看出来当前版本,散景半径完全由经验得出,并没有给我们提供手动调节它的机会OrZ…

在这个阶段的最后,代码还进行了预乘URP Bokeh DOF 分析

	// Premultiply CoC
	// 在_SourceSize.w * 2.0范围内,散景效果会减弱
	avg *= smoothstep(0, _SourceSize.w * 2.0, abs(coc));

#if defined(UNITY_COLORSPACE_GAMMA)
	avg = SRGBToLinear(avg);
#endif

	return half4(avg, coc);
  • _SourceSize:源纹理材质大小(width, height, 1.0f / width, 1.0f / height)

如果你明白Premultiply Alpha的作用,那这里的效果是一样的(在后面的步骤中还会出现)。不明白的小伙伴推荐阅读:

简单地说,由于双线性插值,原本Alpha(此处为CoC)为0的地方,现在的插值结果可能大于0。那么我们期望此处是一个完全不可见的地方,现在却会显示其他颜色。如果这个颜色不是自己所期望的,效果就会打一些折扣(Bleeding artifacts)。我们可以通过预乘来解决这件事。不过,预乘会使原本相近的颜色变得更为相近,存在精度问题,所以本阶段材质已经改为了GraphicsFormat.R16G16B16A16_SFloat格式,单通道用16位去储存它。

最后生成的遮罩效果如下,对焦区域的散景效果会减弱。
URP Bokeh DOF 分析

3.3 制造散景

3.3.1 生成采样点

在Unity的早期版本中,使用的是一个泊松圆盘分布的固定内核DiskKernels

// rings = 2
// points per ring = 5
static const int kSampleCount = 16;
static const float2 kDiskKernel[kSampleCount] = {
    float2(0,0),
    float2(0.54545456,0),
    float2(0.16855472,0.5187581),
    float2(-0.44128203,0.3206101),
    float2(-0.44128197,-0.3206102),
    float2(0.1685548,-0.5187581),
    float2(1,0),
    float2(0.809017,0.58778524),
    float2(0.30901697,0.95105654),
    float2(-0.30901703,0.9510565),
    float2(-0.80901706,0.5877852),
    float2(-1,0),
    float2(-0.80901694,-0.58778536),
    float2(-0.30901664,-0.9510566),
    float2(0.30901712,-0.9510565),
    float2(0.80901694,-0.5877853),
};

但由于光圈叶片数量和曲率的缘故,光圈的形状并不一定是圆形的。
URP Bokeh DOF 分析

考虑到这几个因素,我们需要一种圆到正多变形的映射方式,这种映射方式能在正多边形上生成均匀样本。

代码采用了同心(Concentric)映射的思路。我们以一个单位圆和一个内接正多边形为例:
URP Bokeh DOF 分析

内接正多变形的边数为URP Bokeh DOF 分析,我们沿对角线将正多边形分为了URP Bokeh DOF 分析份。我们先看第一份:
URP Bokeh DOF 分析

我们要把圆弧上的的URP Bokeh DOF 分析点映射到垂直方向上的点URP Bokeh DOF 分析。这很简单利用相似三角形就行了。
URP Bokeh DOF 分析
其中
URP Bokeh DOF 分析
我们设正多边形的大小为 URP Bokeh DOF 分析,那么当 URP Bokeh DOF 分析 时,映射可以用极坐标方程表示为:
URP Bokeh DOF 分析
我们推广到所有范围,只需要对分母的角度做一些调整。首先,得知道当前角度在哪份中:
URP Bokeh DOF 分析
然后,我们可以将当前角度换算到第1份的情况:
URP Bokeh DOF 分析
由于,我们的叶片不一定都是直的,所以还要在此基础之上做个乘方,令曲率为URP Bokeh DOF 分析,完整的映射方程为:
URP Bokeh DOF 分析
注意:映射的公式不止这一种写法。

如果你有点迷糊,建议到Desmos自己操作一下,或则看看其他版本Regular Polygons

明白了这个公式,URP端的代码就好理解了:

void PrepareBokehKernel()
{
    const int kRings = 4;
    const int kPointsPerRing = 7;

    // Check the existing array
    if (m_BokehKernel == null)
        m_BokehKernel = new Vector4[42];
     
    // Fill in sample points (concentric circles transformed to rotated N-Gon)
    int idx = 0;
    float bladeCount = m_DepthOfField.bladeCount.value;
    float curvature = 1f - m_DepthOfField.bladeCurvature.value;
    float rotation = m_DepthOfField.bladeRotation.value * Mathf.Deg2Rad;
    const float PI = Mathf.PI;
    const float TWO_PI = Mathf.PI * 2f;

    for (int ring = 1; ring < kRings; ring++)
    {
        float bias = 1f / kPointsPerRing;
        float radius = (ring + bias) / (kRings - 1f + bias);
        int points = ring * kPointsPerRing;

        for (int point = 0; point < points; point++)
        {
            // Angle on ring
            float phi = 2f * PI * point / points;

            // Transform to rotated N-Gon
            // Adapted from "CryEngine 3 Graphics Gems" [Sousa13]
            float nt = Mathf.Cos(PI / bladeCount);
            float dt = Mathf.Cos(phi - (TWO_PI / bladeCount) * Mathf.Floor((bladeCount * phi + Mathf.PI) / TWO_PI));
            float r = radius * Mathf.Pow(nt / dt, curvature);
            // 这里是为了能够旋转正多边形
            float u = r * Mathf.Cos(phi - rotation);
            float v = r * Mathf.Sin(phi - rotation);

            m_BokehKernel[idx] = new Vector4(u, v);
            idx++;
        }
    }
}

这段代码在3个同心正多边形环上取点,一共采集得到了42个采样点。
URP Bokeh DOF 分析

3.3.2 积累散景

我们接下来要做的就是采样我们算出来的点。

half4 FragBlur(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);

    half4 samp0 = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv);

    half4 farAcc = 0.0;  // Background: far field bokeh
    half4 nearAcc = 0.0; // Foreground: near field bokeh

    // Center sample isn't in the kernel array, accumulate it separately
    Accumulate(samp0, uv, 0.0, farAcc, nearAcc);

    UNITY_LOOP
    for (int si = 0; si < SAMPLE_COUNT; si++)
    {
        float2 disp = _BokehKernel[si].xy * MaxRadius;
        Accumulate(samp0, uv, disp, farAcc, nearAcc);
    }
    // Get the weighted average
    farAcc.rgb /= farAcc.a + (farAcc.a == 0.0);     // Zero-div guard
    nearAcc.rgb /= nearAcc.a + (nearAcc.a == 0.0);
    ...

可以看到,我们先处理了中心点,再处理了周围的点。并在积累散景上进行了前后景分离,即由后景积累的散景效果保存在farAcc,前景积累的散景效果保存在nearAcc。(我们这里将对焦距离以前成为前景,以后称为后景)

在之前的步骤中,也总是出现近景和远景的概念。为什么需要前后景分离?

我们可以设想一下,如果不分离它们,后面的步骤就是根据每个点的CoC值,在原图和散景图之间进行插值,CoC越小的越清晰。听上去似乎很完美,但实际效果:
URP Bokeh DOF 分析

在聚焦位置的前方如果有未聚焦的前景时,以我们上述的插值方式,就会擦除前景制造的散景与模糊效果。例如上图圈出来的部分,这张图也许看不出来,圈出的部分实际上是有缝隙可以看到对焦区域的,这就导致缝隙处是原图的颜色,完全消除了散景与模糊效果,就显得特别突兀。

要解决这个问题,就必须换一种插值方式。通过CoC插值后景没毛病,关键问题是前景。我们希望有一个后景插值器,还有个前景插值器,先插值后景,再插值前景。

好了,思路是这样,我么继续康康Accumulate函数。

void Accumulate(float4 samp0, float2 uv, float2 disp, inout half4 farAcc, inout half4 nearAcc)
{
    float dist = length(disp);

    float2 duv = float2(disp.x * RcpAspect, disp.y);
    half4 samp = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv + duv);

    // Compare CoC of the current sample and the center sample and select smaller one
    half farCoC = max(min(samp0.a, samp.a), 0.0);

    // Compare the CoC to the sample distance & add a small margin to smooth out
    const half margin = _SourceSize.w * _DownSampleScaleFactor.w * 2.0;
    half farWeight = saturate((farCoC - dist + margin) / margin);
    half nearWeight = 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
    nearWeight *= step(_SourceSize.w * _DownSampleScaleFactor.w, -samp.a);

    // Accumulation
    farAcc += half4(samp.rgb, 1.0) * farWeight;
    nearAcc += half4(samp.rgb, 1.0) * nearWeight;
}

首先,这句代码很关键:

// Compare CoC of the current sample and the center sample and select smaller one
half farCoC = max(min(samp0.a, samp.a), 0.0);

它保证了中心点samp0和采样点sampCoC必须同时为正值时(意味着它们都是后景中的点),farCoC的值才会有效。这就有效地分离出了完全由后景产生的散景效果。emmm,唯一有点遗憾的是处理对象由samp变为了它和中心点的CoC最小值。

// Compare the CoC to the sample distance & add a small margin to smooth out
const half margin = _SourceSize.w * _DownSampleScaleFactor.w * 2.0;
half farWeight = saturate((farCoC - dist + margin) / margin);
half nearWeight = saturate((-samp.a - dist + margin) / margin);

分别计算远景和近景的权重,远景使用刚刚获得的farCoC,近景直接使用-samp.a就行(如果samp.a是远景正值,计算获得nearWeight也会几乎为0)。原本这里应该是比较CoC半径大小是否覆盖到采样点的距离dist,如果覆盖,则算有贡献。但此处为了平滑过渡,添加了一个小值margin去平滑这个权重。

// Cut influence from focused areas because they're darkened by CoC premultiplying. This is only
// needed for near field
nearWeight *= step(_SourceSize.w * _DownSampleScaleFactor.w, -samp.a);

别忘了,我们samp.rgb是经过CoC预乘的,在聚焦区域内是变暗的。所以此处我们得把聚焦区域的权重给降下来。emmm,说实话这里,我还是不太理解,按理说确认聚焦范围应该和前文相同,即_SourceSize.w * 2.0,它应该和降采样倍数无关,有无懂的好兄弟告知一下(而且如果将降采样手动调为1,极端情况下还能隐隐看到黑线…)。

Accumulate说完,我们回到片元着色器的剩余部分:

// Normalize the total of the weights for the near field
nearAcc.a *= PI / (SAMPLE_COUNT + 1);

// Alpha premultiplying
half alpha = saturate(nearAcc.a);
half3 rgb = lerp(farAcc.rgb, nearAcc.rgb, alpha);

return half4(rgb, alpha);

这里将nearAcc.a归一化,当作前景插值器传入了alpha通道(乘以PI大概是因为归一化后太弱了,需要提升一下前景效果)。 Alpha premultiplying之前说过了,此处同理。

3.4 制造模糊

到此为止,生成的散景细节还是比较多的。
URP Bokeh DOF 分析

为了更好的观感,我们需要有一个模糊操作,让散景更好地融入图像。

half4 FragPostBlur(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);

    // 9-tap tent filter with 4 bilinear samples
    float4 duv = _SourceSize.zwzw * _DownSampleScaleFactor.zwzw * float4(0.5, 0.5, -0.5, 0);
    half4 acc;
    acc  = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv - duv.xy);
    acc += SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv - duv.zy);
    acc += SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv + duv.zy);
    acc += SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv + duv.xy);
    return acc * 0.25;
}

代码这里做了一个3×3的Tent Blur,没啥好说的(emmm,感觉作用不大Orz)。

3.5 合并图像

最后一步,我们要将散景材质和源材质插值合并。

half4 FragComposite(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);

    half4 dof = SAMPLE_TEXTURE2D_X(_DofTexture, sampler_LinearClamp, uv);
    half coc = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv).r;
    coc = (coc - 0.5) * 2.0 * MaxRadius;

    // Convert CoC to far field alpha value
    float ffa = smoothstep(_SourceSize.w * 2.0, _SourceSize.w * 4.0, coc);

    half4 color = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv);

    #if defined(UNITY_COLORSPACE_GAMMA)
    color = SRGBToLinear(color);
    #endif

    half alpha = Max3(dof.r, dof.g, dof.b);
    color = lerp(color, half4(dof.rgb, alpha), ffa + dof.a - ffa * dof.a);

    #if defined(UNITY_COLORSPACE_GAMMA)
    color = LinearToSRGB(color);
    #endif

    return color;
}

上面的关键代码实际上只有一句:

color = lerp(color, half4(dof.rgb, alpha), ffa + dof.a - ffa * dof.a);

其中ffa是后景插值,dof.a是前景插值。我们按照之前说的先插值后景,再插值前景。令源颜色为URP Bokeh DOF 分析,散景颜色为URP Bokeh DOF 分析,后景插值为URP Bokeh DOF 分析,前景插值为URP Bokeh DOF 分析
URP Bokeh DOF 分析
对应到代码这个插值就是ffa + dof.a - ffa * dof.a
URP Bokeh DOF 分析

4 参考文献

  • Potmesil M., Chakravarty I. “Synthetic Image Generation with a Lens and Aperture Camera Model”, 1981

  • Shirley P., Chiu K., “A Low Distortion Map Between Disk and Square”, 1997

  • Gotanda Y. ”Star Ocean 4: Flexible Shader Management and Post Processing”, 2009

  • Sousa T. ”CryENGINE 3 Graphics Gems”, 2013

  • “Next Generation Post Processing in Call Of Duty Advanced Warfare”, 2014

  • depth-of-field

  • Scheuermann T., Tatarchuk N. “Improved Depth of Field Rendering”, Shader X3, 2005

水平有限,如有错误,请多包涵 (〃‘▽’〃)

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2022年6月13日
下一篇 2022年6月13日

相关推荐