用Unity实现景深效果

景深也是一种非常常见的后处理手段,它用来模拟相机拍摄画面的效果。今天我们讨论如何在Unity中实现它。

简单来说,景深效果可以拆分为两个部分,一个部分是聚焦,使画面中指定的区域清晰显示,另一个部分是失焦,使画面中其他区域,即没有对焦的地方,变得模糊。

我们先从相机说起。产生模糊的主要原因是模拟的相机并不是理想的针孔相机。对于针孔相机,相机前的物体只有一条光线会被记录下来,因而图像总是清晰的,但是相应的,一条光线不够明亮,需要足够长的曝光时间来提高图像的亮度。而这个时间段内如果移动场景中的物体,就会导致比较严重的运动模糊。

而模拟的相机是通过增大光圈大小来减少曝光时间的,也就是说相机前的物体会有多条光线被记录下来。这样造成的效果就是,原本物体上的一个点,对应到成像平面是一个圆片区域。区域的大小和物体到光圈的距离,光圈到成像平面的距离有关。自然而然,最后的图像是模糊的了。

为了让光线重新聚焦,这里引入一个透镜的概念。它可以使穿过光圈的光重新汇聚到一个点上,但是只有离相机特定距离的物体才可以被重新聚焦起来。其他距离的物体,要么提前汇聚后又分散了,要么还没来得及汇聚就到达了成像平面上了。最终的结果就是我们想要的,一部分物体清晰,其他的物体模糊。

对于模糊的物体,它投影到成像平面的点变成了圆片状的区域。这种失焦的效果被称之为模糊圈(circle of confusion),如图所示:

我们首先来对这个CoC进行建模。显然,CoC的大小与物体到相机的距离有关,也就是深度d有关。我们还需要设定一个当前聚焦的距离f,和聚焦的范围r,然后有:
CoC=d−frCoC = \dfrac{d - f}{r} CoC=rd−f​

我们需要把CoC存到一个buffer中,由于CoC是一个值,那么render texture的格式指定为浮点型即可:

     RenderTexture coc = RenderTexture.GetTemporary(source.width, source.height, 0,RenderTextureFormat.RHalf, RenderTextureReadWrite.Linear);Graphics.Blit(source, coc, dofMaterial, circleOfConfusionPass);

CoC的值可正可负,不过既然render texture中存储的是浮点数了,就不需要对负值进行处理:

             half FragmentProgram (Interpolators i) : SV_Target {float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);depth = LinearEyeDepth(depth);float coc = (depth - _FocusDistance) / _FocusRange;coc = clamp(coc, -1, 1);return coc;}

由于只有一个通道,这时候buffer看起来是偏红的,越不在CoC范围内的点,颜色越红;完全处于CoC范围内的点,则颜色为黑色;还有一类的点,它们也不在CoC范围内,但是由于此时CoC的值为负数,体现在颜色上,也是黑色:

接下来,我们来考虑如何把这种模糊虚化的效果做出来。首先它应该是一种模糊,就是要采样周围若干像素作为当前像素的结果;其次它的模糊要有圆的轮廓,也就是说它的采样区域应该是一个半径为R的圆,圆形区域内的点采样的像素是相似的,进而模糊后的结果也是相似的,反映到图像上就是形成了一块圆形的模糊区域:

             half4 FragmentProgram (Interpolators i) : SV_Target {half3 color = 0;float weight = 0;for (int u = -radius; u <= radius; u++) {for (int v = -radius; v <= radius; v++) {float2 o = float2(u, v);if (length(o) <= radius) {o *= _MainTex_TexelSize.xy * sparse;color += tex2D(_MainTex, i.uv + o).rgb;weight += 1;}}}color *= 1.0 / weight;return half4(color, 1);}

这里除了模糊的采样半径以外,还增加了一个稀疏系数。这个参数是用来控制模糊的稀疏程度的。简单来说,就是这个值越大,采样的点越稀疏,也就是圆形区域内只有部分的点模糊之后的结果是相似的,反映到图像上就是一个不那么连续光滑,而是比较锐利的圆形区域了。

