URP Bokeh DOF 分析

“眼中只有你!”

文章目录

  • URP Bokeh DOF 分析
  • 1 景深与散景
  • 2 基本方向
  • 3 实现细节
    • 3.1 弥散圆的计算
    • 3.2 降采样与创建遮罩
    • 3.3 制造散景
      • 3.3.1 生成采样点
      • 3.3.2 积累散景
    • 3.4 制造模糊
    • 3.5 合并图像
  • 4 参考文献

阅读注意:

  • 本文的URP 版本为 10.8.1

1 景深与散景

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

我们以下面的简化模型为例:

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

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

在景深范围外,图像呈现模糊状态。

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

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

2 基本方向

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

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

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

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

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

3 实现细节

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

其中:

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

3.1 弥散圆的计算

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

其中物距PPP是我们的对焦距离(Focus Distance),它对应的像距是VpV_pVp​,完美地聚焦在像平面上;物距ZZZ对应像距VzV_zVz​,在像平面上形成一个弥散圆,弥散圆的直径为CCC。FFF为焦距,AAA是光圈直径。它们之间的关系有:

凸透镜成像公式:
1Z+1Vz=1F1P+1Vp=1F\frac{1}{Z} + \frac{1}{V_z} = \frac{1}{F} \\ \frac{1}{P} + \frac{1}{V_p} = \frac{1}{F} Z1​+Vz​1​=F1​P1​+Vp​1​=F1​
光圈直径AAA、焦距FFF与f-stop之间的关系:
A=FApertureA=\frac{F}{Aperture} A=ApertureF​
图中右侧存在明显的相似三角形(我们把ZZZ看做变量,他可能比PPP大,也可能比PPP小):
CA=∣Vp−VzVz∣\frac{C}{A} = \vert\frac{V_p-V_z}{V_z}\vert AC​=∣Vz​Vp​−Vz​​∣
然后我们根据透镜公式进行推导:
Vz=FZZ−FVp=FPP−FV_z=\frac{FZ}{Z-F} \quad\quad\quad V_p=\frac{FP}{P-F} Vz​=Z−FFZ​Vp​=P−FFP​
那么,我们就可以计算弥散圆直径:
C=A∗∣Vp−VzVz∣=A∗∣VpVz−1∣=A∗∣P(Z−F)Z(P−F)−1∣=A∗FP−F∗∣1−PZ∣\begin{aligned} C=&A*\vert \frac{V_p-V_z}{V_z} \vert \\ =&A* \vert \frac{V_p}{V_z} - 1 \vert \\ =&A* \vert \frac{P(Z-F)}{Z(P-F)} - 1 \vert \\ =&\frac{A*F}{P-F}* \vert 1- \frac{P}{Z} \vert \\ \end{aligned} C====​A∗∣Vz​Vp​−Vz​​∣A∗∣Vz​Vp​​−1∣A∗∣Z(P−F)P(Z−F)​−1∣P−FA∗F​∗∣1−ZP​∣​
事实上,我们将结果的左半边看作最大弥散圆直径MaxCoCMaxCoCMaxCoC。
MaxCoC=A∗FP−FMaxCoC = \frac{A*F}{P-F} MaxCoC=P−FA∗F​
这个MaxCoCMaxCoCMaxCoC实际上就是平行光线入射产生的最大弥散圆:

这让我们能够提前计算一部分,事实上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);   // 近景CoChalf farCoC = saturate(coc);         // 远景CoCreturn 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 colorshalf4 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 CoCshalf4 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;#elsefloat3 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 colorshalf3 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 CoCshalf 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 flickeringhalf 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 sampleshalf3 avg = c0 * w0 + c1 * w1 + c2 * w2 + c3 * w3;avg /= max(w0 + w1 + w2 + w3, 1e-5);#elsehalf3 avg = (c0 + c1 + c2 + c3) / 4.0;#endif// Select the largest CoC valuehalf 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中的一个图,闪烁的效果如下:

为了更好的效果,我们需要基于亮度进行一个加权平均。根据大佬在Tone mapping中给出的权重公式:
weight=11+lumaweight=\frac{1}{1+luma} weight=1+luma1​
其中亮度lumalumaluma由RGB通道中的最大值代替。

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

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

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

[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…

在这个阶段的最后,代码还进行了预乘CoCCoCCoC。

 // Premultiply CoC// 在_SourceSize.w * 2.0范围内,散景效果会减弱avg *= smoothstep(0, _SourceSize.w * 2.0, abs(coc));#if defined(UNITY_COLORSPACE_GAMMA)avg = SRGBToLinear(avg);
#endifreturn half4(avg, coc);
  • _SourceSize:源纹理材质大小(width, height, 1.0f / width, 1.0f / height)

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

  • Beware of Transparent Pixels
  • 关于理解 Premultiplied Alpha 的一些 Tips

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

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

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),
};

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

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

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

