一、如何获得颜色缓冲
网上搜索Unity的后处理或者获得屏幕缓冲,大部分会提到用grabpass到一张指定纹理上或者写一个后处理脚本挂在摄像机上。但是这种方式在Urp管线下已经不生效了。urp取消了默认管线抓取颜色缓冲的grabpass,同时也取消了MonoBehaviour.OnRenderImage,需要使用ScriptableRenderPass 来完成类似的功能。ScriptableRenderPass是urp中的pass基类,urp预定义的pass都继承自该类,我们自定义的pass也需要继承自该类。
1.1 Urp的渲染顺序
urp中通过类型RenderPassEvent定义了一些列pass的渲染顺序或者说时机,大致的顺序是ShadowPass->PrePass(Depth Or DepthNormal)->Opaques->SkyBox->Transparents->PostProcessing,这个顺序也是Urp渲染管线的大致执行顺序。每个Pass或者说每个渲染事件都分Before和After,比如BeforePostProcessing和AfterPostProcessing分别表示后处理之前和后处理之后。
说了这么多,现在说结论,我们的特效Pass或者说特效管线就是要插入在BeforePostProcessing这个事件范围内。对了,同一个事件,比如BeforePostProcessing事件内的pass,最终的执行顺序是已加入管线的先后为准的。
1.2 Urp内置的CameraOpaqueTexture
那么,我们是一定要自定义一个Pass才能获得颜色缓冲吗?不需要,其实Urp的ForwardRenderer内会在某种情况下给我生成一个颜色缓冲存储到贴图_CameraOpaqueTexture中,通过调用函数SampleSceneColor就得获得屏幕颜色。不过,这个贴图的生成时机是固定的,只会在渲染不透明物体之后,更准确的说是在渲染天空盒之后,通过CopyColorPass把摄像机的颜色缓冲Blit到_CameraOpaqueTexture。同时,需要摄像机或者Urp设置中有开启需要OpaqueTexture或者某个Pass的Input有要求ColorTexture。
假如,不需要颜色缓冲中有半透明物体的信息,那么这个_CameraOpaqueTexture就已经足够了。问题是,特效基本是半透明物体,部分场景物体也可能是半透明物体。所以,默认的_CameraOpaqueTexture大概率满足不了需求。
因此,需要在半透明物体渲染之后再获取一次颜色缓冲。这个可以通过在AfterTransparents或者BeforePostProcessing事件中插入一个CopyColorPass来实现。
二、特效渲染管线
说实话,特效同学的要求有点多,要求部分特效受到全屏效果影响部分不受到影响。那么,特效要分成两部分渲染,一部分在全屏特效前,另外一部分在全屏特效后。那么,需要至少4个Pass,全屏特效前的特效Pass->CopyColorPass->全屏特效Pass->全屏特效后的特效Pass。
特效渲染管线如下:
- EffectPass (渲染后处理特效前的特效)
- CopyColorPass (拷贝屏幕颜色)
- UberEffectPostRenderPass (渲染后处理特效)
- EffectPass(渲染后处理特效后的特效)
其中,中间2个Pass最好是能够根据是否有全屏特效来动态激活。
2.1 EffectRenderFeature
Urp中需要定义RenderFeature来配置相应的Pass。因此,我们定义一个专门用于特效管线的Feature。在这个Feature中,我们按照上述的顺序加入这4个Pass,其中2和3根据全屏特效是否存在来判断是否加入渲染管线。
2.2 兼容UI特效穿插UI的解决方案
由于发现自定义一个BeforeRenderingPostProcessing的特效Pass来专门渲染特效,会导致所有的特效都在半透明物体之后渲染,而UI都是在半透明Pass渲染的,ShaderTag是UniversalForward,这样子会导致根据UI的Canvas来动态计算UI特效的sortingOrder以解决UI特效穿插UI的问题失效。因此,需要去除后处理特效前的特效pass,将这个Pass对应的特效改成默认的UniversalForward的ShaderTag。
那么,特效渲染管线最终是:
- Urp默认的DrawObjectsPass(渲染后处理特效前的特效,兼容解决UI特效穿插界面问题的方案)
- CopyColorPass (拷贝屏幕颜色)
- UberEffectPostRenderPass (渲染后处理特效)
- EffectPass(渲染后处理特效后的特效)
关键代码如下,在这个Feature中还定义ColorRT的名字和采样方式、全屏后处理超级Shader的名字等。
1 | public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) |
Urp的ForwardRender配置如图:
2.3 EffectRenderPass
特效渲染Pass用于渲染普通的特效,Pass跟Shader的对应方式是ShaderTag。关键代码如下,
1 | public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) |
有个需要注意的地方是物体渲染的排序方式要用SortingCriteria.CommonTransparent,毕竟特效都是半透明物体。这个标志是Urp默认的渲染半透明物体的排序方式,理论上是从后到前的顺序渲染。
2.4 UberEffectPostRenderPass
后处理特效Pass为了兼容面片类型的扭曲特效和全屏类型的色散、黑白屏、径向模糊特效,调用了2次绘制函数。第一次是用context.DrawRenderers绘制普通的物体;第二次是用cmd.DrawMesh绘制一个全屏三角形。同时为了支持,场景中出现多个全屏特效,该Pass中保存了一个材质数组,同时根据优先级来排序,优先级高的先渲染,这样就可以实现多个全屏特效的叠加效果。
代码如下,
1 | public void AddMaterial(Material mat, int order = 0) |
三、后处理特效
3.1 屏幕扭曲
屏幕扭曲的效果最简单,只是偏移uv坐标即可。实现方式很多,基本上是采样噪声或者法线贴图来偏移uv坐标,核心代码大概如下:
1 | half2 screenUV = input.screenPos.xy / input.screenPos.w; |
以上代码计算了2次偏移,第一次偏移是计算噪声图的uv,第二次是计算颜色缓冲的uv,也就是屏幕uv。
效果如下,中间的部分放了一个扭曲面片特效。
3.2 色散
色散的原理也很简单,计算一个偏移的uv,分别在两个方向上计算r和b,不偏移的位置计算g,合并起来作为完整的颜色输出。
1 | half2 deltaUv = half2(_ColorDispersionStrength * _ColorDispersionU, _ColorDispersionStrength * _ColorDispersionV); |
3.3 黑白屏
黑白屏的关键实现代码也很短。但是想出来不太容易。网上大部分实现,就是简单的灰度化加上和屏幕颜色的插件。后面发现特效同学要的东西其实就是网上找了位特效大佬用ASE生成的shader效果,拿到代码后,过滤掉生成的冗余代码发现核心就是下面2个插值计算。
1 | half luminosity = dot(screenColor.rgb, half3(0.299, 0.587, 0.114)); |
关键代码是smoothstep,在阈值和阈值+阈值范围之间曲线插值,返回的值再用来插值白屏颜色色和黑屏颜色。
3.4 径向模糊
径向模糊的思想是沿着到中点的方向采样几个点,然后平均。代码如下,这里假定是6次采样。
1 | half2 dir = screenUV - half2(_RadialBlurHorizontalCenter, _RadialBlurVerticalCenter); |
不过,以上代码不一定能满足美术的需求。比如dir是否需要归一化,lerp时候是否需要考虑距离中点的远近等都会影响最终的效果。
3.5 色散和径向模糊的结合
如果先计算色散的DeltaUv,再将取屏幕颜色替换为屏幕扭曲的话,就能得到一个色散和径向模糊结合的效果,关键代码如下:
1 | half2 deltaUv = half2(_ColorDispersionStrength * _ColorDispersionU, _ColorDispersionStrength * _ColorDispersionV); |
3.6 黑白屏和其它后处理效果的结合
实现方式是,如果开启了黑白屏,将屏幕颜色都应用一次黑白屏,然后再进行其它的处理,比如色散的代码修改为如下,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 half2 deltaUv = half2(_ColorDispersionStrength * _ColorDispersionU, _ColorDispersionStrength * _ColorDispersionV);
half4 tempScreenColor = SampleScreenColor(screenUV + deltaUv);
tempScreenColor = BlackWhite(tempScreenColor);
result.r = tempScreenColor.r;
tempScreenColor = SampleScreenColor(screenUV);
tempScreenColor = BlackWhite(tempScreenColor);
result.g = tempScreenColor .g;
tempScreenColor = SampleScreenColor(screenUV - deltaUv);
tempScreenColor = BlackWhite(tempScreenColor);
result.b = tempScreenColor .b;
黑白屏和色散结合:
黑白屏和径向模糊结合:
黑白屏和色散、径向模糊结合:
3.7 UberEffectPost超级Shader
具体实现上,我是用一个超级shader将这些功能整合到一起(除了屏幕扭曲,特效的需求是面片)形成一个UberShader。不同的效果通过shader_feature_local的开关来控制,这样既不用增加额外的大小和内存,也更方便美术同学的使用,整合到一起也是美术提出来的。
材质界面如下,
3.8 UberEffectPost脚本
该脚本继承自MonoBehavior,用于判断是否存在全屏特效以及全屏特效材质、全屏特效优先级设置,并且在材质改变时候将后处理材质传入Pass等。
另外,美术同学要求加的后处理参数控制曲线也是在该脚本中,截图如下:
这些参数曲线相对于TimeLine来说,可以更快的生成动态变化的后处理效果,减少美术去编辑TimeLine的工作量,不过自由度会有所降低。
四、参考资料
1、OnRenderImage
2、仿.碧蓝幻想versus黑白闪后处理shader分享(build_in 与urp双版本)