这样说太抽象了,让我们来看一下实际的效果,首先是固定sparse参数为1,调节radius参数从0变换到10:

再看一下固定radius参数为10,调节sparse参数从1变换到10的效果:

我们还可以调整圆形所覆盖的区域,例如它可以是个圆盘区域,我们只对圆盘区域上的点进行采样:

static const int kernelSampleCount = 16;
static const float2 kernel[kernelSampleCount] = {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),
};half4 FragmentProgram (Interpolators i) : SV_Target {half3 color = 0;for (int k = 0; k < kernelSampleCount; k++) {float2 o = kernel[k];o *= _MainTex_TexelSize.xy * radius;color += tex2D(_MainTex, i.uv + o).rgb;}color *= 1.0 / kernelSampleCount;return half4(color, 1);
}

这里用到的一堆魔数,可视化如下:

显而易见采样点连成的形状是一个圆盘。使用这套参数,我们还可以将采样半径和稀疏系数统一为一个半径参数,这是因为半径越大,圆盘区域越大,采样点之间的距离也越大,进而采样越稀疏。

来看一下不同半径下的效果:

使用圆盘区域的效果看上去更加梦幻了。不过,在采样半径变大的同时,我们能够很明显地发现采样点之间的间隙,为了消除它,我们准备在这后面再套一层模糊。这次是真的模糊,使用3x3的tent filter:

             half4 FragmentProgram (Interpolators i) : SV_Target {float4 o =_MainTex_TexelSize.xyxy * float2(-0.5, 0.5).xxyy;half4 s =tex2D(_MainTex, i.uv + o.xy) +tex2D(_MainTex, i.uv + o.zy) +tex2D(_MainTex, i.uv + o.xw) +tex2D(_MainTex, i.uv + o.zw);return s * 0.25;}

来看下模糊后的效果,这时候的模糊圈虚虚实实,叠加在了一起:

下一步,我们来考虑如何实现聚焦的效果。首先,不是所有的地方都需要虚化的,聚焦的地方应该是清晰的,这里的模糊圈应该要去掉。我们可以使用之前建模的CoC来辅助判断。CoC越小,说明当前点离聚焦范围越近,它就不可能很模糊,也就是距离它很远的采样点(例如圆盘中外围的点)在模糊过程中要被过滤掉:

             half4 FragmentProgram (Interpolators i) : SV_Target {half3 color = 0;half weight = 0;for (int k = 0; k < kernelSampleCount; k++) {float2 o = kernel[k] * _BokehRadius;half radius = length(o);o *= _MainTex_TexelSize.xy;half coc = tex2D(_CoCTex, i.uv + o).r * radius;if(abs(coc) >= radius){color += tex2D(_MainTex, i.uv + o).rgb;weight += 1;}}color *= 1.0 / weight;return half4(color, 1);}

顺便看看不同聚焦范围下的效果:

过滤的过程可以做得更加平滑,我们引入权重的概念,越处于过滤边缘的采样点所占权重越低:

             half Weigh (half coc, half radius) {return saturate((abs(coc) - radius + 2) / 2);}half4 FragmentProgram (Interpolators i) : SV_Target {half3 color = 0;half weight = 0;for (int k = 0; k < kernelSampleCount; k++) {float2 o = kernel[k] * _BokehRadius;half radius = length(o);o *= _MainTex_TexelSize.xy;half coc = tex2D(_CoCTex, i.uv + o).r * _BokehRadius;half3 rgb = tex2D(_MainTex, i.uv + o).rgb;half sw = Weigh(coc, radius);color += rgb * sw;weight += sw;}color *= 1.0 / weight;return half4(color, 1);}

来对比一下:

由于我们之前在生成模糊圈时,对整个render texture是做了模糊处理的,而聚焦的地方是不需要模糊的,需要借助最原始的render texture,做一个blend,当CoC的值越大,模糊render texture的权重越高;反之,原始render texture的权重越高:

             half4 FragmentProgram (Interpolators i) : SV_Target {half4 source = tex2D(_MainTex, i.uv);half coc = tex2D(_CoCTex, i.uv).r;half4 dof = tex2D(_DoFTex, i.uv);half dofStrength = smoothstep(0.1, 1, abs(coc));half3 color = lerp(source.rgb, dof.rgb, dofStrength);return half4(color, source.a );}

