找回密码
 立即注册
查看: 303|回复: 0

[笔记] Unity URP14.0 自定义后措置系统

[复制链接]
发表于 2024-7-15 18:58 | 显示全部楼层 |阅读模式
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的脚本代码如下。
  1. using UnityEngine;
  2. using UnityEngine.Rendering;
  3. using UnityEngine.Rendering.Universal;
  4. internal class ColorBlitRendererFeature : ScriptableRendererFeature{
  5.     public Shader m_Shader;
  6.     public float m_Intensity;
  7.     private Material m_Material;
  8.     private ColorBlitPass m_RenderPass = null;
  9.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
  10.         if (renderingData.cameraData.cameraType == CameraType.Game)
  11.             // Pass入队
  12.             renderer.EnqueuePass(m_RenderPass);
  13.     }
  14.     public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) {
  15.         // 只对游戏摄像机应用后措置(还有预览摄像机等)
  16.         if (renderingData.cameraData.cameraType == CameraType.Game) {
  17.             // 设置向pass输入color (m_RenderPass父类)
  18.             m_RenderPass.ConfigureInput(ScriptableRenderPassInput.Color);
  19.             // 设置RT为相机的color
  20.             m_RenderPass.SetTarget(renderer.cameraColorTargetHandle, m_Intensity);
  21.         }
  22.     }
  23.     // 基类的抽象函数 OnEnable和OnValidate时调用
  24.     public override void Create() {
  25.         // 创建一个附带m_Shader的material
  26.         m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
  27.         // 创建BiltPass脚本实例
  28.         m_RenderPass = new ColorBlitPass(m_Material);
  29.     }
  30.     protected override void Dispose(bool disposing) {
  31.         CoreUtils.Destroy(m_Material);
  32.     }
  33. }
复制代码
由于用来自定义RendererFeature,所以这个脚本的用处也显而易见:首先Create()函数将在OnEnable和OnValidate时被调用,它创建一个挂载指定Shader的材质。当为每个camera设置一个renderer时,AddRenderPasses函数将被调用,从而向这个camera的renderer入队自定义RenderPass。当每个camera衬着前,SetupRenderPasses函数将被调用,从而设置自定义RenderPass的衬着源RT和衬着方针RT。

担任于SetupRenderPasses的脚本如下。
  1. using UnityEngine;
  2. using UnityEngine.Rendering;
  3. using UnityEngine.Rendering.Universal;
  4. internal class ColorBlitPass : ScriptableRenderPass{
  5.     // 给profiler入一个新的事件
  6.     private ProfilingSampler m_ProfilingSampler = new ProfilingSampler(”ColorBlit”);
  7.     private Material m_Material;
  8.     // RTHandle,封装了纹理及相关信息,可以认为是CPU端纹理
  9.     private RTHandle m_CameraColorTarget;
  10.     private float m_Intensity;
  11.     public ColorBlitPass(Material material) {
  12.         m_Material = material;
  13.         // 指定执行这个Pass的时机
  14.         renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
  15.     }
  16.     // 指定进行后措置的target
  17.     public void SetTarget(RTHandle colorHandle, float intensity) {
  18.         m_CameraColorTarget = colorHandle;
  19.         m_Intensity = intensity;
  20.     }
  21.     // OnCameraSetup是纯虚函数,相机初始化时调用
  22.     public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {
  23.         // (父类函数)指定pass的render target
  24.         ConfigureTarget(m_CameraColorTarget);
  25.     }
  26.     // Execute时抽象函数,把cmd命令添加到context中(然后进一步送到GPU调用)
  27.     public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
  28.         var cameraData = renderingData.cameraData;
  29.         if (cameraData.cameraType != CameraType.Game)
  30.             return;
  31.         if (m_Material == null)
  32.             return;
  33.         // 获取commandbuffer
  34.         CommandBuffer cmd = CommandBufferPool.Get();
  35.         // 把cmd里执行的命令添加到m_ProfilingSampler定义的profiler块中
  36.         // using用来自动释放new的资源
  37.         using (new ProfilingScope(cmd, m_ProfilingSampler)) {
  38.             m_Material.SetFloat(”_Intensity”, m_Intensity);
  39.             // 使用cmd里的命令(设置viewport等,分辩率等),执行m_Material的pass0,将m_CameraColorTarget衬着到m_CameraColorTarget
  40.             // 本质上画了一个覆盖屏幕的三角形
  41.             Blitter.BlitCameraTexture(cmd, m_CameraColorTarget, m_CameraColorTarget, m_Material, 0);
  42.         }
  43.         // 把cmd中的命令入到context中
  44.         context.ExecuteCommandBuffer(cmd);
  45.         // 清空cmd栈
  46.         cmd.Clear();
  47.         
  48.         CommandBufferPool.Release(cmd);
  49.     }
  50. }
复制代码
此中最重要的是Execute函数,它措置了后措置的逻辑。具体用途看代码。

