概述
URP项目中,是自带一些后处理效果的,可以通过添加Volume组件来启用相应的效果。那如果想添加自定义的后处理效果呢? 之前在《练习项目(四):深度图基础及应用》 部分介绍了在URP中添加后处理的方法。这里,基本上还是按照上面介绍的流程,只是代码方面有了一些改变,下面会详细介绍。
一、通用的Renderer Feature
对于一些简单的,只是对图像做一次处理的后处理类型,这里可以实现一个通用的Renderer
Feature,不同的效果可以通过不同的Shader实现。
首先是实现CommonRendererFeature,主要是可以配置后处理的材质,还有可以选择后处理的时机。主要的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class CommonRendererFeature : ScriptableRendererFeature { public Material UsedMaterial; public RenderPassEvent PassEvent = RenderPassEvent.BeforeRenderingPostProcessing; CommonPass m_ScriptablePass; public override void Create ( ) { m_ScriptablePass = new CommonPass(UsedMaterial) { renderPassEvent = PassEvent }; } public override void AddRenderPasses (ScriptableRenderer renderer, ref RenderingData renderingData ) { var dest = RenderTargetHandle.CameraTarget; m_ScriptablePass.Setup(renderer.cameraColorTarget, dest); renderer.EnqueuePass(m_ScriptablePass); } }
定义了UsedMaterial和PassEvent两个公有变量,暴露出接口。然后初始化CommonPass,再对CommonPass传递一些变量,最后添加进渲染管线中。这里多说一点,在设置PassEvent的时候,一般选择BeforeRenderingPostProcessing,这样,自定义处理后的纹理,还可以继续被URP自带的后处理流程处理。具体的PassEvent需要具体分析。
然后实现CommonPass,这里是主要的操作处理,主要的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 void Render(CommandBuffer cmd , ref RenderingData renderingData ) { if (renderingData.cameraData.isSceneViewCamera) return; cmd.GetTemporaryRT(m_TemporaryColorTexture .id , renderingData .cameraData .cameraTargetDescriptor , FilterMode.Bilinear) ; var source = currentTarget; cmd.Blit(source , m_TemporaryColorTexture .Identifier() , m_Material); cmd.Blit(m_TemporaryColorTexture .Identifier() ,source); }
首先获取一张临时的渲染贴图m_TemporaryColorTexture,与屏幕的宽高等相同;然后,将当前的帧缓冲source中的数据传递给m_TemporaryColorTexture,并进行相应的后处理,这一步是通过cmd.Blit()方式实现的,相应的处理根据m_Material来执行。
此时,m_TemporaryColorTexture中存储的就是经过后处理的图像。最后一步,将m_TemporaryColorTexture中的内容传递给source。这一步要注意,这里如果传递给destination的话,URP自带的最后的FinalPostProcessing就获取不到经过后处理的图像了,所以这里要传递给source,这样,经过自定义的后处理后,还可以经过URP中的后处理。
这样处理的话,需要两次cmd.Blit()操作,消耗比较大。后来又发现另一种实现方式,主要的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class CommonRendererFeature : ScriptableRendererFeature { public Material UsedMaterial; public RenderPassEvent PassEvent = RenderPassEvent.BeforeRenderingPostProcessing; CommonPass m_ScriptablePass; RenderTargetHandle m_CameraColorAttachment; public override void Create() { m_ScriptablePass = new CommonPass(UsedMaterial) { renderPassEvent = PassEvent }; m_CameraColorAttachment.Init("_CameraColorTexture" ) ; } public override void AddRenderPasses(ScriptableRenderer renderer , ref RenderingData renderingData ) { m_ScriptablePass.Setup(renderer .cameraColorTarget , m_CameraColorAttachment ) ; renderer.EnqueuePass(m_ScriptablePass ) ; } } void Render(CommandBuffer cmd , ref RenderingData renderingData ) { if (renderingData.cameraData.isSceneViewCamera) return; cmd.Blit(currentTarget , destination .Identifier() , m_Material); }
主要的区别,就是使用“_CameraColorTexture”初始化了m_CameraColorAttachment作为CommonPass的渲染目标。从ForwardRenderer.cs中,可以发现,也有一个m_CameraColorAttachment,应该是对应了颜色缓冲。这样,在CommonPass中,cmd.Blit()相当于currentTarget和destination都指向了颜色缓冲。这样做,也可以达到上面的效果,而且只有一次cmd.Blit()操作。但是,我并不确定这样做是否合适。有知道的大佬麻烦告知一下。谢谢!
这里有一点要注意,后处理Shader中必须添加Properties,其中必须有名称为“_MainTex”的属性。否则,cmd.Blit(source,
m_TemporaryColorTexture.Identifier(),
m_Material)这里就无法将source中的纹理传递给Shader。
1、Cross Hatching
思路:采样纹理信息,计算出颜色的“长度”,然后给定一些阈值,对不同范围,满足一定条件的像素,返回黑色;不满足条件的话,返回白色。这样,就形成交叉条纹的效果。
代码如下
2、Thermal Vision
思路:采样纹理信息,计算出颜色值的亮度值,然后使用亮度值在三个颜色之间插值。这样,整个画面好像热视图一样的效果。
代码如下
3、Dream Vision
思路:采样像素及其周围的9个点,然后把颜色相加除以9,这一步,主要是对纹理进行模糊;最后,把得到的颜色,对R、G、B三个通道相加取平均。得到一种黑白化的画面。
代码如下
4、Edge Detection By Sobel
思路:使用Sobel算子根据颜色值进行边界检测。主要是对像素点周围的9个像素值采样,再根据Sobel算子计算,判断该像素点是不是边界。
代码如下
5、Lens Circle
思路:采样纹理,根据UV距离中心的距离,在内圆半径和外圆半径之间插值,根据插值结果与采样得到的纹理颜色相乘。这样,就可以得到内圆内正常显示,外圆外是黑色的,内圆和外圆之间是一个渐变色的效果。
代码如下
6、Pixelation
思路:使用一个变量PixelSize对UV先进行缩小,然后使用floor()操作,向下取整,在使用PixelSize进行放大。最后用处理后的UV采样,这样,就可以得到像素化的效果。
代码如下
7、Posterization
思路:采样纹理,对采样得到的颜色值先使用Num进行放大,然后使用floor()操作,向下取整;再使用Num进行缩小,得到最终的颜色。
代码如下
8、Brightness、Saturation、Contrast
思路:这里主要是使用亮度、饱和度和对比度对纹理采样的颜色进行处理。
代码如下
二、Gaussian Blur
这里,具体的原理思路可以参考《Unity
Shader入门精要》12.4高斯模糊一节。Shader部分的代码基本相同。这里要说一下的是C#部分的代码改动比较多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void Render(CommandBuffer cmd , ref RenderingData renderingData ) { if (renderingData.cameraData.isSceneViewCamera) return; var source = currentTarget; RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor; opaqueDesc.width /= m_DownSample; opaqueDesc.height /= m_DownSample; opaqueDesc.depthBufferBits = 0 ; cmd.GetTemporaryRT(bufferTex0 .id , opaqueDesc , FilterMode.Bilinear) ; cmd.GetTemporaryRT(bufferTex1 .id , opaqueDesc , FilterMode.Bilinear) ; Blit(cmd , source , bufferTex0 .Identifier() ); for (int i = 0 ; i < m_Iterations; i++) { gaussianBlurMat.SetFloat("_BlurSize" , 1.0f + i * m_BlurSpread ) ; Blit(cmd , bufferTex0 .Identifier() , bufferTex1.Identifier() , gaussianBlurMat, 0 ); Blit(cmd , bufferTex1 .Identifier() , bufferTex0.Identifier() , gaussianBlurMat, 1 ); } Blit(cmd , bufferTex0 .Identifier() , source); }
上面,获取了两张临时的纹理,用来进行交换模糊。模糊结束后,还要将最终的模糊纹理传递给source,这样,URP中的自带的后处理可以对模糊后的图像继续处理。
代码如下
三、Bloom
具体的原理思路可以参考《Unity
Shader入门精要》12.5Bloom效果一节,这里介绍一下不同的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void Render(CommandBuffer cmd , ref RenderingData renderingData ) { if (renderingData.cameraData.isSceneViewCamera) return; var source = currentTarget; RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor; opaqueDesc.width /= m_DownSample; opaqueDesc.height /= m_DownSample; opaqueDesc.depthBufferBits = 0 ; cmd.GetTemporaryRT(bufferTex0 .id , opaqueDesc , FilterMode.Bilinear) ; cmd.GetTemporaryRT(bufferTex1 .id , opaqueDesc , FilterMode.Bilinear) ; bloomMat.SetFloat("_LuminanceThreshold" , m_LuminanceThreshold ) ; Blit(cmd , source , bufferTex0 .Identifier() , bloomMat, EXTRACT_PASS); for (int i = 0 ; i < m_Iterations; i++) { bloomMat.SetFloat("_BlurSize" , 1.0f + i * m_BlurSpread ) ; Blit(cmd , bufferTex0 .Identifier() , bufferTex1.Identifier() , bloomMat, GAUSSIAN_HOR_PASS); Blit(cmd , bufferTex1 .Identifier() , bufferTex0.Identifier() , bloomMat, GAUSSIAN_VERT_PASS); } cmd.SetGlobalTexture("_BloomTex" , bufferTex0 .Identifier() ); Blit(cmd , source , bufferTex1 .Identifier() , bloomMat, BLOOM_PASS); Blit(cmd , bufferTex1 .Identifier() , source); }
主要的思路是,先提取原始纹理中亮度超过一定阈值的区域,传递到临时纹理bufferTex0中;然后使用高斯模糊在bufferTex0和bufferTex1之间进行迭代操作;再然后,把原始纹理和模糊后的纹理进行叠加;最后,还要把叠加后的纹理传递回source。
代码如下
四、Motion Blur
具体的原理思路可以参考《Unity
Shader入门精要》12.6运动模糊一节,这里不再详细介绍。主要的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void Render(CommandBuffer cmd , ref RenderingData renderingData ) { if (renderingData.cameraData.isSceneViewCamera) return; var source = currentTarget; if (m_LastRT == null || m_LastRT.width != renderingData.cameraData.cameraTargetDescriptor.width || m_LastRT.height != renderingData.cameraData.cameraTargetDescriptor.height) { Object.DestroyImmediate(m_LastRT ) ; m_LastRT = new RenderTexture(renderingData .cameraData .cameraTargetDescriptor ) ; m_LastRT.hideFlags = HideFlags.HideAndDontSave; Blit(cmd , source , m_LastRT ) ; return; } m_LastRT.MarkRestoreExpected() ; m_Material.SetFloat("_BlurAmount" ,m_BlurAmount ) ; Blit(cmd , source , m_LastRT , m_Material ) ; Blit(cmd , m_LastRT , source ) ; }
这里有个问题,书上是在Build
in管线实现的,在OnDisable()的时候,会销毁m_LastRT。但是在Renderer
Feature中,目前没有发现类似的接口。那么这里可能要使用一些方式自己处理销毁了。这里如果有更好的方法,麻烦告知!
代码如下
五、总结
本篇主要介绍了一些URP中的自定义后处理。与Build
in管线相比,Shader部分的变化并不大,只是这里是使用Renderer
Feature实现的后处理,差别还是有的,但类比之后,还是比较容易上手的。最后说一下,这些流程、效果都是本人自己学习的,目前没有用到实际的项目中,有需要的话,请自行辨别。
参考