内接正多变形的边数为n=6n=6n=6,我们沿对角线将正多边形分为了nnn份。我们先看第一份:

我们要把圆弧上的的DDD点映射到垂直方向上的点CCC。这很简单利用相似三角形就行了。
OC=OD∗OAOBOC=\frac{OD*OA}{OB} OC=OBOD∗OA​
其中
OD=1OB=cos⁡θOA=cos⁡α且α=πnOD=1 \quad\quad OB=\cos\theta \quad\quad OA=\cos\alpha \quad 且\quad \alpha=\frac{\pi}{n} OD=1OB=cosθOA=cosα且α=nπ​
我们设正多边形的大小为 rrr,那么当 −α≤θ≤α-\alpha\leq\theta\leq\alpha−α≤θ≤α 时,映射可以用极坐标方程表示为:
r1=r∗cos⁡πncos⁡θr_1=r*\frac{\cos\frac{\pi}{n}}{\cos\theta} r1​=r∗cosθcosnπ​​
我们推广到所有范围,只需要对分母的角度做一些调整。首先,得知道当前角度在哪份中:
m=floor(nθ+π2π)m=floor(\frac{n\theta+\pi}{2\pi}) m=floor(2πnθ+π​)
然后,我们可以将当前角度换算到第1份的情况:
θm=θ−2πnm\theta_m=\theta-\frac{2\pi}{n}m θm​=θ−n2π​m
由于,我们的叶片不一定都是直的,所以还要在此基础之上做个乘方,令曲率为ccc,完整的映射方程为:
rngon=r∗(cos⁡πncos⁡(θ−2πnfloor(nθ+π2π)))cr_{ngon}=r*(\frac{\cos{\frac{\pi}{n}}} {\cos(\theta-\frac{2\pi}{n}floor(\frac{n\theta+\pi}{2\pi}))})^c rngon​=r∗(cos(θ−n2π​floor(2πnθ+π​))cosnπ​​)c
注意:映射的公式不止这一种写法。

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

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

void PrepareBokehKernel()
{const int kRings = 4;const int kPointsPerRing = 7;// Check the existing arrayif (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 ringfloat 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个采样点。

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 bokehhalf4 nearAcc = 0.0; // Foreground: near field bokeh// Center sample isn't in the kernel array, accumulate it separatelyAccumulate(samp0, uv, 0.0, farAcc, nearAcc);UNITY_LOOPfor (int si = 0; si < SAMPLE_COUNT; si++){float2 disp = _BokehKernel[si].xy * MaxRadius;Accumulate(samp0, uv, disp, farAcc, nearAcc);}// Get the weighted averagefarAcc.rgb /= farAcc.a + (farAcc.a == 0.0);     // Zero-div guardnearAcc.rgb /= nearAcc.a + (nearAcc.a == 0.0);...

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

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

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

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

要解决这个问题,就必须换一种插值方式。通过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 onehalf farCoC = max(min(samp0.a, samp.a), 0.0);// Compare the CoC to the sample distance & add a small margin to smooth outconst 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 fieldnearWeight *= step(_SourceSize.w * _DownSampleScaleFactor.w, -samp.a);// AccumulationfarAcc += 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 制造模糊

到此为止,生成的散景细节还是比较多的。

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

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 samplesfloat4 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;
}

代码这里做了一个3x3的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 valuefloat 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);#endifhalf 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);#endifreturn color;
}

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

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

其中ffa是后景插值,dof.a是前景插值。我们按照之前说的先插值后景,再插值前景。令源颜色为aaa,散景颜色为bbb,后景插值为xxx,前景插值为yyy。
Cbackground=a+(b−a)xCforeground=Cbackground+(b−Cbackground)y=a+(b−a)x+(b−a−(b−a)x)y=a+(b−a)x+(b−a)y−(b−a)xy=a+(b−a)(x+y−xy)\begin{aligned} C_{background}&=a+(b-a)x \\ C_{foreground}&=C_{background}+(b-C_{background})y\\ &=a+(b-a)x+(b-a-(b-a)x)y\\ &=a+(b-a)x+(b-a)y-(b-a)xy\\ &=a+(b-a)(x+y-xy) \end{aligned} Cbackground​Cforeground​​=a+(b−a)x=Cbackground​+(b−Cbackground​)y=a+(b−a)x+(b−a−(b−a)x)y=a+(b−a)x+(b−a)y−(b−a)xy=a+(b−a)(x+y−xy)​
对应到代码这个插值就是ffa + dof.a - ffa * dof.a

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


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