最后就是具体的ColorBlitShader,很简单就不解释了。
  1. Shader ”ColorBlit”
  2. {
  3.         SubShader
  4.     {
  5.         Tags { ”RenderType”=”Opaque” ”RenderPipeline” = ”UniversalPipeline”}
  6.         LOD 100
  7.         ZWrite Off Cull Off
  8.         Pass
  9.         {
  10.             Name ”ColorBlitPass”
  11.             HLSLPROGRAM
  12.             #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
  13.             // The Blit.hlsl file provides the vertex shader (Vert),
  14.             // input structure (Attributes) and output strucutre (Varyings)
  15.             #include ”Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl”
  16.             #pragma vertex Vert
  17.             #pragma fragment frag
  18.             TEXTURE2D_X(_CameraOpaqueTexture);
  19.             SAMPLER(sampler_CameraOpaqueTexture);
  20.             float _Intensity;
  21.             half4 frag (Varyings input) : SV_Target
  22.             {
  23.                 UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
  24.                 float4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.texcoord);
  25.                 return color * float4(0, _Intensity, 0, 1);
  26.             }
  27.             ENDHLSL
  28.         }
  29.     }
  30. }
复制代码
要应用这个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。
  1.     public abstract class CustomPostProcessing : VolumeComponent, IPostProcessComponent, IDisposable{
  2.         }
复制代码
然后添加IPostProcessComponent必需要override的一些函数,同样可以通过查看Bloom.cs来填写。(其实是按照不写会报错)
IPostProcessComponent要求定义IsActive()用来返回当前后措置是否active;IsTileCompatible()不知道用来干嘛的,但Bloom.cs里get值false,抄下来就行了。
  1.         #region IPostProcessComponent
  2.         public abstract bool IsActive();
  3.        
  4.         public virtual bool IsTileCompatible() => false;
  5.          #endregion
复制代码
IDispose部门如下:
  1.         #region IDisposable  
  2.         public void Dispose() {  
  3.             Dispose(true);  
  4.             GC.SuppressFinalize(this);  
  5.         }  
  6.        
  7.         public virtual void Dispose(bool disposing) {  
  8.         }  
  9.         #endregion
复制代码
最后为其添加每个公用的属性和函数。
首先是这个后措置效果的注入点,这里先分三个。而且,插手当前后措置在注入点的执行挨次。
  1. public enum CustomPostProcessInjectionPoint{
  2.         AfterOpaqueAndSky,
  3.         BeforePostProcess,
  4.         AfterPostProcess
  5. }
  6.     public abstract class CustomPostProcessing : VolumeComponent, IPostProcessComponent, IDisposable{
  7.         // 注入点
  8.         public virtual CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;
  9.         
  10.         //  在注入点的挨次
  11.         public virtual int OrderInInjectionPoint => 0;
  12. }
复制代码
最后,插手Setup配置当前后措置,Render衬着当前后措置。
  1.         // 配置当前后措置
  2.         public abstract void Setup();
  3.         // 执行衬着
  4.         public abstract void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination);
复制代码
测试一下子类是否能在Volume组件里显示出来,这里新写一个ColorBlit.cs:
  1. [VolumeComponentMenu(”Custom Post-processing/Color Blit”)]
  2. public class ColorBlit : CustomPostProcessing{
  3.         public ClampedFloatParameter intensity = new(0.0f, 0.0f, 2.0f)
  4.         public override bool IsActive() => true;
  5.                
  6.         public override void Setup() {
  7.         }
  8.         public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
  9.         }
  10.         public override void Dispose(bool disposing) {
  11.         }
  12. }
复制代码
成功。



抓取Volume内后措置组件

由前面的分析,CustomRedererFeature应该抓取Volume内后措置的组件,然后按照注入点位置创建3种RenderPass,入到队列中。
CustomRenderPass

首先填写CustomRenderPass的必要代码。为了在Execute内调用对应注入点的CustomPostProcessing的Render函数,我们需要在构造函数中向其传递当前注入点的所有CustomPostProcessing实例。同时,再声明一些必要的变量。
CustomPostProcessingPass.cs
  1. public class CustomPostProcessingPass : ScriptableRenderPass{
  2.         // 所有自定义后措置基类
  3.         private List<CustomPostProcessing> mCustomPostProcessings;
  4.         // 当前active组件下标
  5.         private List<int> mActiveCustomPostProcessingIndex;
  6.         // 每个组件对应的ProfilingSampler
  7.         private string mProfilerTag;
  8.         private List<ProfilingSampler> mProfilingSamplers;
  9.         // 声明RT
  10.         private RTHandle mSourceRT;
  11.         private RTHandle mDesRT;
  12.         private RTHandle mTempRT0;
  13.         private RTHandle mTempRT1;
  14.         private string mTempRT0Name => ”_TemporaryRenderTexture0”;
  15.         private string mTempRT1Name => ”_TemporaryRenderTexture1”;
  16.         public CustomPostProcessingPass(string profilerTag, List<CustomPostProcessing> customPostProcessings) {
  17.             mProfilerTag = profilerTag;
  18.             mCustomPostProcessings = customPostProcessings;
  19.             mActiveCustomPostProcessingIndex = new List<int>(customPostProcessings.Count);
  20.             // 将自定义后措置器对象列表转换成一个性能采样器对象列表
  21.             mProfilingSamplers = customPostProcessings.Select(c => new ProfilingSampler(c.ToString())).ToList();
  22.             mTempRT0 = RTHandles.Alloc(mTempRT0Name, name: mTempRT0Name);
  23.             mTempRT1 = RTHandles.Alloc(mTempRT1Name, name: mTempRT1Name);
  24.         }
  25. }
