前言
紧接上一篇文章,简单在URP实现以下屏幕空间平面反射,同样会和上篇一样着重于原理以及基础实现,不会涉及更加深层次的东西比如多平台优化(如果想学习更加深入的,可以移步Colin的ColinLeung-NiloCat/UnityURP-MobileScreenSpacePlanarReflection: Reusable RendererFeature of MobileScreenSpacePlanarReflection (github.com) 或者Unity URP 移动平台的屏幕空间平面反射(SSPR)趟坑记 - 知乎 (zhihu.com)),当然,后续如果有空了可能会把这部分学完了作为(2)发布。当然为SSPR会涉及到computeshader,是一个不错的入门computeshader的切入点。
关于ScreenSpacePlanarReflection(SSPR)
没怎么仔细考究到底是哪家先提出的概念,但是最有名的估计还属Ubisoft在ghost-recon-wildlands中使用的SSPR。Ubisoft的图形程序remi-genin在自己的博客也发布了他们的做法(Screen Space Planar Reflections in Ghost Recon Wildlands – Rémi Génin (remi-genin.fr))。相比SSR,SSPR免去了Rymarching的高消耗,性能大幅提升,甚至可以适用于移动端,相比平面反射也不存在drawcall翻倍的情况,当然没有免费的午餐,和SSR一样无法映射屏幕外的物体,而且会由于透视的问题导致gaps的出现需要自己处理拉伸
[^]: 图源:Screen Space Planar Reflections in Ghost Recon Wildlands – Rémi Génin (remi-genin.fr)
同时也会存在许多的问题需要补救。
ScreenSpacePlanarReflection原理
类似SSR,我们需要首先通过深度图重建世界坐标,然后通过预定的高度进行坐标翻转,再用翻转后的屏幕坐标会对应翻转前的屏幕颜色。相比SSR会比较好理解。
关于ComputeShader
由于实现SSR会需要用到ComputeShader,也许也有许多和笔者一样没怎么接触过的,这里可以稍微对computeshader做一些前置理解。
首先是为什么SSPR需要computeshader
因为sspr需要把一个坐标的颜色记录到另一个坐标,传统的shader在fragment阶段都是对对应的fragment处理,而computeshader可以做到这种操作(乱序写入)
computeshader的优点
CPU和GPU最大的区别在于GPU具有高并行结构(highly parallel stucture),GPU 拥有更多的 ALU (Arithmetic Logic Unit,逻辑运算单元)用于数据处理,这样的结构适合对密集型数据进行并行处理。
computeshader能充分利用GPU的数据处理能力,把大量重复的计算放在GPU端处理(瓶颈在于需要CPU传输数据到GPU)。
computeshader的名词解释
(以下摘自 URP管线的自学HLSL之路 第二十九篇 ComputeShader释义 - 哔哩哔哩专栏 (bilibili.com),可以去原文进一步了解,这里只放我认为比较重要的)
线程(Thread):最基础工元。
线程组(ThreadGroup):多个线程的组合,一个线程组的线程是按XYZ3个维度进行排布。如[numthreads(4,8,12)]是定义x方向4个线程,y方向8个线程,z方向12个线程的一个线程组。1个线程组的大小(ThreadGroupSize)最大为1024,也就是xyz<=1024。
对于AMD的GPU平台,线程组的大小建议定义成64的倍数(WaveFront构架)。
对于Nvidia的GPU平台,线程组的大小建议定义成32的倍数(SIMD32(Warp)构架)。
调度(Dispatch):多个线程组的组合,一个Dispatch的线程组是按XYZ3个维度进行排布。如Dispatch(KernelID,3,9,12)是定义x方向3个线程组,y方向9个线程组,z方向12个线程组,调用ComputeShader里KernelID的kernel函数的一个调度。
这里对以上做一个总结,线程是员工,线程组是一个团队,而调度是公司能调动的所有团队。因此在SSPR中调度的线程数线程组内线程的数目需要满足申请的RT的大小,同时考虑到平台,这里用64作为线程组的大小,也就是[numthreads(8,8,1)] (Texture是二维的,没有z,所以z用1)
SSPR 开冲
Volume编写(其实直接放在RenderFeature里也行,写在后处理面板方便调试)
同上一篇博客,写法框架很死了。
1 | using System.Collections; |
RenderFeature编写
二话不多说,直接粘贴上一篇SSR博客的renderfeatur(经典废物利用环节),只不过这次不需要material去Blit了,RenderFeature只负责调度ComputeShader,首先自然是需要处理RT申请,这里有一个注意事项
1 | descriptor = new RenderTextureDescriptor(Mathf.CeilToInt(sspr.RT_Size.value* aspect), Mathf.CeilToInt(sspr.RT_Size.value), RenderTextureFormat.ARGB32); |
就是Mathf.CeilToInt,因为Float转到Int会自动截去小数部分,这会导致RT最后的图有黑边(比如512.4会变成512,这个512-512.4的部分就莫得了)
在Execute()方法内传递ComputeShader需要的属性
1 | //sspr |
重头戏ComputeShader编写
上次写深度重建用的在知乎偷来的方法,前几天VScode瞎搜发现unity在common里有另一种做法,并且封装好了,就是ComputeWorldSpacePosition,在Common.hlsl中可以看到。这里我们用的colin大佬的做法去解决排序问题,通过写入世界坐标的Y值来做比较解决这个问题
[^]: 深度排序问题的原因是经过投影可能不同高度对应同一像素,我们使用最小的Y,因为在 “看的到” 的物体里,不存在Y小还被挡住的情况
1 |
|
SSPR额外
经过上述内容,基本的SSPR算法已经阐述过一边,简单在URP上做了一个幼儿园入门版的,有许多地方需要解决:比如由于CS乱序写入的闪面,相近像素对应到同像素导致的黑洞问题(移步colin大神的github,CS里加一个补洞的pass,或者简单粗暴点按照像素灰度确定是否是洞做补洞)等等。
另外如果对SSPR感兴趣可以去看Screen Space Planar Reflections in Ghost Recon Wildlands – Rémi Génin (remi-genin.fr)原文,其中也提到了Ubisoft对于不同水平面的处理,以及拉伸的处理。
2017年的SIGGRAPH上也有一篇平面反射的优化论文,Prezentacja programu PowerPoint (realtimerendering.com)
如果学有余力可以去深挖,笔者写本文更多在于常规做法学习以及computeshader练手,并未涉及进阶部分以及优化(问就是不会)。
以上,本篇完。去通GhostRunner了。