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

我准备在文字冒险中加入景深的效果,但是因为unity内置shader的缘故,sprite是不会写入深度的,而如果要使用PostProcessing的DOF效果的话,是必须要将半透明物体的深度正确写入Depth Buffer的,这篇消耗了我大量精力的文章就是要介绍正确写入的方法。

其实,如果只是要在2D游戏中实现类似于景深的效果,不写入深度其实也是可以的,我找到的有两种方案:一种是实时对Sprite进行模糊计算;二是直接使用离线预模糊的图片,根据强度进行混合。

离线预模糊

虽然这两种方法都可以比较简单的实现,但是都有缺点:实时模糊消耗计算量太大,Sprite太多很影响性能,且相比后处理的降采样模糊,效果比较差;离线模糊会增加内存的占用,并且一张图可能需要多张模糊贴图。但是最致命的缺点是你需要手动控制每个Sprite的模糊程度,很难操作,并且DOF的模糊效果其实是“散景(Bokeh)”,并不是简单的模糊,效果上是有差别的。

离线预模糊的介绍:

Achieving Optimized Depth-Of-Field effect in a 2D Unity Game – Moo Lander​moolander.com/achieving-optimized-depth-of-field-effect-in-a-2d-unity-game/https://link.zhihu.com/?target=https%3A//moolander.com/achieving-optimized-depth-of-field-effect-in-a-2d-unity-game/

相比于之前的方法,老老实实写入深度应该才是比较通用的流程,可以直接在3D游戏中使用,且可以直接利用已有的后处理效果。

经过半个月的尝试,试验了很多方法,最终得到了如下的深度图和DOF效果图,我将先简单介绍我尝试过的几种方法,最后介绍最终实现的方法。


 1、试验方法一:直接修改为“ZWrite ON”

将内置的Sprite的shader中的标签:“ZWrite Off”修改为“ZWrite ON”,这种方法看起来是没有一点问题的,在网上搜索到的方法也大多是这种方法,但是这种方法写入的深度是整个Sprite的Mesh,也就是一整块,并不是根据图片的alpha进行写入,这就会导致DOF效果出现错误,所以pass了。

但是真正卡住我的是:虽然Sprite把Mesh的深度写入了,但是PostProcessing使用的”_CameraDepthTexture“并没有获得正确的深度,只包含了不透明物体的深度,在FrameDebgger中也会看到”CopyDepth“的pass在”DrawOpaqueObjects“的pass后就立刻执行了,所以即使前面写入了深度,但在此之前就已经把深度图拷贝出来了,相当于白写了,在我查看了urp的代码后才发现,它会自动计算拷贝深度图的时机:

UniversalRenderer.cs

可是我在这个文件中没有找到应该如何修改拷贝深度图的时机,所以我就尝试在urp的源代码中搜索,发现需要使用ConfigureInput函数标记pass需要使用的贴图,urp就会根据全部pass的需求,自动计算拷贝深度图的最早时机。所以增加一个空pass把拷贝内存的时机向后推就可以使“_CameraDepthTexture”在正确的时机拷贝。

DecalPreviewPass.cs

 ScriptableRenderPass.cs


2、试验方法二:“ZWrite On”结合“Mesh Type”

在图片导入时,是可以使用Mesh Type选项选择Mesh贴合不透明区域的大小的,想情况下,似乎是可以得到一个完美贴合边缘的mesh的,这样结合“ZWrite On”就可以得到正确深度了。但是实际并不会的到完美贴合边缘的Mesh,总会差一点,这差一点的多边形在DOF中很影响效果,所以pass了。


  3、试验方法三:多通道写入深度

有一种通过多个通道画半透明3D物体的方法,在urp中是通过制定pass的“LightMode”关键字为“SRPDefaultUnlit”实现的,但是这种方法会打断SRP合批,并且仍然存在边缘不贴合的问题,非常难看,使用clip函数也只能丢弃掉颜色,而不能丢弃掉深度,所以这种方法也pass掉了。

 真是僵硬


最终使用的方案的原理其实非常的简单,就是根据图片的alpha值来决定是否写入深度,具体分为3个步骤:

(1)拷贝原深度图,以供比较深度。

(2)将每一个Sprite绘制在深度图上,只有离相机更近的且alpha大于0的才可以写入。

(3)将修改过的深度图拷贝回原深度图。

其中的难点是如何计算与写入深度,在看urp的手册的时候,找到有一个官方案例中使用了一个“ComputeNormalizedDeviceCoordinatesWithZ()”函数,在“Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl”文件中可以找到:

 

使用这个函数,并传入裁剪坐标系下的坐标,就可以得到NDC下的坐标了,其中的z分量就是深度值,直接写入就可以了。而写入的方法我是用的是直接将片段着色器输出的值直接绑定给“SV_Depth”,但是由于另开的一个pass中的SpriteRenderer是无序的,并不是从远向近排序的,所以计算出的深度远的地方应该输出原深度,所以我们应该事先拷贝一份出来以供其采样使用;alpha为0的像素的处理方法也是同样。这样就可以输出完全贴合的深度了:

但是实际使用中,我们经常会见到一些自带透视效果的图片素材,例如这张屋岛作战中的初号机:

 显然在素材制作时,初号机和阳电子炮是一个整体的,如果直接根据Sprite的z坐标来计算将会使最近端的炮口和最远端的初号机都在同一个深度,这回造成明显的视觉错误,应该再给它指定一张深度偏移图来让一张Sprite的各个像素可以有不同深度。

我们仅仅需要一张黑白图就可以了,在计算深度的过程中,加上从这张图中采样到的偏移量就可以了,非常简单,效果如下:

 深度偏移图

 对焦至前景炮口

还存在有一个问题,当一个Sprite逐渐透明时,它的深度值应该是逐渐靠近后边的物体的深度的,在实现上是很简单的,只要把算出来的深度乘上alpha值就可以了,因为前边已经判断过深度是否应该写入,所以也非常简单:

 可以看到右一男消隐后深度仍是合理的,不会使后面的人的画面表现错误。但是写shader的时候应该注意由于平台关系,ndc空间是不同的,dx还是反z存储的,所以不要写错了。


总结一下:

思想比较简单

需要搜索尝试的东西比较多

平台相关性比较强

感觉也不是特别难,但是就是找不到合适的解决方法,真的奇怪,外网内网都没有

 默默看着你。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值