复制代码
值得注意的是,在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的列表,代码如下:
  1. var derivedVolumeComponentTypes = VolumeManager.instance.baseComponentTypeArray;  
  2. 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。
代码如下:
  1. // 获取VolumeStack  
  2. var stack = VolumeManager.instance.stack;  
  3.   
  4. // 获取volumeStack中所有CustomPostProcessing实例  
  5. var customPostProcessings = VolumeManager.instance.baseComponentTypeArray  
  6.     .Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))) // 筛选出volumeStack中的CustomPostProcessing类型元素 非论是否在Volume中 非论是否激活  
  7.     .Select(t => stack.GetComponent(t) as CustomPostProcessing) // 将类型元素转换为实例  
  8.     .ToList(); // 转换为List
复制代码
具体代码

颠末上述分析,我们首先需要在Create()里抓取VolumeComponent的所有派生类实例。
CustomPostProcessingFeature.cs
  1. public override void Create() {
  2.         // 获取VolumeStack
  3.         var stack = VolumeManager.instance.stack;
  4.         // 获取所有的CustomPostProcessing实例
  5.         mCustomPostProcessings = VolumeManager.instance.baseComponentTypeArray
  6.                 .Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))) // 筛选出VolumeComponent派生类类型中所有的CustomPostProcessing类型元素 非论是否在Volume中 非论是否激活
  7.                 .Select(t => stack.GetComponent(t) as CustomPostProcessing) // 将类型元素转换为实例
  8.                 .ToList(); // 转换为List
  9. }
复制代码
下一步,对抓取到的所有CustomPostProcessing实例按照注入点分类,而且按照在注入点的挨次进行排序。再把对应CPPs实例传递给RenderPass实例,依据注入点分类通过renderPassEvent设置衬着时间。
  1.         // 初始化分歧插入点的render pass
  2.         // 找到在透明物和天空后衬着的CustomPostProcessing
  3.         var afterOpaqueAndSkyCPPs = mCustomPostProcessings
  4.                 .Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.AfterOpaqueAndSky) // 筛选出所有CustomPostProcessing类中注入点为透明物体和天空后的实例
  5.                 .OrderBy(c => c.OrderInInjectionPoint) // 按照挨次排序
  6.                 .ToList(); // 转换为List
  7.         // 创建CustomPostProcessingPass类
  8.         mAfterOpaqueAndSkyPass = new CustomPostProcessingPass(”Custom PostProcess after Skybox”, afterOpaqueAndSkyCPPs);
  9.         // 设置Pass执行时间
  10.         mAfterOpaqueAndSkyPass.renderPassEvent = RenderPassEvent.AfterRenderingSkybox;
  11.         var beforePostProcessingCPPs = mCustomPostProcessings
  12.                 .Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.BeforePostProcess)
  13.                 .OrderBy(c => c.OrderInInjectionPoint)
  14.                 .ToList();
  15.         mBeforePostProcessPass = new CustomPostProcessingPass(”Custom PostProcess before PostProcess”, beforePostProcessingCPPs);
  16.         mBeforePostProcessPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
  17.         var afterPostProcessCPPs = mCustomPostProcessings
  18.                 .Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.AfterPostProcess)
  19.                 .OrderBy(c => c.OrderInInjectionPoint)
  20.                 .ToList();
  21.         mAfterPostProcessPass = new CustomPostProcessingPass(”Custom PostProcess after PostProcessing”, afterPostProcessCPPs);
  22.         mAfterPostProcessPass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
复制代码
抓取完CustomPostProcessing实例后,我们需要在Dispose里全部释放他们。
CustomPostProcessingFeature.cs
  1. protected override void Dispose(bool disposing) {
  2.         base.Dispose(disposing);
  3.         if (disposing && mCustomPostProcessings != null) {
  4.                 foreach (var item in mCustomPostProcessings) {
  5.                         item.Dispose();
  6.                 }
  7.         }
  8. }
复制代码
注入Pass

前面提到,通过VolumeManager抓取是所有VolumeComponent的派生类,不管是否在Volume中,所以我们还需要在RenderPass中判断杂交的CustomPostProcessing是否是激活状态。
CustomPostProcessing.cs
  1. // 获取active的CPPs下标,并返回是否存在有效组件
  2. public bool SetupCustomPostProcessing() {
  3.         mActiveCustomPostProcessingIndex.Clear();
  4.         for (int i = 0; i < mCustomPostProcessings.Count; i++) {
  5.                 mCustomPostProcessings[i].Setup();
  6.                 if (mCustomPostProcessings[i].IsActive()) {
  7.                         mActiveCustomPostProcessingIndex.Add(i);
  8.                 }
  9.         }
  10.         return mActiveCustomPostProcessingIndex.Count != 0;
  11. }