看上去效果不错,不过出现了一个问题,右下角的cube,有一个角变清晰了,显得很不自然。原因肯定是出在这个blend上了,我们在把coc代入计算时使用了绝对值,这就可能出现,本来聚焦的地方在后面,但前面有部分像素也算在了聚焦范围内了。因此,我们有必要把前景颜色和背景颜色分开计算,前景的coc是负值,而背景的coc是正值。

我们首先对模糊圈进行处理,分别计算前景和背景之后,以一个权重的方式进行融合:

             half4 FragmentProgram (Interpolators i) : SV_Target {half3 bgColor = 0, fgColor = 0;half bgWeight = 0, fgWeight = 0;for (int k = 0; k < kernelSampleCount; k++) {float2 o = kernel[k] * _BokehRadius;half radius = length(o);o *= _MainTex_TexelSize.xy;half coc = tex2D(_CoCTex, i.uv + o).r * _BokehRadius;half3 rgb = tex2D(_MainTex, i.uv + o).rgb;half bgw = Weigh(max(0, coc), radius);bgColor += rgb * bgw;bgWeight += bgw;half fgw = Weigh(-coc, radius);fgColor += rgb * fgw;fgWeight += fgw;}bgColor *= 1.0 / (bgWeight + (bgWeight == 0));fgColor *= 1.0 / (fgWeight + (fgWeight == 0));half bgfg = min(1, fgWeight * _ForegroundScale);half3 color = lerp(bgColor, fgColor, bgfg);return half4(color, bgfg);}

这里_ForegroundScale参数是用来灵活调节前景模糊程度的,考虑极端情况,如果参数的值为0,说明前景不参与模糊圈的计算,即前景也是清晰的;反正如果值为1,则前景和背景一样模糊。来看一下这个参数从0到1的变化效果:

看起来,和source render texture的融合效果还是不太对。这也难怪,毕竟我们现在还是只用的abs(coc)作为融合的权重。现在,还要把前景和背景的融合参数也考虑进来:

             half4 FragmentProgram (Interpolators i) : SV_Target {half4 source = tex2D(_MainTex, i.uv);half coc = tex2D(_CoCTex, i.uv).r;half4 dof = tex2D(_DoFTex, i.uv);half bgfg = dof.a;half dofStrength = smoothstep(0.1, 1, abs(coc));half3 color = lerp(source.rgb, dof.rgb, dofStrength + bgfg - dofStrength * bgfg);return half4(color, source.a );}

这里最终的融合参数为dofStrength + bgfg - dofStrength * bgfg,推导起来其实很简单,就是source和dof融合两次,一次使用dofStrength ,另一次使用bgfg作为参数:
c=a+(b−a)⋅xd=c+(b−c)⋅yd=a+bx−ax+(b−a−bx+ax)yd=a+(b−a)(x+y−xy)c = a + (b-a) \cdot x \\ d = c + (b-c) \cdot y \\ d = a + bx - ax + (b - a - bx + ax)y \\ d = a + (b - a)(x + y - xy) c=a+(b−a)⋅xd=c+(b−c)⋅yd=a+bx−ax+(b−a−bx+ax)yd=a+(b−a)(x+y−xy)
来看一下最终的效果,依旧是调节前景的融合系数_ForegroundScale从0到1:

看上去还不错。自此我们可以自由地调节前景的模糊程度,聚焦的距离与范围,还有模糊的半径了。最后是一张静态效果:

如果你觉得我的文章有帮助,欢迎关注我的微信公众号:我是真的想做游戏啊

Reference

[1] Depth of Field

