白纸一张

三十而立,四十而不惑

0%

练习项目(十二):URP中自定义后处理实例

概述

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
///CommonRendererFeature.cs
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);
}
}

///CommonPass.cs
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实现的后处理,差别还是有的,但类比之后,还是比较容易上手的。最后说一下,这些流程、效果都是本人自己学习的,目前没有用到实际的项目中,有需要的话,请自行辨别。

参考

-------------本文结束感谢您的阅读-------------