复制代码
下面完成RenderFeature的第二个任务,将RenderPass EnqueuePass。它在AddRenderPasses函数中进行,我们将分歧的注入点的RenderPass注入到renderer中。
CustomRenderFeature.cs
  1. // 当为每个摄像机设置一个衬着器时,调用此方式
  2. // 将分歧注入点的RenderPass注入到renderer中
  3. public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
  4.         // 当前衬着的游戏相机撑持后措置
  5.         if (renderingData.cameraData.postProcessEnabled) {
  6.                 // 为每个render pass设置RT
  7.                 // 而且将pass列表加到renderer中
  8.                 if (mAfterOpaqueAndSkyPass.SetupCustomPostProcessing()) {
  9.                         mAfterOpaqueAndSkyPass.ConfigureInput(ScriptableRenderPassInput.Color);
  10.                         renderer.EnqueuePass(mAfterOpaqueAndSkyPass);
  11.                 }
  12.                
  13.                 if (mBeforePostProcessPass.SetupCustomPostProcessing()) {
  14.                         mBeforePostProcessPass.ConfigureInput(ScriptableRenderPassInput.Color);
  15.                         renderer.EnqueuePass(mBeforePostProcessPass);
  16.                 }
  17.                
  18.                 if (mAfterPostProcessPass.SetupCustomPostProcessing()) {
  19.                         mAfterPostProcessPass.ConfigureInput(ScriptableRenderPassInput.Color);
  20.                         renderer.EnqueuePass(mAfterPostProcessPass);
  21.                 }
  22.         }
  23. }
复制代码
网上有些资料在这个函数里配置RenderPass的源RT和方针RT,具体来说使用类似RenderPass.Setup(renderer.cameraColorTargetHandle, renderer.cameraColorTargetHandle)的方式,但是这在URP14.0中会报错,提示renderer.cameraColorTargetHandle只能在ScriptableRenderPass子类里调用。具体细节可以查看最后的参考连接。

下面再讨论一种错误做法。转到RenderPass中,在OnCameraSetup重载函数中设置当前RenderPass的源RT和方针RT,它将在相机衬着前被调用。
  1. public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {
  2.         RenderTextureDescriptor blitTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
  3.         blitTargetDescriptor.depthBufferBits = 0;
  4.         var renderer = renderingData.cameraData.renderer;
  5.         // 源RT固定为相机的颜色RT ”_CameraColorAttachmentA”
  6.         mSourceRT = renderer.cameraColorTargetHandle;
  7.         mDesRT = renderer.cameraColorTargetHandle;
  8. }
复制代码
这会带来什么错误呢?如果注入点非后措置后,则相机源和方针应该为_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中。

完整代码如下:
  1. // 实现衬着逻辑
  2. public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
  3.         // 初始化commandbuffer
  4.         var cmd = CommandBufferPool.Get(mProfilerTag);
  5.         context.ExecuteCommandBuffer(cmd);
  6.         cmd.Clear();
  7.         // 获取相机Descriptor
  8.         var descriptor = renderingData.cameraData.cameraTargetDescriptor;
  9.         descriptor.msaaSamples = 1;
  10.         descriptor.depthBufferBits = 0;
  11.         // 初始化临时RT
  12.         bool rt1Used = false;
  13.         // 设置源和方针RT为本次衬着的RT 在Execute里进行 特殊措置后措置后注入点
  14.         mDesRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
  15.         mSourceRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
  16.         // 声明temp0临时纹理
  17.         // cmd.GetTemporaryRT(Shader.PropertyToID(mTempRT0.name), descriptor);
  18.         // mTempRT0 = RTHandles.Alloc(mTempRT0.name);
  19.         RenderingUtils.ReAllocateIfNeeded(ref mTempRT0, descriptor, name: mTempRT0Name);
  20.         // 执行每个组件的Render方式
  21.         if (mActiveCustomPostProcessingIndex.Count == 1) {
  22.                 int index = mActiveCustomPostProcessingIndex[0];
  23.                 using (new ProfilingScope(cmd, mProfilingSamplers[index])) {
  24.                         mCustomPostProcessings[index].Render(cmd, ref renderingData, mSourceRT, mTempRT0);
  25.                 }
  26.         }
  27.         else {
  28.                 // 如果有多个组件,则在两个RT上来回bilt
  29.                 RenderingUtils.ReAllocateIfNeeded(ref mTempRT1, descriptor, name: mTempRT1Name);
  30.                 rt1Used = true;
  31.                 Blit(cmd, mSourceRT, mTempRT0);
  32.                 for (int i = 0; i < mActiveCustomPostProcessingIndex.Count; i++) {
  33.                         int index = mActiveCustomPostProcessingIndex[i];
  34.                         var customProcessing = mCustomPostProcessings[index];
  35.                         using (new ProfilingScope(cmd, mProfilingSamplers[index])) {
  36.                                 customProcessing.Render(cmd, ref renderingData, mTempRT0, mTempRT1);
  37.                         }
  38.                         CoreUtils.Swap(ref mTempRT0, ref mTempRT1);
  39.                 }
  40.         }
  41.        
  42.         Blitter.BlitCameraTexture(cmd, mTempRT0, mDesRT);
  43.         // 释放
  44.         cmd.ReleaseTemporaryRT(Shader.PropertyToID(mTempRT0.name));
  45.         if (rt1Used) cmd.ReleaseTemporaryRT(Shader.PropertyToID(mTempRT1.name));
  46.         context.ExecuteCommandBuffer(cmd);
  47.         CommandBufferPool.Release(cmd);
  48. }
复制代码
例子:ColorBlit

在这之前,注意把CustomPostProcessingFeature添加到Renderer中。



下面把官方文档里的ColorBlit例子改为我们CPP系统能用的脚本。
ColorBlit.cs