用Unity实现景深效果相关推荐

  1. unity shader景深效果

    实现效果 景深效果 实现思路 由两张图组成,分别是远处的模糊状态和近处的清晰状态,根据物体的深度判断物体离摄像机的距离确定物体的状态.两个图进行插值,越近越靠近清晰的图像. 代码 脚本代码: usin ...

  2. Unity 屏幕特效 之 简单地使用 Shader 获取深度,实现景深效果

    Unity 屏幕特效 之 简单地使用 Shader 获取深度,实现景深效果 目录

  3. unity 景深效果

    可以在unity中window→Package Manager,找到Post Processing插件 然后通过此插件就可以调整景深效果了 注意 摄像机上需绑定 Post Process Layer ...

  4. (一)unity shader在实际项目中出现的问题————unity的后处理插件景深效果在某些低档机(如三星)无效的解决方案

    本专栏主要解决一些移动平台上unity shader效果异常的问题.很多情况下我们发现unity中的shader在PC平台效果正常,但是在移动平台上效果不对,或者部分机型效果不对的问题,尤其是低档老年 ...

  5. 景深决定照相机什么特性_手机为什么达不到相机的景深效果?

    首先你提到的镜头虚化和3D电影的双镜头是两回事. 因为3D电影,也是有全景深,和大光圈带来的虚化效果的.所以要把2D和3D放在一起讨论,全景深和浅景深放在一起讨论. 然后相机镜头的虚化效果是物理带来的 ...

  6. unity 条目换位效果_Unity AI主题博客条目

    unity 条目换位效果 Welcome to the first of Unity's new AI-themed blog entries! We have set up this space a ...

  7. Unity 实现贴花效果的制作教程

    c#教程​https://www.cnblogs.com/Yesi/p/15829200.html ​ 有些游戏中的战斗痕迹的效果会通过贴花来实现的,贴花的方式多种多样.而在Unity中,有一种给官方 ...

  8. unity气流模拟效果

    机房气流模拟效果 参考 效果展示: 资源链接:unity气流模拟效果-Unity3D文档类资源-CSDN下载

  9. Unity 翻书效果

    Unity翻书效果 目前做的VR项目中需要一个翻阅魔法书的效果,考虑过使用UnityBookPageCurl-master插件,但是那个插件是纯UI显示的,只有二维效果,在VR里观感不佳,之后在网上找 ...

最新文章

  1. 量子计算生态:市场预期、行业应用与“霸权”争夺
  2. HTML超文本描述语言,HTML超文本标记语言的介绍
  3. CloudCC: 智能CRM究竟能否成为下一个行业风口?
  4. python中类的定义方法_在Python中定义类变量的正确方法
  5. 老司机学习MyBatis之如何通过select返回Map
  6. KEIL5 编译器导致的程序异常
  7. Laravel核心解读--路由(Route)
  8. 支持vxlan的服务器网卡,3台服务器互通vxlan
  9. 开始我们的Snippets!
  10. 私藏的18个黑科技网站,想找什么软件就找什么软件!
  11. 18年12月英语六级第二套听力单词
  12. nginx静态代理设置一:静态文件在本机
  13. 运行代码时出现的问题
  14. 关于研究一个新领域,研究思路的总结
  15. 听说股票是暴利?花了一晚上时间,用Python写了个股票提醒系统
  16. Recoil - Facebook 官方 React 状态管理器
  17. Devise邮件模板路径
  18. Python - 康威生命游戏Conway's game of life
  19. python面向对象之抽象类
  20. 基于自然语言处理的垃圾信息过滤方法

热门文章

  1. ZUCC数据库原理作业5
  2. 第十二周 任务二
  3. 查看qq空间说说及评论,设置相关表结构
  4. matlab建模DNA双链,PPT绘制科研图形—DNA双链、分子细胞模型
  5. hadoop实战(二)
  6. java开发:mysql
  7. Invalid bound statement (not found): cn.jeefast.xiangmu.dao.AchDao.selectByI 解决
  8. 找工作的程序员应该这样优化简历【内附120套优质简历模板】
  9. 程序员养娃记:撸一手好代码,却带不好一个娃?!
  10. 爬虫实战——爬取小说《从你的全世界路过》