Unity版本:2022
URP版本:14.0.6
项目连接:
场景来源网络
在Unity官方文档中,给出了两种使用后措置的方式。第一种使用Global Volume,但仅限于使用内置后措置,自定义后措置需要改削URP,十分麻烦。第二种使用自定义RenderFeature添加自定义RenderPass,但一个后措置效果对应两个脚本,而且会带来多个RenderPass,不竭纹理申请和获取的消耗。
在这个基础上,我们创建一个本身的后措置系统,使得Renderer挂载一个RenderFeature,而且一个后措置效果只需要创建一个脚本。
建议先过一遍官方文档,出格是三部门,Render feature、Volume和Fullscreen blit部门。
官方文档分析
Global Volume 后措置
URP使用Volume框架来措置后措置,但是这些后措置都是URP自带的,也就是在RP里写死了的。
给场景添加一个Global Volume,它用来挂载Volume Profile,而Volume Profile用来挂载具体的后措置实例。通过对分歧场景的Global Volume挂载不异Volume Profile或分歧Volume Profile实现场景不异或分歧的后措置效果。例如下图,给场景添加了一个Bloom效果。
通过查看Bloom.cs源码,发现它是一个担任于VolumeComponent和IPostProcessComponent的子类,而且使用VolumeComponentMenuForRenderPipeline向Volume添加后措置选项。
自定义RendererFeature/Pass
在官方文档的Fullscreen blit中,举了一个用担任于
ScriptableRendererFeature和ScriptableRenderPass的脚本分袂自定义RendererFeature和RendererPass,从而实现一个绿色filter的后措置系统。
此中,担任于ScriptableRendererFeature的脚本代码如下。- using UnityEngine;
- using UnityEngine.Rendering;
- using UnityEngine.Rendering.Universal;
- internal class ColorBlitRendererFeature : ScriptableRendererFeature{
- public Shader m_Shader;
- public float m_Intensity;
- private Material m_Material;
- private ColorBlitPass m_RenderPass = null;
- public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
- if (renderingData.cameraData.cameraType == CameraType.Game)
- // Pass入队
- renderer.EnqueuePass(m_RenderPass);
- }
- public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) {
- // 只对游戏摄像机应用后措置(还有预览摄像机等)
- if (renderingData.cameraData.cameraType == CameraType.Game) {
- // 设置向pass输入color (m_RenderPass父类)
- m_RenderPass.ConfigureInput(ScriptableRenderPassInput.Color);
- // 设置RT为相机的color
- m_RenderPass.SetTarget(renderer.cameraColorTargetHandle, m_Intensity);
- }
- }
- // 基类的抽象函数 OnEnable和OnValidate时调用
- public override void Create() {
- // 创建一个附带m_Shader的material
- m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
- // 创建BiltPass脚本实例
- m_RenderPass = new ColorBlitPass(m_Material);
- }
- protected override void Dispose(bool disposing) {
- CoreUtils.Destroy(m_Material);
- }
- }
复制代码 由于用来自定义RendererFeature,所以这个脚本的用处也显而易见:首先Create()函数将在OnEnable和OnValidate时被调用,它创建一个挂载指定Shader的材质。当为每个camera设置一个renderer时,AddRenderPasses函数将被调用,从而向这个camera的renderer入队自定义RenderPass。当每个camera衬着前,SetupRenderPasses函数将被调用,从而设置自定义RenderPass的衬着源RT和衬着方针RT。
担任于SetupRenderPasses的脚本如下。- using UnityEngine;
- using UnityEngine.Rendering;
- using UnityEngine.Rendering.Universal;
- internal class ColorBlitPass : ScriptableRenderPass{
- // 给profiler入一个新的事件
- private ProfilingSampler m_ProfilingSampler = new ProfilingSampler(”ColorBlit”);
- private Material m_Material;
- // RTHandle,封装了纹理及相关信息,可以认为是CPU端纹理
- private RTHandle m_CameraColorTarget;
- private float m_Intensity;
- public ColorBlitPass(Material material) {
- m_Material = material;
- // 指定执行这个Pass的时机
- renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
- }
- // 指定进行后措置的target
- public void SetTarget(RTHandle colorHandle, float intensity) {
- m_CameraColorTarget = colorHandle;
- m_Intensity = intensity;
- }
- // OnCameraSetup是纯虚函数,相机初始化时调用
- public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {
- // (父类函数)指定pass的render target
- ConfigureTarget(m_CameraColorTarget);
- }
- // Execute时抽象函数,把cmd命令添加到context中(然后进一步送到GPU调用)
- public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
- var cameraData = renderingData.cameraData;
- if (cameraData.cameraType != CameraType.Game)
- return;
- if (m_Material == null)
- return;
- // 获取commandbuffer
- CommandBuffer cmd = CommandBufferPool.Get();
- // 把cmd里执行的命令添加到m_ProfilingSampler定义的profiler块中
- // using用来自动释放new的资源
- using (new ProfilingScope(cmd, m_ProfilingSampler)) {
- m_Material.SetFloat(”_Intensity”, m_Intensity);
- // 使用cmd里的命令(设置viewport等,分辩率等),执行m_Material的pass0,将m_CameraColorTarget衬着到m_CameraColorTarget
- // 本质上画了一个覆盖屏幕的三角形
- Blitter.BlitCameraTexture(cmd, m_CameraColorTarget, m_CameraColorTarget, m_Material, 0);
- }
- // 把cmd中的命令入到context中
- context.ExecuteCommandBuffer(cmd);
- // 清空cmd栈
- cmd.Clear();
-
- CommandBufferPool.Release(cmd);
- }
- }
复制代码 此中最重要的是Execute函数,它措置了后措置的逻辑。具体用途看代码。
最后就是具体的ColorBlitShader,很简单就不解释了。- Shader ”ColorBlit”
- {
- SubShader
- {
- Tags { ”RenderType”=”Opaque” ”RenderPipeline” = ”UniversalPipeline”}
- LOD 100
- ZWrite Off Cull Off
- Pass
- {
- Name ”ColorBlitPass”
- HLSLPROGRAM
- #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
- // The Blit.hlsl file provides the vertex shader (Vert),
- // input structure (Attributes) and output strucutre (Varyings)
- #include ”Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl”
- #pragma vertex Vert
- #pragma fragment frag
- TEXTURE2D_X(_CameraOpaqueTexture);
- SAMPLER(sampler_CameraOpaqueTexture);
- float _Intensity;
- half4 frag (Varyings input) : SV_Target
- {
- UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
- float4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.texcoord);
- return color * float4(0, _Intensity, 0, 1);
- }
- ENDHLSL
- }
- }
- }
复制代码 要应用这个Shader,需要将这个RendererFeature脚本挂载给管线Renderer。
fullscreen blit的效果如下,由于只打开对Game相机的后措置,所以Scene是不能预览的,而且也不管你相机打没打开后措置。
后措置系统设计分析
通过上两个官方文档中的例子,可以发现第二种使用ReanderFeature/Pass来创建一个指定的后措置效果其实就是一个后措置系统的雏形。
然而这样会由一些性能问题。在类似ColorBlitFeature.cs的具体后措置Feature中,它们都在AddRenderPasses()中调用EnqueuePass函数。每个后措置效果城市创建一个RenderPass,即使它们在不异的注入点。这样会导致一些性能问题,例如不竭地请求纹理和创建纹理。
为了解决这个问题,我们可以优化衬着过程。我们可以将不异注入点的后措置效果杂交到一个RenderPass中,然后再通过RendererFeature对这个杂交的RenderPass调用EnqueuePass函数。这样,不异注入点的后措置效果可以共享一个RenderPass,在这个RenderPass的Execute中调用每个具体后措置的衬着逻辑,在不异纹理签名间Blit,从而节省性能消耗。换句话说,让不异注入点的RenderPass包含许多子Pass(子Pass=>一次Blit),每个子Pass措置对应的后措置效果。通过上述描述,可以得出我们只需要向Renderer添加一个RenderFeature即可。
除此之外,这样的设计也很繁琐。每次创建一个新的后措置Shader时,都需要额外创建两个脚本:一个RendererFeature用于创建RenderPass,一个RenderPass用于执行Blit命令。
第一种使用Global Volume的方式就很便利,但仅适用于自带的后措置,要添加自定义后措置可能涉及到管线的改削,不采用。于是有了一个想法,即将第一种和第二种方式杂交一下。
因此,我们想到了一个基本的思路:创建一个后措置基类CustomPostProcessing.cs,让具体的后措置效果担任自这个基类。具体的后措置效果只需要实现本身的衬着逻辑即可。然后,我们需要一个自定义的RendererFeature类来获取所有Volume中CustomPostProcessing.cs子类,并按照它们的具体设置(如注入位置等)创建对应的自定义RenderPass实例。每个RenderPass类在Execute函数中调用对应所有CustomPostProcessing基类的Render函数,从而实现具体后措置脚本的衬着。
大致框架如下。
后措置基类
颠末上面分析,首先需要将CustomPostProcessing的基类添加到Volume的Override菜单里。这里通过前面对Bloom.cs源码的查看。
发现只需要担任VolumeComponent和IPostProcessComponent即可。基类添加VolumeComponentMenuForRenderPipeline()来添加菜单。由于衬着时会生成临时RT,所以还需要担任IDisposable。- public abstract class CustomPostProcessing : VolumeComponent, IPostProcessComponent, IDisposable{
- }
复制代码 然后添加IPostProcessComponent必需要override的一些函数,同样可以通过查看Bloom.cs来填写。(其实是按照不写会报错)
IPostProcessComponent要求定义IsActive()用来返回当前后措置是否active;IsTileCompatible()不知道用来干嘛的,但Bloom.cs里get值false,抄下来就行了。- #region IPostProcessComponent
- public abstract bool IsActive();
-
- public virtual bool IsTileCompatible() => false;
- #endregion
复制代码 IDispose部门如下:- #region IDisposable
- public void Dispose() {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- public virtual void Dispose(bool disposing) {
- }
- #endregion
复制代码 最后为其添加每个公用的属性和函数。
首先是这个后措置效果的注入点,这里先分三个。而且,插手当前后措置在注入点的执行挨次。- public enum CustomPostProcessInjectionPoint{
- AfterOpaqueAndSky,
- BeforePostProcess,
- AfterPostProcess
- }
- public abstract class CustomPostProcessing : VolumeComponent, IPostProcessComponent, IDisposable{
- // 注入点
- public virtual CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;
-
- // 在注入点的挨次
- public virtual int OrderInInjectionPoint => 0;
- }
复制代码 最后,插手Setup配置当前后措置,Render衬着当前后措置。- // 配置当前后措置
- public abstract void Setup();
- // 执行衬着
- public abstract void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination);
复制代码 测试一下子类是否能在Volume组件里显示出来,这里新写一个ColorBlit.cs:- [VolumeComponentMenu(”Custom Post-processing/Color Blit”)]
- public class ColorBlit : CustomPostProcessing{
- public ClampedFloatParameter intensity = new(0.0f, 0.0f, 2.0f)
- public override bool IsActive() => true;
-
- public override void Setup() {
- }
- public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
- }
- public override void Dispose(bool disposing) {
- }
- }
复制代码 成功。
抓取Volume内后措置组件
由前面的分析,CustomRedererFeature应该抓取Volume内后措置的组件,然后按照注入点位置创建3种RenderPass,入到队列中。
CustomRenderPass
首先填写CustomRenderPass的必要代码。为了在Execute内调用对应注入点的CustomPostProcessing的Render函数,我们需要在构造函数中向其传递当前注入点的所有CustomPostProcessing实例。同时,再声明一些必要的变量。
CustomPostProcessingPass.cs- public class CustomPostProcessingPass : ScriptableRenderPass{
- // 所有自定义后措置基类
- private List<CustomPostProcessing> mCustomPostProcessings;
- // 当前active组件下标
- private List<int> mActiveCustomPostProcessingIndex;
- // 每个组件对应的ProfilingSampler
- private string mProfilerTag;
- private List<ProfilingSampler> mProfilingSamplers;
- // 声明RT
- private RTHandle mSourceRT;
- private RTHandle mDesRT;
- private RTHandle mTempRT0;
- private RTHandle mTempRT1;
- private string mTempRT0Name => ”_TemporaryRenderTexture0”;
- private string mTempRT1Name => ”_TemporaryRenderTexture1”;
- public CustomPostProcessingPass(string profilerTag, List<CustomPostProcessing> customPostProcessings) {
- mProfilerTag = profilerTag;
- mCustomPostProcessings = customPostProcessings;
- mActiveCustomPostProcessingIndex = new List<int>(customPostProcessings.Count);
- // 将自定义后措置器对象列表转换成一个性能采样器对象列表
- mProfilingSamplers = customPostProcessings.Select(c => new ProfilingSampler(c.ToString())).ToList();
- mTempRT0 = RTHandles.Alloc(mTempRT0Name, name: mTempRT0Name);
- mTempRT1 = RTHandles.Alloc(mTempRT1Name, name: mTempRT1Name);
- }
- }
复制代码 值得注意的是,在URP14.0(或者在这之前)中,丢弃了原有RenderTargetHandle,而通通使用RTHandle。本来的Init也变成了RTHandles.Alloc,具体更新内容可以看最后的参考连接。
CustomRenderFeature
然后需要在CustomRenderFeature中抓取所有Volume中的后措置组件并按照注入点分类。这在Create()函数里进行,它会在OnEnable和OnValidate时被调用。
管线源码分析
下面讨论如何抓取所有Volume中的组件。
首先,与Volume相关代码封装在VolumeManager.cs中。首先可以发现,它采用单例模式。
忽略那些Internal函数,它提供了一个baseComponentArray属性,而且它是public的。
按照描述,它是担任于VolumeComponent的所有子类类型的列表(注意是类型,而非实例)。注意这个描述,这说明非论后措置基类是否在Volume中,它城市存在baseComponentArray里面。
跟踪一下给这个属性赋值的处所,找到ReloadBaseType()函数。
在函数里获取了所有担任自VolumeComponent的非抽象类类型派生。而且,循环baseComponentTypeArray,把它作为VolumeComponent派生类实例添加到m_ComponentDefaultState里面,这个m_ComponentDefaultState在后面也有用处。
按照注释描述,得知它只会在运行时调用一次,而每次脚本重载在编纂器中启动时,我们需要跟踪项目中的任何兼容组件。
继续跟踪ReloadBaseTypes()函数,发现它是在构造函数里创建的(忽略Editor Only)。的确是运行时只调用一次。也就是说,这个单例被创建时,就填充baseComponentTypeArray。
通过上述讨论,我们得知,如果想要获取所有担任自VolumeComponent的派生类类型,而且筛选出派生类型为CustomPostProcessing的列表,代码如下:- var derivedVolumeComponentTypes = VolumeManager.instance.baseComponentTypeArray;
- var customPostProcessingTypes = derivedVolumeComponentTypes.Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))).Tolist(;
复制代码 但是这只能筛选出担任自VolumeComponent的派生类类型的列表,并不能获取具体派生类实例。继续分析。
注意到这个构造函数里还有一个CreateStack()函数赋值给m_DefaultStack,而且将stack赋值。首先stack是public属性,我们可以直接通过单例访谒到它。然后跟踪CreateStack()。
发现它用Reload(m_ComponentDefaultStata)函数填充stack并返回。继续跟踪Reload函数。发现它就是把m_ComponentDefaultStata里存放的VolumeComponent派生实例的类型和实例分割,放到一个字典components里。
也就是说,如果我们想要知道一个VolumeComponent派生类的具体实例,只需要访谒components即可。但是components是internal的,我们对它进行跟踪,找到GetComponent()函数,正是我们需要的public的返回components的函数。
那么获取所有担任于VolumeComponent的CustomPostProcessing实例的思路就很清晰了:首先获取所有类型为CustomProcessing的元素,让后将它们替换为stack.components里对应的实例。这样既保证了获取的是实例,又保证了实例类型是CustomProcessing。
代码如下:- // 获取VolumeStack
- var stack = VolumeManager.instance.stack;
-
- // 获取volumeStack中所有CustomPostProcessing实例
- var customPostProcessings = VolumeManager.instance.baseComponentTypeArray
- .Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))) // 筛选出volumeStack中的CustomPostProcessing类型元素 非论是否在Volume中 非论是否激活
- .Select(t => stack.GetComponent(t) as CustomPostProcessing) // 将类型元素转换为实例
- .ToList(); // 转换为List
复制代码 具体代码
颠末上述分析,我们首先需要在Create()里抓取VolumeComponent的所有派生类实例。
CustomPostProcessingFeature.cs- public override void Create() {
- // 获取VolumeStack
- var stack = VolumeManager.instance.stack;
- // 获取所有的CustomPostProcessing实例
- mCustomPostProcessings = VolumeManager.instance.baseComponentTypeArray
- .Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))) // 筛选出VolumeComponent派生类类型中所有的CustomPostProcessing类型元素 非论是否在Volume中 非论是否激活
- .Select(t => stack.GetComponent(t) as CustomPostProcessing) // 将类型元素转换为实例
- .ToList(); // 转换为List
- }
复制代码 下一步,对抓取到的所有CustomPostProcessing实例按照注入点分类,而且按照在注入点的挨次进行排序。再把对应CPPs实例传递给RenderPass实例,依据注入点分类通过renderPassEvent设置衬着时间。- // 初始化分歧插入点的render pass
- // 找到在透明物和天空后衬着的CustomPostProcessing
- var afterOpaqueAndSkyCPPs = mCustomPostProcessings
- .Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.AfterOpaqueAndSky) // 筛选出所有CustomPostProcessing类中注入点为透明物体和天空后的实例
- .OrderBy(c => c.OrderInInjectionPoint) // 按照挨次排序
- .ToList(); // 转换为List
- // 创建CustomPostProcessingPass类
- mAfterOpaqueAndSkyPass = new CustomPostProcessingPass(”Custom PostProcess after Skybox”, afterOpaqueAndSkyCPPs);
- // 设置Pass执行时间
- mAfterOpaqueAndSkyPass.renderPassEvent = RenderPassEvent.AfterRenderingSkybox;
- var beforePostProcessingCPPs = mCustomPostProcessings
- .Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.BeforePostProcess)
- .OrderBy(c => c.OrderInInjectionPoint)
- .ToList();
- mBeforePostProcessPass = new CustomPostProcessingPass(”Custom PostProcess before PostProcess”, beforePostProcessingCPPs);
- mBeforePostProcessPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
- var afterPostProcessCPPs = mCustomPostProcessings
- .Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.AfterPostProcess)
- .OrderBy(c => c.OrderInInjectionPoint)
- .ToList();
- mAfterPostProcessPass = new CustomPostProcessingPass(”Custom PostProcess after PostProcessing”, afterPostProcessCPPs);
- mAfterPostProcessPass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
复制代码 抓取完CustomPostProcessing实例后,我们需要在Dispose里全部释放他们。
CustomPostProcessingFeature.cs- protected override void Dispose(bool disposing) {
- base.Dispose(disposing);
- if (disposing && mCustomPostProcessings != null) {
- foreach (var item in mCustomPostProcessings) {
- item.Dispose();
- }
- }
- }
复制代码 注入Pass
前面提到,通过VolumeManager抓取是所有VolumeComponent的派生类,不管是否在Volume中,所以我们还需要在RenderPass中判断杂交的CustomPostProcessing是否是激活状态。
CustomPostProcessing.cs- // 获取active的CPPs下标,并返回是否存在有效组件
- public bool SetupCustomPostProcessing() {
- mActiveCustomPostProcessingIndex.Clear();
- for (int i = 0; i < mCustomPostProcessings.Count; i++) {
- mCustomPostProcessings[i].Setup();
- if (mCustomPostProcessings[i].IsActive()) {
- mActiveCustomPostProcessingIndex.Add(i);
- }
- }
- return mActiveCustomPostProcessingIndex.Count != 0;
- }
复制代码 下面完成RenderFeature的第二个任务,将RenderPass EnqueuePass。它在AddRenderPasses函数中进行,我们将分歧的注入点的RenderPass注入到renderer中。
CustomRenderFeature.cs- // 当为每个摄像机设置一个衬着器时,调用此方式
- // 将分歧注入点的RenderPass注入到renderer中
- public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
- // 当前衬着的游戏相机撑持后措置
- if (renderingData.cameraData.postProcessEnabled) {
- // 为每个render pass设置RT
- // 而且将pass列表加到renderer中
- if (mAfterOpaqueAndSkyPass.SetupCustomPostProcessing()) {
- mAfterOpaqueAndSkyPass.ConfigureInput(ScriptableRenderPassInput.Color);
- renderer.EnqueuePass(mAfterOpaqueAndSkyPass);
- }
-
- if (mBeforePostProcessPass.SetupCustomPostProcessing()) {
- mBeforePostProcessPass.ConfigureInput(ScriptableRenderPassInput.Color);
- renderer.EnqueuePass(mBeforePostProcessPass);
- }
-
- if (mAfterPostProcessPass.SetupCustomPostProcessing()) {
- mAfterPostProcessPass.ConfigureInput(ScriptableRenderPassInput.Color);
- renderer.EnqueuePass(mAfterPostProcessPass);
- }
- }
- }
复制代码 网上有些资料在这个函数里配置RenderPass的源RT和方针RT,具体来说使用类似RenderPass.Setup(renderer.cameraColorTargetHandle, renderer.cameraColorTargetHandle)的方式,但是这在URP14.0中会报错,提示renderer.cameraColorTargetHandle只能在ScriptableRenderPass子类里调用。具体细节可以查看最后的参考连接。
下面再讨论一种错误做法。转到RenderPass中,在OnCameraSetup重载函数中设置当前RenderPass的源RT和方针RT,它将在相机衬着前被调用。- public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {
- RenderTextureDescriptor blitTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
- blitTargetDescriptor.depthBufferBits = 0;
- var renderer = renderingData.cameraData.renderer;
- // 源RT固定为相机的颜色RT ”_CameraColorAttachmentA”
- mSourceRT = renderer.cameraColorTargetHandle;
- mDesRT = renderer.cameraColorTargetHandle;
- }
复制代码 这会带来什么错误呢?如果注入点非后措置后,则相机源和方针应该为_CameraColorAttachmentA。但是注入点在后措置后时,RP会进行一次FinalBlit,最终相机的源和方针应该为_CameraColorAttachmentB。不外这不需要判断,直接在Execute里面赋值即可。(按理来说可能消耗会更高,但是我不知道怎样在有finalBlit的情况下提前获取_CameraColorAttachmentB)
如图,这是只有注入点在衬着天空盒后的Frame Debugger。它没有Final Blit,所以可以把RT设置为_CameraColorAttachmentA。
然后,这是添加注入点在后措置后的Frame Debugger。它生出了Final Blit,而且Final Blit的输入源RT为_CameraColorAttachmentB。
所以这个注入点在后措置后的后措置RT应该为_CameraColorAttchmentB。
RenderPass衬着
下面完成最后一步,即在CustomPostProcessingPass的Execute函数里填写具体的衬着代码。
主要流程如下
- 声明临时纹理
- 设置源衬着纹理mSourceRT方针衬着纹理mDesRT为衬着数据的相机颜色方针措置。(区分有无finalBlit)
- 如果只有一个后措置效果,则直接将这个后措置效果从mSourceRT衬着到mTempRT0。
- 如果有多个后措置效果,则逐后措置的在mTempRT0和mTempRT1之间衬着。由于每次循环结束交换它们,所以最终纹理依然存在mTempRT0。
- 使用Blitter.BlitCameraTexture函数将mTempRT0中的成果复制到方针衬着纹理mDesRT中。
完整代码如下:- // 实现衬着逻辑
- public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
- // 初始化commandbuffer
- var cmd = CommandBufferPool.Get(mProfilerTag);
- context.ExecuteCommandBuffer(cmd);
- cmd.Clear();
- // 获取相机Descriptor
- var descriptor = renderingData.cameraData.cameraTargetDescriptor;
- descriptor.msaaSamples = 1;
- descriptor.depthBufferBits = 0;
- // 初始化临时RT
- bool rt1Used = false;
- // 设置源和方针RT为本次衬着的RT 在Execute里进行 特殊措置后措置后注入点
- mDesRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
- mSourceRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
- // 声明temp0临时纹理
- // cmd.GetTemporaryRT(Shader.PropertyToID(mTempRT0.name), descriptor);
- // mTempRT0 = RTHandles.Alloc(mTempRT0.name);
- RenderingUtils.ReAllocateIfNeeded(ref mTempRT0, descriptor, name: mTempRT0Name);
- // 执行每个组件的Render方式
- if (mActiveCustomPostProcessingIndex.Count == 1) {
- int index = mActiveCustomPostProcessingIndex[0];
- using (new ProfilingScope(cmd, mProfilingSamplers[index])) {
- mCustomPostProcessings[index].Render(cmd, ref renderingData, mSourceRT, mTempRT0);
- }
- }
- else {
- // 如果有多个组件,则在两个RT上来回bilt
- RenderingUtils.ReAllocateIfNeeded(ref mTempRT1, descriptor, name: mTempRT1Name);
- rt1Used = true;
- Blit(cmd, mSourceRT, mTempRT0);
- for (int i = 0; i < mActiveCustomPostProcessingIndex.Count; i++) {
- int index = mActiveCustomPostProcessingIndex[i];
- var customProcessing = mCustomPostProcessings[index];
- using (new ProfilingScope(cmd, mProfilingSamplers[index])) {
- customProcessing.Render(cmd, ref renderingData, mTempRT0, mTempRT1);
- }
- CoreUtils.Swap(ref mTempRT0, ref mTempRT1);
- }
- }
-
- Blitter.BlitCameraTexture(cmd, mTempRT0, mDesRT);
- // 释放
- cmd.ReleaseTemporaryRT(Shader.PropertyToID(mTempRT0.name));
- if (rt1Used) cmd.ReleaseTemporaryRT(Shader.PropertyToID(mTempRT1.name));
- context.ExecuteCommandBuffer(cmd);
- CommandBufferPool.Release(cmd);
- }
复制代码 例子:ColorBlit
在这之前,注意把CustomPostProcessingFeature添加到Renderer中。
下面把官方文档里的ColorBlit例子改为我们CPP系统能用的脚本。
ColorBlit.cs
C#很简单,首先创建一个挂载ColorBlit.shader的材质,然后在Render()函数中调用Blit从sourceRT调用材质的Shader衬着给destinationRT。
需要注意的是,我们的RenderFeature抓取的CusomProcessing是全部基于VolumenComponent的派生类,而不是当前场景Global Volume组件里的后措置。所以为了RenderPass能够判断当前后措置是否有效,最好给每个后措置IsActive()除了判断材质非空外加上一个属性的约束。- using CPP;
- using UnityEngine;
- using UnityEngine.Rendering;
- using UnityEngine.Rendering.Universal;
- namespace CPP.EFFECTS{
- [VolumeComponentMenu(”Custom Post-processing/Color Blit”)]
- public class ColorBlit : CustomPostProcessing{
- public ClampedFloatParameter intensity = new(0.0f, 0.0f, 2.0f);
- private Material mMaterial;
- private const string mShaderName = ”Hidden/PostProcess/ColorBlit”;
- public override bool IsActive() => mMaterial != null && intensity.value > 0;
- public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterOpaqueAndSky;
- public override int OrderInInjectionPoint => 0;
- public override void Setup() {
- if (mMaterial == null)
- mMaterial = CoreUtils.CreateEngineMaterial(mShaderName);
- }
- public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
- if (mMaterial == null) return;
- mMaterial.SetFloat(”_Intensity”, intensity.value);
- cmd.Blit(source, destination, mMaterial, 0);
- }
- public override void Dispose(bool disposing) {
- base.Dispose(disposing);
- CoreUtils.Destroy(mMaterial);
- }
- }
- }
复制代码 PostProcessing.hlsl
在官方文档中,对相机RT的采样用的是对_BlitTexture的SAMPLE_TEXTURE2D_X。_BlitTexture在Blit.hlsl库中声明。但是使用Blit函数衬着到的相机纹理是_MainTex(不知道是不是API调用的原因)。而且,_MainTex必需在Properties中定义。
这里仿照官方提供的Blit.hlsl,将公用的声明和常用函数放到Common/PostProcessing.hlsl中。- #ifndef POSTPROCESSING_INCLUDED
- #define POSTPROCESSING_INCLUDED
- #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
- TEXTURE2D(_MainTex);
- SAMPLER(sampler_MainTex);
- TEXTURE2D(_CameraDepthTexture);
- SAMPLER(sampler_CameraDepthTexture);
- struct Attributes {
- float4 positionOS : POSITION;
- float2 uv : TEXCOORD0;
- };
- struct Varyings {
- float2 uv : TEXCOORD0;
- float4 vertex : SV_POSITION;
- UNITY_VERTEX_OUTPUT_STEREO
- };
- half4 SampleSourceTexture(float2 uv) {
- return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
- }
- half4 SampleSourceTexture(Varyings input) {
- return SampleSourceTexture(input.uv);
- }
- Varyings Vert(Attributes input) {
- Varyings output = (Varyings)0;
- // 分配instance id
- UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
- VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
- output.vertex = vertexInput.positionCS;
- output.uv = input.uv;
- return output;
- }
- #endif
复制代码 ColorBlit.shader
- Shader ”Hidden/PostProcess/ColorBlit” {
- Properties {
- // 显式声明出来_MainTex
- [HideInInspector]_MainTex (”Base (RGB)”, 2D) = ”white” {}
- }
- SubShader {
- Tags {
- ”RenderType”=”Opaque”
- ”RenderPipeline” = ”UniversalPipeline”
- }
- LOD 200
- Pass {
- Name ”ColorBlitPass”
- HLSLPROGRAM
- #include ”Common/PostProcessing.hlsl”
- #pragma vertex Vert
- #pragma fragment frag
- float _Intensity;
- half4 frag(Varyings input) : SV_Target {
- float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
- return color * float4(0, _Intensity, 0, 1);
- }
- ENDHLSL
- }
- }
- }
复制代码 例子:ColorAdjustment
最基本的色彩调整后措置。
ColorAdjustment.cs
- using UnityEngine;
- using UnityEngine.Rendering;
- using UnityEngine.Rendering.Universal;
- using CPP;
- namespace CPP.EFFECTS{
- [VolumeComponentMenu(”Custom Post-processing/Color Adjusment”)]
- public class ColorAdjustments : CustomPostProcessing{
- #region Parameters Define
- // 后曝光
- public FloatParameter postExposure = new FloatParameter(0.0f);
- // 对比度
- public ClampedFloatParameter contrast = new ClampedFloatParameter(0.0f, 0.0f, 100.0f);
- // 颜色滤镜
- public ColorParameter colorFilter = new ColorParameter(Color.white, true, false, false);
- // 色相偏移
- public ClampedFloatParameter hueShift = new ClampedFloatParameter(0.0f, -180.0f, 180.0f);
- // 饱和度
- public ClampedFloatParameter saturation = new ClampedFloatParameter(0.0f, -100.0f, 100.0f);
- #endregion
- private Material mMaterial;
- private const string mShaderName = ”Hidden/PostProcess/ColorAdjusments”;
- #region Active State Check
- public override bool IsActive() =>
- mMaterial != null && (IsPostExposureActive() || IsContrastActive() || IsContrastActive() || IsColorFilterActive() || IsHueShiftActive() || IsSaturationActive());
- private bool IsPostExposureActive() => postExposure.value != 0.0f;
- private bool IsContrastActive() => contrast.value != 0.0f;
- private bool IsColorFilterActive() => colorFilter.value != Color.white;
- private bool IsHueShiftActive() => hueShift.value != 0.0f;
- private bool IsSaturationActive() => saturation.value != 0.0f;
- #endregion
- public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;
- public override int OrderInInjectionPoint => 99;
- private int mColorAdjustmentsId = Shader.PropertyToID(”_ColorAdjustments”),
- mColorFilterId = Shader.PropertyToID(”_ColorFilter”);
- private const string mExposureKeyword = ”EXPOSURE”,
- mContrastKeyword = ”CONTRAST”,
- mHueShiftKeyword = ”HUE_SHIFT”,
- mSaturationKeyword = ”SATURATION”,
- mColorFilterKeyword = ”COLOR_FILTER”;
- public override void Setup() {
- if (mMaterial == null)
- mMaterial = CoreUtils.CreateEngineMaterial(mShaderName);
- }
- public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
- if (mMaterial == null) return;
- Vector4 colorAdjustmentsVector4 = new Vector4(
- Mathf.Pow(2f, postExposure.value), // 曝光度 曝光单元是2的幂次
- contrast.value * 0.01f + 1f, // 对比度 将范围从[-100, 100]映射到[0, 2]
- hueShift.value * (1.0f / 360.0f), // 色相偏移 将范围从[-180, 180]转换到[-0.5, 0.5]
- saturation.value * 0.01f + 1.0f); // 饱和度 将范围从[-100, 100]转换到[0, 2]
- mMaterial.SetVector(mColorAdjustmentsId, colorAdjustmentsVector4);
- mMaterial.SetColor(mColorFilterId, colorFilter.value);
- // 按照是否激活对应调整设置keyword
- SetKeyWord(mExposureKeyword, IsPostExposureActive());
- SetKeyWord(mContrastKeyword, IsContrastActive());
- SetKeyWord(mHueShiftKeyword, IsHueShiftActive());
- SetKeyWord(mSaturationKeyword, IsSaturationActive());
- SetKeyWord(mColorFilterKeyword, IsColorFilterActive());
- cmd.Blit(source, destination, mMaterial, 0);
- }
- private void SetKeyWord(string keyword, bool enabled = true) {
- if (enabled) mMaterial.EnableKeyword(keyword);
- else mMaterial.DisableKeyword(keyword);
- }
- public override void Dispose(bool disposing) {
- base.Dispose(disposing);
- CoreUtils.Destroy(mMaterial);
- }
- }
- }
复制代码 ColorAdjustment.shader
其他
首先,编纂模式下。切换场景,Scene和Game视图相机里的后措置并不会刷新,此时先封锁Renderer的CustomPostProcessingFeature组件再打开,后措置就刷新了。
但是,在播放模式下,通过场景切换脚本,后措置还是可以刷新的。
目前还没找到编纂模式下直接刷新的方式,以后找到了再更新一下。
此外,衬着的性能也没怎么优化,例如Blit替换为全屏三角形等,后面再更新。
先把这套用一用再更新一些细节。
更新:绘制法式化全屏三角形
在之前的衬着方式中,我们使用带材质参数的Blit函数,通过查看frame debugger,可以明确它实际上绘制了一个四边形。
然而,我们使用不带有材质参数的Blit时,它会调用CoreBlit.shader,而且只绘制一个三角形。这使得顶点和索引数量减少,带来的消耗必定更少了。
所以我们的目的就是放弃使用带材质参数的Blit函数,而是自定义一个函数来使用绘制法式化三角形的方式进行衬着。
在后措置基类中,新建一个Draw函数,它接收CommandBuffer,源、方针RT以及调用pass索引。绘制法式如下:将GPU端的”_SourceTexture”纹理设置为sourceRT,将RenderTarget设置为destination,绘制法式化三角形。
CustomPostProcessing.cs- // 材质声明
- protected Material mMaterial = null;
- private Material mCopyMaterial = null;
- private const string mCopyShaderName = ”Hidden/PostProcess/PostProcessCopy”;
- protected override void OnEnable() {
- base.OnEnable();
- if (mCopyMaterial == null) {
- mCopyMaterial = CoreUtils.CreateEngineMaterial(mCopyShaderName);
- }
-
- #region Draw Function
- private int mSourceTextureId = Shader.PropertyToID(”_SourceTexture”);
- public virtual void Draw(CommandBuffer cmd, in RTHandle source, in RTHandle destination, int pass = -1) {
- // 将GPU端_SourceTexture设置为source
- cmd.SetGlobalTexture(mSourceTextureId, source);
- // 将RT设置为destination 不关心初始状态(直接填充) 需要存储
- cmd.SetRenderTarget(destination, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
- // 绘制法式化三角形
- if (pass == -1 || mMaterial == null)
- cmd.DrawProcedural(Matrix4x4.identity, mCopyMaterial, 0, MeshTopology.Triangles, 3);
- else
- cmd.DrawProcedural(Matrix4x4.identity, mMaterial, pass, MeshTopology.Triangles, 3);
- }
- #endregion
复制代码 这个三角形应该是覆盖整个屏幕的大三角形,则此时屏幕xy只占了三角形xy的一半,而且索引挨次按顺时针。屏幕坐标范围为 [-1,1] ,所以这个三角形的(裁剪)坐标分袂为 (-1, -1),(-1,3),(3,-1) 。UV范围为 [-1,1] ,所以这个三角形的UV坐标(屏幕)分袂为 (0,0), (0, 2), (2, 0) 。
然后更新后措置shader公用基础Shader,顶点着色器中,我们通过顶点索引判断裁剪空间顶点坐标和屏幕UV。这里仿照VertexPositionInputs的方式写了一个转换函数,用于便利可能呈现的自定义顶点着色器情况。
PostProcessing.hlsl- struct ScreenSpaceData {
- float4 positionCS;
- float2 uv;
- };
- ScreenSpaceData GetScreenSpaceData(uint vertexID : SV_VertexID) {
- ScreenSpaceData output;
- // 按照id判断三角形顶点的坐标
- // 坐标挨次为(-1, -1) (-1, 3) (3, -1)
- output.positionCS = float4(vertexID <= 1 ? -1.0 : 3.0, vertexID == 1 ? 3.0 : -1.0, 0.0, 1.0);
- output.uv = float2(vertexID <= 1 ? 0.0 : 2.0, vertexID == 1 ? 2.0 : 0.0);
- // 分歧API可能会发生倒置的情况 进行判断
- if (_ProjectionParams.x < 0.0) {
- output.uv.y = 1.0 - output.uv.y;
- }
- return output;
- }
- Varyings Vert(uint vertexID : SV_VertexID) {
- Varyings output;
- ScreenSpaceData ssData = GetScreenSpaceData(vertexID);
- output.positionCS = ssData.positionCS;
- output.uv = ssData.uv;
- return output;
- }
复制代码 然后再定义一些基本的功能。- #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
- #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl”
- TEXTURE2D(_SourceTexture);
- SAMPLER(sampler_SourceTexture);
- TEXTURE2D(_CameraDepthTexture);
- SAMPLER(sampler_CameraDepthTexture);
- struct Varyings {
- float4 positionCS : SV_POSITION;
- float2 uv : TEXCOORD0;
- UNITY_VERTEX_OUTPUT_STEREO
- };
- half4 GetSource(float2 uv) {
- return SAMPLE_TEXTURE2D(_SourceTexture, sampler_SourceTexture, uv);
- }
- half4 GetSource(Varyings input) {
- return GetSource(input.uv);
- }
- float SampleDepth(float2 uv) {
- #if defined(UNITY_STEREO_INSTANCING_ENABLED) || defined(UNITY_STEREO_MULTIVIEW_ENABLED)
- return SAMPLE_TEXTURE2D_ARRAY(_CameraDepthTexture, sampler_CameraDepthTexture, uv, unity_StereoEyeIndex).r;
- #else
- return SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv);
- #endif
- }
- float SampleDepth(Varyings input) {
- return SampleDepth(input.uv);
- }
复制代码 注意,此时的后措置Shader需要封锁深度写入,封锁剔除,即:然后再写一个用了复制color buffer的Shader。
PostProcessCopy.shader- Shader ”Hidden/PostProcess/PostProcessCopy” {
- SubShader {
- Tags {
- ”RenderType”=”Opaque”
- ”RenderPipeline” = ”UniversalPipeline”
- }
- LOD 200
- ZWrite Off
- Cull Off
- HLSLINCLUDE
- #include ”PostProcessing.hlsl”
- ENDHLSL
- Pass {
- Name ”CopyPass”
- HLSLPROGRAM
- #pragma vertex Vert
- #pragma fragment frag;
- half4 frag(Varyings input) : SV_Target {
- half4 color = GetSource(input);
- return half4(color.rgb, 1.0);
- }
- ENDHLSL
- }
- }
- }
复制代码 最后,把自定义后措置的C#脚本中的Blit函数通通换成Draw函数。- // cmd.Blit(source, destination, mMaterial, 0);
- Draw(cmd, source, destination, 0);
复制代码 更新
改了临时纹理的声明和释放逻辑。
具体来说,在CustomPostProcessingPass的OnCameraSetup()函数中分配本次主Pass的中间纹理,而且分袂该主Pass下的CustomPostProcessing实例的OnCameraSetup()函数,它们各自在此中分配本身的临时纹理。纹理的分配使用RenderingUtils.ReAllocateIfNeeded函数,猜测它不像CommandBuffer加命令那样可以让GPU按挨次执行分配纹理和之后的衬着逻辑,所以只能在正式衬着前将纹理全部门配,最后在统一释放。这也是Unity文档和我搜索资料的写法,有没有更好的方式我暂时没有看到。
最重要的是释放纹理,否则会直接死机。参考Unity文档的写法,在CustomPostProcessingFeature的Dispose()函数中,首先调用各个主Pass实例和CustomPostProcessing实例的Dispose()函数,它们在此中分袂释放各自纹理。使用RTHandle.Release()函数。
添加更多后措置效果,部门参考毛星云前辈的X-PostProcessing库。
参考
pamisu: [Unity]为了更好用的后措置——扩展URP后措置踩坑记录
tatsuya-kosuda: URPCustomPostProcessing
Adapt URP to use RTHandles
(URP 13.1.8) Proper RTHandle usage in a Renderer Feature
How to Blit in URP - Documentation & Unity blog post on every Blit function |