C#很简单,首先创建一个挂载ColorBlit.shader的材质,然后在Render()函数中调用Blit从sourceRT调用材质的Shader衬着给destinationRT。
需要注意的是,我们的RenderFeature抓取的CusomProcessing是全部基于VolumenComponent的派生类,而不是当前场景Global Volume组件里的后措置。所以为了RenderPass能够判断当前后措置是否有效,最好给每个后措置IsActive()除了判断材质非空外加上一个属性的约束。
  1. using CPP;
  2. using UnityEngine;
  3. using UnityEngine.Rendering;
  4. using UnityEngine.Rendering.Universal;
  5. namespace CPP.EFFECTS{
  6.     [VolumeComponentMenu(”Custom Post-processing/Color Blit”)]
  7.     public class ColorBlit : CustomPostProcessing{
  8.         public ClampedFloatParameter intensity = new(0.0f, 0.0f, 2.0f);
  9.         private Material mMaterial;
  10.         private const string mShaderName = ”Hidden/PostProcess/ColorBlit”;
  11.         public override bool IsActive() => mMaterial != null && intensity.value > 0;
  12.         public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterOpaqueAndSky;
  13.         public override int OrderInInjectionPoint => 0;
  14.         public override void Setup() {
  15.             if (mMaterial == null)
  16.                 mMaterial = CoreUtils.CreateEngineMaterial(mShaderName);
  17.         }
  18.         public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
  19.             if (mMaterial == null) return;
  20.             mMaterial.SetFloat(”_Intensity”, intensity.value);
  21.             cmd.Blit(source, destination, mMaterial, 0);
  22.         }
  23.         public override void Dispose(bool disposing) {
  24.             base.Dispose(disposing);
  25.             CoreUtils.Destroy(mMaterial);
  26.         }
  27.     }
  28. }
复制代码
PostProcessing.hlsl

在官方文档中,对相机RT的采样用的是对_BlitTexture的SAMPLE_TEXTURE2D_X。_BlitTexture在Blit.hlsl库中声明。但是使用Blit函数衬着到的相机纹理是_MainTex(不知道是不是API调用的原因)。而且,_MainTex必需在Properties中定义。
这里仿照官方提供的Blit.hlsl,将公用的声明和常用函数放到Common/PostProcessing.hlsl中。
  1. #ifndef POSTPROCESSING_INCLUDED
  2. #define POSTPROCESSING_INCLUDED
  3. #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
  4. TEXTURE2D(_MainTex);
  5. SAMPLER(sampler_MainTex);
  6. TEXTURE2D(_CameraDepthTexture);
  7. SAMPLER(sampler_CameraDepthTexture);
  8. struct Attributes {
  9.     float4 positionOS : POSITION;
  10.     float2 uv : TEXCOORD0;
  11. };
  12. struct Varyings {
  13.     float2 uv : TEXCOORD0;
  14.     float4 vertex : SV_POSITION;
  15.     UNITY_VERTEX_OUTPUT_STEREO
  16. };
  17. half4 SampleSourceTexture(float2 uv) {
  18.     return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
  19. }
  20. half4 SampleSourceTexture(Varyings input) {
  21.     return SampleSourceTexture(input.uv);
  22. }
  23. Varyings Vert(Attributes input) {
  24.     Varyings output = (Varyings)0;
  25.     // 分配instance id
  26.     UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
  27.     VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
  28.     output.vertex = vertexInput.positionCS;
  29.     output.uv = input.uv;
  30.     return output;
  31. }
  32. #endif
复制代码
ColorBlit.shader
  1. Shader ”Hidden/PostProcess/ColorBlit” {
  2.     Properties {
  3.         // 显式声明出来_MainTex
  4.         [HideInInspector]_MainTex (”Base (RGB)”, 2D) = ”white” {}
  5.     }
  6.     SubShader {
  7.         Tags {
  8.             ”RenderType”=”Opaque”
  9.             ”RenderPipeline” = ”UniversalPipeline”
  10.         }
  11.         LOD 200
  12.         Pass {
  13.             Name ”ColorBlitPass”
  14.             HLSLPROGRAM
  15.             #include ”Common/PostProcessing.hlsl”
  16.             #pragma vertex Vert
  17.             #pragma fragment frag
  18.             float _Intensity;
  19.             half4 frag(Varyings input) : SV_Target {
  20.                 float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
  21.                 return color * float4(0, _Intensity, 0, 1);
  22.             }
  23.             ENDHLSL
  24.         }
  25.     }
  26. }
复制代码
例子:ColorAdjustment