URP Bokeh DOF 分析相关推荐

  1. 《塞尔达传说:旷野之息》技术分析:神作是怎么炼成的

    我其实很久以前就想做一篇正式的<塞尔达传说:旷野之息>引擎分析文章了,但一直没时间去弄.然而,现在Switch有了新的视频录制功能后,我想这是一个完美的时机来重游这款游戏并通过我上传到Tw ...

  2. 《塞尔达传说:旷野之息》技术分析:游戏神作是怎么炼成的

    我其实很久以前就想做一篇正式的<塞尔达传说:旷野之息>引擎分析文章了,但一直没时间去弄.然而,现在Switch有了新的视频录制功能后,我想这是一个完美的时机来重游这款游戏并通过我上传到Tw ...

  3. 项目07城市餐饮店铺选址分析

    问题 [项目07] 城市餐饮店铺选址分析 1.从三个维度"口味"."人均消费"."性价比"对不同菜系进行比较,并筛选出可开店铺的餐饮类型 要 ...

  4. 在URP中正确写入Sprite深度以使用DOF

    我准备在文字冒险中加入景深的效果,但是因为unity内置shader的缘故,sprite是不会写入深度的,而如果要使用PostProcessing的DOF效果的话,是必须要将半透明物体的深度正确写入D ...

  5. 深入URP之Shader篇5: SimpleLit Shader分析(1)

    SimpleLit.shader 本篇开始分析simple lit shader.我们通过分析unlit shader了解了URP shader的结构,以及一些基础功能.而simple lit sha ...

  6. URP的工作流程源码分析

    URP Analysis URP是unity推出的,用于替代传统build-in管线.该篇为阅读的笔记~ URP的入口文件为UniversalRenderPiplineAsset.cs,在该文件中,进 ...

  7. 深入URP之Shader篇3: Unlit Shader分析[下]

    Unlit shader 上篇中我们分析了Unlit shader的Properties在ShaderGUI中的处理,接下来看Sub Shader. SubShader unlit shader以及其 ...

  8. 深入URP之Shader篇8: SimpleLit Shader分析(4)

    Simple Lit Forward Pass 本篇继续 Fragment shader 函数 InitializeInputData InputData inputData; InitializeI ...

  9. 光流分析 Optical Flow Lucas-Kanade 算法 DOF Dense Optical Flow

    光流跟踪算法对车位进行跟踪 概念 光流是空间运动物体在观察成像平面上的像素运动的瞬时速度,是利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧跟当前帧之间存在的对应关系,从而计算出相 ...

最新文章

  1. CString的用法
  2. FPGA设计心得(9)基于DDS IP核的任意波形发生器设计
  3. Kali Linux NetHunter教程Kali NetHunter支持的设备和ROMs
  4. fileinputstream自定义类序列化和反序列化_Rest Assured篇:Java中的序列化和反序列化...
  5. hdoj1242(dfs 剪枝 解法)
  6. xfce不小心禁用了鼠标
  7. 计算机加入域 不能访问网络位置 解决办法
  8. 朝鲜时蔬(分数据点写算法+毒瘤数学)
  9. MySQL CookBook 学习笔记-01
  10. Java 类型, Hibernate 映射类型及 SQL 类型之间的相应关系
  11. goldengate for mysql_GoldenGate for mysql to mysql:单向同步
  12. mysql为何500w拆表_【mysql】MySQL 单表500W+数据,查询超时,如何优化呢?
  13. windows10录制视频 电脑屏幕录像
  14. 周日历插件weeklyCalendar,可添加日历提醒
  15. 【C 语言】文件操作 ( 使用 fseek 函数生成指定大小文件 | 偏移量 文件字节数 - 1 )
  16. dropbox访问_使用PHP访问Dropbox
  17. 手把手教你如何巧用Github的Action功能
  18. jdbc,基本数据库命令封装
  19. 在校大学生学业预警系统java_学业预警系统
  20. Matlab实现Galton板的动画演示

热门文章

  1. Python技能树及Markdown编辑器的测评
  2. GO语言开山篇(二):诞生小故事
  3. 【Benewake(北醒) 】短距 TFmini Plus 12m介绍以及资料整理
  4. React + TypeScript实战(二)hooks用法
  5. {:query, :rabbit@centos7, {:badrpc, :timeout}} 快速解决
  6. JavaFX最小化和通过动画最大化未装饰的舞台
  7. 50 个杀手级人工智能项目
  8. 南加大和纽约大学计算机专业,NYU,南加大RD狂放榜?科比女儿喜提Offer大满贯!...
  9. 利用Cesium加载 M3D BIM 模型
  10. 云计算基础2-什么是云存储?