最基本的色彩调整后措置。
ColorAdjustment.cs
  1. using UnityEngine;
  2. using UnityEngine.Rendering;
  3. using UnityEngine.Rendering.Universal;
  4. using CPP;
  5. namespace CPP.EFFECTS{
  6.     [VolumeComponentMenu(”Custom Post-processing/Color Adjusment”)]
  7.     public class ColorAdjustments : CustomPostProcessing{
  8.         #region Parameters Define
  9.         // 后曝光
  10.         public FloatParameter postExposure = new FloatParameter(0.0f);
  11.         // 对比度
  12.         public ClampedFloatParameter contrast = new ClampedFloatParameter(0.0f, 0.0f, 100.0f);
  13.         // 颜色滤镜
  14.         public ColorParameter colorFilter = new ColorParameter(Color.white, true, false, false);
  15.         // 色相偏移
  16.         public ClampedFloatParameter hueShift = new ClampedFloatParameter(0.0f, -180.0f, 180.0f);
  17.         // 饱和度
  18.         public ClampedFloatParameter saturation = new ClampedFloatParameter(0.0f, -100.0f, 100.0f);
  19.         #endregion
  20.         private Material mMaterial;
  21.         private const string mShaderName = ”Hidden/PostProcess/ColorAdjusments”;
  22.         #region Active State Check
  23.         public override bool IsActive() =>
  24.             mMaterial != null && (IsPostExposureActive() || IsContrastActive() || IsContrastActive() || IsColorFilterActive() || IsHueShiftActive() || IsSaturationActive());
  25.         private bool IsPostExposureActive() => postExposure.value != 0.0f;
  26.         private bool IsContrastActive() => contrast.value != 0.0f;
  27.         private bool IsColorFilterActive() => colorFilter.value != Color.white;
  28.         private bool IsHueShiftActive() => hueShift.value != 0.0f;
  29.         private bool IsSaturationActive() => saturation.value != 0.0f;
  30.         #endregion
  31.         public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;
  32.         public override int OrderInInjectionPoint => 99;
  33.         private int mColorAdjustmentsId = Shader.PropertyToID(”_ColorAdjustments”),
  34.             mColorFilterId = Shader.PropertyToID(”_ColorFilter”);
  35.         private const string mExposureKeyword = ”EXPOSURE”,
  36.             mContrastKeyword = ”CONTRAST”,
  37.             mHueShiftKeyword = ”HUE_SHIFT”,
  38.             mSaturationKeyword = ”SATURATION”,
  39.             mColorFilterKeyword = ”COLOR_FILTER”;
  40.         public override void Setup() {
  41.             if (mMaterial == null)
  42.                 mMaterial = CoreUtils.CreateEngineMaterial(mShaderName);
  43.         }
  44.         public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
  45.             if (mMaterial == null) return;
  46.             Vector4 colorAdjustmentsVector4 = new Vector4(
  47.                 Mathf.Pow(2f, postExposure.value), // 曝光度 曝光单元是2的幂次
  48.                 contrast.value * 0.01f + 1f, // 对比度 将范围从[-100, 100]映射到[0, 2]
  49.                 hueShift.value * (1.0f / 360.0f), // 色相偏移 将范围从[-180, 180]转换到[-0.5, 0.5]
  50.                 saturation.value * 0.01f + 1.0f); // 饱和度 将范围从[-100, 100]转换到[0, 2]
  51.             mMaterial.SetVector(mColorAdjustmentsId, colorAdjustmentsVector4);
  52.             mMaterial.SetColor(mColorFilterId, colorFilter.value);
  53.             // 按照是否激活对应调整设置keyword
  54.             SetKeyWord(mExposureKeyword, IsPostExposureActive());
  55.             SetKeyWord(mContrastKeyword, IsContrastActive());
  56.             SetKeyWord(mHueShiftKeyword, IsHueShiftActive());
  57.             SetKeyWord(mSaturationKeyword, IsSaturationActive());
  58.             SetKeyWord(mColorFilterKeyword, IsColorFilterActive());
  59.             cmd.Blit(source, destination, mMaterial, 0);
  60.         }
  61.         private void SetKeyWord(string keyword, bool enabled = true) {
  62.             if (enabled) mMaterial.EnableKeyword(keyword);
  63.             else mMaterial.DisableKeyword(keyword);
  64.         }
  65.         public override void Dispose(bool disposing) {
  66.             base.Dispose(disposing);
  67.             CoreUtils.Destroy(mMaterial);
  68.         }
  69.     }
  70. }
复制代码
ColorAdjustment.shader
  1. Shader ”Hidden/PostProcess/ColorAdjusments” {
  2.     Properties {
  3.         [HideInInspector] _MainTex (”Base (RGB)”, 2D) = ”white” {}
  4.     }
  5.     SubShader {
  6.         Tags {
  7.             ”RenderType” = ”Opaque”
  8.             ”RenderPipeline” = ”UniversalPipeline”
  9.         }
  10.         LOD 200
  11.         Pass {
  12.             name ”ColorAdjustmentPass”
  13.             HLSLPROGRAM
  14.             #include ”Common/PostProcessing.hlsl”
  15.             #include ”Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl”
  16.             #pragma vertex Vert
  17.             #pragma fragment frag
  18.             #pragma shader_feature EXPOSURE
  19.             #pragma shader_feature CONTRAST
  20.             #pragma shader_feature COLOR_FILTER
  21.             #pragma shader_feature HUE_SHIFT
  22.             #pragma shader_feature SATURATION
  23.             // 曝光度 对比度 色相偏移 饱和度
  24.             float4 _ColorAdjustments;
  25.             float4 _ColorFilter;
  26.             // 后曝光
  27.             half3 ColorAdjustmentExposure(half3 color) {
  28.                 return color * _ColorAdjustments.x;
  29.             }
  30.             // 对比度
  31.             half3 ColorAdjustmentContrast(float3 color) {
  32.                 // 为了更好的效果 将颜色从线性空间转换到logC空间(因为要取美术中灰)
  33.                 color = LinearToLogC(color);
  34.                 // 从颜色中减去均匀的中间灰度,然后通过对比度进行缩放,然后在中间添加中间灰度
  35.                 color = (color - ACEScc_MIDGRAY) * _ColorAdjustments.y + ACEScc_MIDGRAY;
  36.                 return LogCToLinear(color);
  37.             }
  38.             // 颜色滤镜
  39.             half3 ColorAdjustmentColorFilter(float3 color) {
  40.                 color = SRGBToLinear(color);
  41.                 color = color * _ColorFilter.rgb;
  42.                 return color;
  43.             }
  44.             // 色相偏移
  45.             half3 ColorAdjustmentHueShift(half3 color) {
  46.                 // 将颜色格式从rgb转换为hsv
  47.                 color = RgbToHsv(color);
  48.                 // 将色相偏移添加到h
  49.                 float hue = color.x + _ColorAdjustments.z;
  50.                 // 如果色相超出范围 将其截断
  51.                 color.x = RotateHue(hue, 0.0, 1.0);
  52.                 // 将颜色格式从hsv转换为rgb
  53.                 return HsvToRgb(color);
  54.             }
  55.             // 饱和度
  56.             half3 ColorAdjustmentSaturation(half3 color) {
  57.                 // 获取颜色的亮度
  58.                 float luminance = Luminance(color);
  59.                 // 从颜色中减去亮度,然后通过饱和度进行缩放,然后在中间添加亮度
  60.                 return (color - luminance) * _ColorAdjustments.w + luminance;
  61.             }
  62.             half3 ColorAdjustment(half3 color) {
  63.                 // 防止颜色值过大的潜在隐患
  64.                 color = min(color, 60.0);
  65.                 // 后曝光
  66.                 #ifdef EXPOSURE
  67.                 color = ColorAdjustmentExposure(color);
  68.                 #endif
  69.                 // 对比度
  70.                 #ifdef CONTRAST
  71.                 color = ColorAdjustmentContrast(color);
  72.                 #endif
  73.                 // 颜色滤镜
  74.                 #ifdef COLOR_FILTER
  75.                 color = ColorAdjustmentColorFilter(color);
  76.                 #endif
  77.                 // 当对比度增加时,会导致颜色分量变暗,在这之后将颜色钳位
  78.                 color = max(color, 0.0);
  79.                 // 色相偏移
  80.                 #ifdef HUE_SHIFT
  81.                 color = ColorAdjustmentHueShift(color);
  82.                 #endif
  83.                 // 饱和度
  84.                 #ifdef SATURATION
  85.                 color = ColorAdjustmentSaturation(color);
  86.                 #endif
  87.                 // 当饱和度增加时,可能发生负数,在这之后将颜色钳位
  88.                 return max(color, 0.0);
  89.                 return color;
  90.             }
  91.             half4 frag(Varyings input) : SV_Target {
  92.                 half3 color = SampleSourceTexture(input).xyz;
  93.                 half3 finalCol = ColorAdjustment(color);
  94.                 return half4(finalCol, 1.0);
  95.             }
  96.             ENDHLSL
  97.         }
  98.     }
  99. }
复制代码
其他

首先,编纂模式下。切换场景,Scene和Game视图相机里的后措置并不会刷新,此时先封锁Renderer的CustomPostProcessingFeature组件再打开,后措置就刷新了。
但是,在播放模式下,通过场景切换脚本,后措置还是可以刷新的。
目前还没找到编纂模式下直接刷新的方式,以后找到了再更新一下。

此外,衬着的性能也没怎么优化,例如Blit替换为全屏三角形等,后面再更新。

先把这套用一用再更新一些细节。
更新:绘制法式化全屏三角形

在之前的衬着方式中,我们使用带材质参数的Blit函数,通过查看frame debugger,可以明确它实际上绘制了一个四边形。





然而,我们使用不带有材质参数的Blit时,它会调用CoreBlit.shader,而且只绘制一个三角形。这使得顶点和索引数量减少,带来的消耗必定更少了。


所以我们的目的就是放弃使用带材质参数的Blit函数,而是自定义一个函数来使用绘制法式化三角形的方式进行衬着。

在后措置基类中,新建一个Draw函数,它接收CommandBuffer,源、方针RT以及调用pass索引。绘制法式如下:将GPU端的”_SourceTexture”纹理设置为sourceRT,将RenderTarget设置为destination,绘制法式化三角形。
CustomPostProcessing.cs
  1.         // 材质声明
  2.         protected Material mMaterial = null;
  3.         private Material mCopyMaterial = null;
  4.         private const string mCopyShaderName = ”Hidden/PostProcess/PostProcessCopy”;
  5.         protected override void OnEnable() {
  6.                 base.OnEnable();
  7.                 if (mCopyMaterial == null) {
  8.                         mCopyMaterial = CoreUtils.CreateEngineMaterial(mCopyShaderName);
  9.                 }
  10.                
  11.         #region Draw Function
  12.         private int mSourceTextureId = Shader.PropertyToID(”_SourceTexture”);
  13.         public virtual void Draw(CommandBuffer cmd, in RTHandle source, in RTHandle destination, int pass = -1) {
  14.                 // 将GPU端_SourceTexture设置为source
  15.                 cmd.SetGlobalTexture(mSourceTextureId, source);
  16.                 // 将RT设置为destination 不关心初始状态(直接填充) 需要存储
  17.                 cmd.SetRenderTarget(destination, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
  18.                 // 绘制法式化三角形
  19.                 if (pass == -1 || mMaterial == null)
  20.                         cmd.DrawProcedural(Matrix4x4.identity, mCopyMaterial, 0, MeshTopology.Triangles, 3);
  21.                 else
  22.                         cmd.DrawProcedural(Matrix4x4.identity, mMaterial, pass, MeshTopology.Triangles, 3);
  23.         }
  24.         #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
  1. struct ScreenSpaceData {
  2.     float4 positionCS;
  3.     float2 uv;
  4. };
  5. ScreenSpaceData GetScreenSpaceData(uint vertexID : SV_VertexID) {
  6.     ScreenSpaceData output;
  7.     // 按照id判断三角形顶点的坐标
  8.     // 坐标挨次为(-1, -1) (-1, 3) (3, -1)
  9.     output.positionCS = float4(vertexID <= 1 ? -1.0 : 3.0, vertexID == 1 ? 3.0 : -1.0, 0.0, 1.0);
  10.     output.uv = float2(vertexID <= 1 ? 0.0 : 2.0, vertexID == 1 ? 2.0 : 0.0);
  11.     // 分歧API可能会发生倒置的情况 进行判断
  12.     if (_ProjectionParams.x < 0.0) {
  13.         output.uv.y = 1.0 - output.uv.y;
  14.     }
  15.     return output;
  16. }
  17. Varyings Vert(uint vertexID : SV_VertexID) {
  18.     Varyings output;
  19.     ScreenSpaceData ssData = GetScreenSpaceData(vertexID);
  20.     output.positionCS = ssData.positionCS;
  21.     output.uv = ssData.uv;
  22.     return output;
  23. }
复制代码
然后再定义一些基本的功能。
  1. #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
  2. #include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl”
  3. TEXTURE2D(_SourceTexture);
  4. SAMPLER(sampler_SourceTexture);
  5. TEXTURE2D(_CameraDepthTexture);
  6. SAMPLER(sampler_CameraDepthTexture);
  7. struct Varyings {
  8.     float4 positionCS : SV_POSITION;
  9.     float2 uv : TEXCOORD0;
  10.     UNITY_VERTEX_OUTPUT_STEREO
  11. };
  12. half4 GetSource(float2 uv) {
  13.     return SAMPLE_TEXTURE2D(_SourceTexture, sampler_SourceTexture, uv);
  14. }
  15. half4 GetSource(Varyings input) {
  16.     return GetSource(input.uv);
  17. }
  18. float SampleDepth(float2 uv) {
  19.     #if defined(UNITY_STEREO_INSTANCING_ENABLED) || defined(UNITY_STEREO_MULTIVIEW_ENABLED)
  20.     return SAMPLE_TEXTURE2D_ARRAY(_CameraDepthTexture, sampler_CameraDepthTexture, uv, unity_StereoEyeIndex).r;
  21.     #else
  22.     return SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv);
  23.     #endif
  24. }
  25. float SampleDepth(Varyings input) {
  26.     return SampleDepth(input.uv);
  27. }
复制代码
注意,此时的后措置Shader需要封锁深度写入,封锁剔除,即:
  1.         ZWrite Off
  2.         Cull Off
复制代码
然后再写一个用了复制color buffer的Shader。
PostProcessCopy.shader
  1. Shader ”Hidden/PostProcess/PostProcessCopy” {
  2.     SubShader {
  3.         Tags {
  4.             ”RenderType”=”Opaque”
  5.             ”RenderPipeline” = ”UniversalPipeline”
  6.         }
  7.         LOD 200
  8.         ZWrite Off
  9.         Cull Off
  10.         HLSLINCLUDE
  11.         #include ”PostProcessing.hlsl”
  12.         ENDHLSL
  13.         Pass {
  14.             Name ”CopyPass”
  15.             HLSLPROGRAM
  16.             #pragma vertex Vert
  17.             #pragma fragment frag;
  18.             half4 frag(Varyings input) : SV_Target {
  19.                 half4 color = GetSource(input);
  20.                 return half4(color.rgb, 1.0);
  21.             }
  22.             ENDHLSL
  23.         }
  24.     }
  25. }
复制代码
最后,把自定义后措置的C#脚本中的Blit函数通通换成Draw函数。
  1. // cmd.Blit(source, destination, mMaterial, 0);
  2. Draw(cmd, source, destination, 0);
复制代码
更新
2023/8/23
改了临时纹理的声明和释放逻辑。
具体来说,在CustomPostProcessingPassOnCameraSetup()函数中分配本次主Pass的中间纹理,而且分袂该主Pass下的CustomPostProcessing实例的OnCameraSetup()函数,它们各自在此中分配本身的临时纹理。纹理的分配使用RenderingUtils.ReAllocateIfNeeded函数,猜测它不像CommandBuffer加命令那样可以让GPU按挨次执行分配纹理和之后的衬着逻辑,所以只能在正式衬着前将纹理全部门配,最后在统一释放。这也是Unity文档和我搜索资料的写法,有没有更好的方式我暂时没有看到。
最重要的是释放纹理,否则会直接死机。参考Unity文档的写法,在CustomPostProcessingFeatureDispose()函数中,首先调用各个主Pass实例和CustomPostProcessing实例的Dispose()函数,它们在此中分袂释放各自纹理。使用RTHandle.Release()函数。
2023/8/23
添加更多后措置效果,部门参考毛星云前辈的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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-8 09:33 , Processed in 0.106451 second(s), 29 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表