Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved Enabling Post Processing removes benefits from MSAA and Render Scale

Discussion in 'Universal Render Pipeline' started by FaithlessOne, Jul 17, 2023.

  1. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    257
    I am using Unity 2022.3.5 and URP 14.0.8 with a 2D renderer. I recently experimented with render settings to avoid some ugly pixel flickering effects in my 2D plattformer. I found out that using MSAA 2x and doubling the render pixel size (Render Scale 1.41) solves the flickering even at high camera speeds, so I was happy about it.

    But now I wanted to introduce Post Processing effects. But when only enabling the "Post Processing" option in the main camera without adding any concrete Post Processing effect the flickering is fully back. Disabling the "Post Processing" again causes the disappearance of the flickering again. For me it looks like enabling "Post Processing" suppresses MSAA and Render Scale. I have already read that MSAA and Render Scale settings are compatible with Post Processing effects, but this is something I cannot observe in my project. Do I miss something here? Are there settings which have to be made that all working in conjunction?

    I did all tests and observations in the standalone build (development build) and not in Editor playmode.
     
    goncalo-vasconcelos likes this.
  2. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    257
    I could reproduce the described effects of my original post in very simplistic test project. I tried various URP/2D render settings but there was no magical combination that got Post Processing and MSAA and Render Scale working together in the repro project or at least I did not found that combination. I then tried finding alternative ways. One vary obvious approach was using Camera Stacking while an overlay camera only enabled Post Processing while the level background was rendered by a camera without Post Processing. This did not work. When activating Post Processing on any camera in the camera Stack MSAA/Render Scale did not worked anymore. It is the same behavior when using two separate cameras instead of a Camera Stack.

    The next logical step was using two separate cameras, one for the level background and one for all other stuff including Post Processing and render the Post Processing stuff into a RenderTexture. The RenderTexture was then rendered to a Canvas/RawImage. This works! But it comes at a high price in my opinion. I made some performance tests using standalone build with bloom post processing effect on my system. Using a single camera with bloom I got up to 1150 FPS. Using the two camera approach with Post Processing with Render Texture on Canvas/RawImage dropped the FPS to 800. I have only very rudimentary knowledge about HLSL/Shaders and I implmented a shader with two passes cause of different blending modes to correctly apply the bloom effect from the RenderTexture to the Canvase/RawImage. The second pass costs 20-30 FPS. You can also imagine that the doubled pixel count cause of Render Scale 1.41 already costs performance. Now doing stuff in a separate RenderTexture using the same doubled pixel count additionally worses the performance. I tested a bit with the settings of the Render Texture, for example a lower size on the RenderTexture negating the Render Scale but this does caused a minor negative FPS effect. So I used parameters that seems to have the most beneficial performance.

    I also tried using Full Screen Pass Renderer Feature for rendering of the RenderTexture directly onto the screen without Canvas/RawImage to see if there are benefits regarding performance. But unfortunately there is a bug in URP 14.0.8 so this does currently not work. See https://forum.unity.com/threads/argumentnullexception-value-cannot-be-null.1442200/

    For sake of completness here is are settings of the RenderTexture and the implemented shader. To preserve the alpha values from Post Processing to RenderTexture I followed the steps in the following thread:
    upload_2023-7-19_11-1-59.png
    (Hardcoded the size to my screen size for the tests)
    (EDIT: I tested with colors and a bloom threshold below 1.0 so the specified format was sufficient, but when using colors and a bloom threshold above 1.0 then a HDR color format needs to be used like R16G16B16A16_SLFOAT)

    Code (CSharp):
    1. Shader "Custom/TransparentRenderTexture"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "Queue"="Transparent" "RenderType"="Transparent" }
    10.         LOD 100
    11.  
    12.         Pass
    13.         {
    14.             Blend SrcAlpha OneMinusSrcAlpha
    15.             ZWrite Off
    16.             Cull Back
    17.      
    18.             CGPROGRAM
    19.             #pragma vertex vert
    20.             #pragma fragment frag
    21.  
    22.             #include "UnityCG.cginc"
    23.  
    24.             struct appdata
    25.             {
    26.                 float4 vertex : POSITION;
    27.                 float2 uv : TEXCOORD0;
    28.             };
    29.  
    30.             struct v2f
    31.             {
    32.                 float2 uv : TEXCOORD0;
    33.                 float4 vertex : SV_POSITION;
    34.             };
    35.  
    36.             sampler2D _MainTex;
    37.             float4 _MainTex_ST;
    38.  
    39.             v2f vert (appdata v)
    40.             {
    41.                 v2f o;
    42.                 o.vertex = UnityObjectToClipPos(v.vertex);
    43.                 o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    44.                 return o;
    45.             }
    46.  
    47.             half4 frag (v2f i) : SV_Target
    48.             {
    49.                 half4 col = tex2D(_MainTex, i.uv);
    50.                 return col;
    51.             }      
    52.             ENDCG
    53.         }
    54.  
    55.         Pass
    56.         {
    57.             Blend SrcAlpha One
    58.             ZWrite Off
    59.             Cull Back
    60.      
    61.             CGPROGRAM
    62.             #pragma vertex vert
    63.             #pragma fragment frag
    64.  
    65.             #include "UnityCG.cginc"
    66.  
    67.             struct appdata
    68.             {
    69.                 float4 vertex : POSITION;
    70.                 float2 uv : TEXCOORD0;
    71.             };
    72.  
    73.             struct v2f
    74.             {
    75.                 float2 uv : TEXCOORD0;
    76.                 float4 vertex : SV_POSITION;
    77.             };
    78.  
    79.             sampler2D _MainTex;
    80.             float4 _MainTex_ST;
    81.  
    82.             v2f vert (appdata v)
    83.             {
    84.                 v2f o;
    85.                 o.vertex = UnityObjectToClipPos(v.vertex);
    86.                 o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    87.                 return o;
    88.             }
    89.  
    90.             half4 frag (v2f i) : SV_Target
    91.             {
    92.                 half4 col = tex2D(_MainTex, i.uv);
    93.  
    94.                 // EDIT: That is buggy and will cause loosing pixels
    95.                 //col.a = col.a == 0 ? 1.0 : 0.0;
    96.                 // EDIT: Replaced with:
    97.                 col.a = col.a < 1 ? 1.0 : 0.0;
    98.  
    99.                 return col;  
    100.             }      
    101.             ENDCG
    102.         }
    103.     }
    104. }

    I would appreciate if anyone has suggestions for improving performance on the described approach or having an alternative approach.
     
    Last edited: Jul 21, 2023
  3. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    257
    EDIT: Found a better approach than the one below described in my fifth post (https://forum.unity.com/threads/enabling-post-processing-removes-benefits-from-msaa-and-render-scale.1461779/#post-9181940)

    So, last update from me: I implemented the the two camera approach in my real project. I decided to use a
    ScriptableRendererFeature for rendering the post processed texture instead of a Canvas/RawImage. But this does not make a difference on FPS, still I personally find the approach cleaner directly rendering the Post Processing texture this way. To get that two work two different 2D renderer are required:
    upload_2023-7-20_21-9-49.png
    The MainRenderer2D is used for the main player camera, while the EffectsRender2D for the effects camera, which is a child of the main player camera using the same transform position and orthographic size to ensure it always has the same scene view. The EffectsRenderer2D uses the customized UberPost shader which allows preserving the alpha as mentionend in my last post:
    upload_2023-7-20_21-16-1.png
    upload_2023-7-20_21-17-31.png
    (The inspector has to be switched to Debug mode to show the shader list above)
    The SH_AlphaUberPost_Default shader can be constructed using the following thread. It should be constructed because the original UberPost.shader may change between URP versions even minor version ones:
    Also don't foget to compare/update the shader when URP versions change.

    The MainRender2D has the DetachedEffectsRenderPassFeature added:
    upload_2023-7-20_21-22-27.png
    The material M_EffectsRenderPass is using the shader code I posted in my previous post. This is the feature code:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Experimental.Rendering;
    3. using UnityEngine.Rendering;
    4. using UnityEngine.Rendering.Universal;
    5.  
    6. namespace My.Game.Scripts.Graphics
    7. {
    8.   /// <summary>
    9.   /// Render pass feature for rendering effects rendered by the effects camera into a effect render texture to the pipeline.
    10.   /// </summary>
    11.   /// <remarks>
    12.   /// This has been implemented for rendering Post Processing effects without harming
    13.   /// MSAA and render scale, because otherwise pixel flickering effects may re-occur.
    14.   /// </remarks>
    15.   public class DetachedEffectsRenderPassFeature : ScriptableRendererFeature
    16.   {
    17.     public Material Material;
    18.     private bool _isEnabled;
    19.     private bool _isReady;
    20.     private Camera _registeredEffectsCamera;
    21.  
    22.     private EffectsRenderPass _renderPass;
    23.     private RenderTexture _renderTexture;
    24.  
    25.     /// <inheritdoc />
    26.     public DetachedEffectsRenderPassFeature()
    27.     {
    28.       this._renderPass = new()
    29.       {
    30.         renderPassEvent = RenderPassEvent.AfterRendering,
    31.       };
    32.  
    33.       this.SetEnabled(false, true);
    34.     }
    35.  
    36.     /// <inheritdoc />
    37.     protected override void Dispose(bool disposing)
    38.     {
    39.       this.ReleaseEffectsTexture();
    40.       base.Dispose(disposing);
    41.     }
    42.  
    43.     /// <summary>
    44.     /// Gets or sets a value indicating whether the effects render pass feature is enabled or not.
    45.     /// </summary>
    46.     public bool IsEnabled
    47.     {
    48.       get => this._isEnabled;
    49.       set =>
    50.         // Force always because of issues how serialization and deserialization works on RenderPassFeatures
    51.         this.SetEnabled(value, true);
    52.     }
    53.  
    54.     // Here you can inject one or multiple render passes in the renderer.
    55.     // This method is called when setting up the renderer once per-camera.
    56.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    57.     {
    58.       if (this.IsEnabled && this._isReady)
    59.       {
    60.         renderer.EnqueuePass(this._renderPass);
    61.       }
    62.     }
    63.  
    64.     /// <inheritdoc />
    65.     public override void Create()
    66.     {
    67.       if (this.Material == null)
    68.       {
    69.         Debug.LogWarning($"No material specified for '{nameof(DetachedEffectsRenderPassFeature)}'.");
    70.         return;
    71.       }
    72.  
    73.       this._renderPass.Material = this.Material;
    74.       this._renderPass.RenderTexture = this._renderTexture;
    75.     }
    76.  
    77.     private void CreateEffectsTexture()
    78.     {
    79.       if (this._renderTexture != null)
    80.       {
    81.         if (this._registeredEffectsCamera)
    82.         {
    83.           this._registeredEffectsCamera.targetTexture = null;
    84.         }
    85.  
    86.         this._renderTexture.Release();
    87.       }
    88.  
    89.       // EDIT: Do not increase the Render Scale, because it will be increased automatically
    90.       //var renderScale = UniversalRenderPipeline.asset.renderScale;
    91.       //this._renderTexture = new RenderTexture(Mathf.CeilToInt(Screen.width * renderScale), Mathf.CeilToInt(Screen.height * renderScale),    
    92.       this._renderTexture = new RenderTexture(Screen.width, Screen.height,
    93. Mathf.CeilToInt(Screen.height),
    94.         GraphicsFormat.R16G16B16A16_SFloat, GraphicsFormat.None)
    95.       {
    96.         anisoLevel = 0,
    97.         filterMode = FilterMode.Point, // EDIT: When desried other filter modes could be used
    98.         wrapMode = TextureWrapMode.Clamp,
    99.         dimension = TextureDimension.Tex2D,
    100.         antiAliasing = 1, // EDIT: When desired MSAA can be used
    101.         useDynamicScale = false,
    102.         useMipMap = false,
    103.         autoGenerateMips = false,
    104.         enableRandomWrite = false,
    105.         depth = 0,
    106.         memorylessMode = RenderTextureMemoryless.Depth,
    107.       };
    108.  
    109.       // Assign textures and configure appropriately
    110.       this._renderPass.RenderTexture = this._renderTexture;
    111.       this.Material.mainTexture = this._renderTexture;
    112.  
    113.       // Update camera target texture
    114.       if (this._registeredEffectsCamera)
    115.       {
    116.         this._registeredEffectsCamera.targetTexture = this._renderTexture;
    117.       }
    118.  
    119.       this._isReady = true;
    120.     }
    121.  
    122.     /// <summary>
    123.     /// Registers a camera which records onto the effects texture.
    124.     /// </summary>
    125.     /// <param name="camera">The camera which records to the effects texture.</param>
    126.     public void RegisterTargetCamera(Camera camera)
    127.     {
    128.       if (this._registeredEffectsCamera != camera)
    129.       {
    130.         if (this._registeredEffectsCamera)
    131.         {
    132.           this._registeredEffectsCamera.targetTexture = null;
    133.         }
    134.  
    135.         this._registeredEffectsCamera = camera;
    136.  
    137.         if (this._renderTexture != null)
    138.         {
    139.           this._registeredEffectsCamera.targetTexture = this._renderTexture;
    140.         }
    141.         else
    142.         {
    143.           this._registeredEffectsCamera.targetTexture = null;
    144.         }
    145.       }
    146.     }
    147.  
    148.     private void ReleaseEffectsTexture()
    149.     {
    150.       if (this._isReady)
    151.       {
    152.         if (this._registeredEffectsCamera)
    153.         {
    154.           this._registeredEffectsCamera.targetTexture = null;
    155.         }
    156.  
    157.         this._renderTexture.Release();
    158.         this.Material.mainTexture = null;
    159.         this._renderPass.RenderTexture = null;
    160.         this._isReady = false;
    161.       }
    162.     }
    163.  
    164.     private void SetEnabled(bool isEnabled, bool forced)
    165.     {
    166.       if (this._isEnabled != isEnabled || forced)
    167.       {
    168.         if (isEnabled)
    169.         {
    170.           this.CreateEffectsTexture();
    171.         }
    172.         else
    173.         {
    174.           this.ReleaseEffectsTexture();
    175.         }
    176.  
    177.         this._isEnabled = isEnabled;
    178.       }
    179.     }
    180.  
    181.     private class EffectsRenderPass : ScriptableRenderPass
    182.     {
    183.       public Material Material;
    184.  
    185.       public RenderTexture RenderTexture;
    186.       private RenderTargetIdentifier _currentTarget;
    187.  
    188.       /// <inheritdoc />
    189.       public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    190.       {
    191.       }
    192.  
    193.       // Here you can implement the rendering logic.
    194.       // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
    195.       // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
    196.       // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
    197.       public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    198.       {
    199.         // Skip rendering if the render texture is null or not created
    200.         if (this.RenderTexture == null || !this.RenderTexture.IsCreated() || this.Material == null)
    201.         {
    202.           return;
    203.         }
    204.  
    205.         var cmd = CommandBufferPool.Get("RenderDetachedPostProcessingEffects");
    206.         cmd.Blit(this.RenderTexture, renderingData.cameraData.renderer.cameraColorTargetHandle, this.Material);
    207.         context.ExecuteCommandBuffer(cmd);
    208.         CommandBufferPool.Release(cmd);
    209.       }
    210.  
    211.       // Cleanup any allocated resources that were created during the execution of this render pass.
    212.       public override void OnCameraCleanup(CommandBuffer cmd)
    213.       {
    214.       }
    215.  
    216.       // This method is called before executing the render pass.
    217.       // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
    218.       // When empty this render pass will render to the active camera render target.
    219.       // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
    220.       // The render pipeline will ensure target setup and clearing happens in a performant manner.
    221.       public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    222.       {
    223.       }
    224.     }
    225.   }
    226. }

    Last but not least I implemented a GraphicsController-MonoBehavior for enabling/disabling the DetachedEffectsRenderPassFeature and adjusting the cameras:
    Code (CSharp):
    1. using System.Linq;
    2.  
    3. using Microsoft.Extensions.Logging;
    4.  
    5. using My.Game.Scripts.Dependencies;
    6. using My.Game.Scripts.Project;
    7.  
    8. using UnityEngine;
    9. using UnityEngine.Rendering;
    10. using UnityEngine.Rendering.Universal;
    11. using UnityEngine.Serialization;
    12.  
    13. using Zenject;
    14.  
    15. namespace My.Game.Scripts.Graphics
    16. {
    17.   public class GraphicsController : MonoBehaviour
    18.   {
    19.     [Tooltip("The effects renderer 2D used for rendering post processing effects only when configured.")]
    20.     public Renderer2DData EffectsRenderer2D = null!;
    21.  
    22.     [FormerlySerializedAs("IsSeparatingEffects")]
    23.     [Tooltip("The post processing effects are processed detached from the main camera.")]
    24.     public bool IsDetachedPostProcessingEffects;
    25.  
    26.     [Tooltip("The main renderer 2D used for rendering all kinds of game objects.")]
    27.     public Renderer2DData MainRenderer2D = null!;
    28.  
    29.     [Inject(Id = DependencyIdentifiers.SceneHierarchy.Cameras.Effects)]
    30.     private Camera _effectsCamera;
    31.  
    32.     [Inject]
    33.     private ILogger<GraphicsController> _logger;
    34.  
    35.     [Inject(Id = DependencyIdentifiers.SceneHierarchy.Cameras.Player)]
    36.     private Camera _playerCamera;
    37.  
    38.     private UniversalRenderPipelineAsset? _urpAsset;
    39.  
    40.     private void Start()
    41.     {
    42.       if (this.MainRenderer2D == null)
    43.       {
    44.         this._logger.LogWarning("No main renderer 2D set, '{Name}' not working correctly", nameof(GraphicsController));
    45.         return;
    46.       }
    47.  
    48.       if (this.EffectsRenderer2D == null)
    49.       {
    50.         this._logger.LogWarning("No effects renderer 2D set, '{Name}' not working correctly", nameof(GraphicsController));
    51.         return;
    52.       }
    53.  
    54.       var urpAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
    55.  
    56.       if (urpAsset != null)
    57.       {
    58.         this._urpAsset = urpAsset;
    59.         this.UpdateGraphicSettings();
    60.       }
    61.       else
    62.       {
    63.         this._logger.LogWarning("URP asset not found, '{Name}' not working correctly", nameof(GraphicsController));
    64.       }
    65.     }
    66.  
    67.     public void UpdateGraphicSettings()
    68.     {
    69.       if (this._urpAsset != null)
    70.       {
    71.         this.UpdateDetachedPostProcessingEffects();
    72.       }
    73.       else
    74.       {
    75.         this._logger.LogWarning("URP asset not found, graphic settings cannot be updated.");
    76.       }
    77.     }
    78.  
    79.     private void UpdateDetachedPostProcessingEffects()
    80.     {
    81.       var effectsRenderPassFeature = this.MainRenderer2D.rendererFeatures.OfType<DetachedEffectsRenderPassFeature>().Single();
    82.  
    83.       var isDetachedPostProcessingEffects = this.IsDetachedPostProcessingEffects;
    84.  
    85.       // When effect separation is enabled
    86.       if (isDetachedPostProcessingEffects)
    87.       {
    88.         unchecked
    89.         {
    90.           this._playerCamera.cullingMask = (int)(UnityUserLayers.All & ~UnityUserLayers.Effects);
    91.         }
    92.       }
    93.       else
    94.       {
    95.         unchecked
    96.         {
    97.           this._playerCamera.cullingMask = (int)UnityUserLayers.All;
    98.         }
    99.       }
    100.  
    101.       this._playerCamera.GetUniversalAdditionalCameraData().renderPostProcessing = !isDetachedPostProcessingEffects;
    102.  
    103.       this._logger.LogInformation("Setting enabled of effects render pass feature to '{EnabledState}'", isDetachedPostProcessingEffects);
    104.       this._effectsCamera.gameObject.SetActive(isDetachedPostProcessingEffects);
    105.       effectsRenderPassFeature.RegisterTargetCamera(this._effectsCamera);
    106.       effectsRenderPassFeature.IsEnabled = isDetachedPostProcessingEffects;
    107.     }
    108.   }
    109. }

    I made a performance test in my real project and the approach results in a drop from 530 to 450 FPS. This are roughly 15% loss, which I find acceptable. Without really knowing I guess the fewer the frames the lesser the impact of this approach. I make this technique a graphical quality setting in my game and assign them to the upper quality presets, so the player can decide whether to use it or not. So at the end I am satisfied with this solution, most importantly a clean camera movement quality can be achieved with the upper quality presets.
     
    Last edited: Jul 30, 2023
  4. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    257
    But one more post from me on this topic. I found another way of handling the pixel flickering: Introducing Motion Blur. Because the flickering only appears during camera movement in my 2D plattformer it is useful to mask the pixel flickering through bluring the full screen. I used the following settings which only introduced a subtle but noticable blur, but highly depends on camera movement speeds in general:
    upload_2023-7-21_16-41-23.png
    It really helps, but makes the overall image blurry and not only the causing sprites. Also it does not work on slow camera movements. And to be honest especially increasing Render Scale looks way better for the overall graphics than blurring. But I consider using it for the lower graphic quality presets.
     
  5. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    257
    Another more post from me :) Reason: I figured out a better solution than described in my third post:
    Reasons:
    • Approach is easier to implement, because no customization of UberPost.shader required and no custom shader required for bliting. Also no shader adjustments on URP updates needed.
    • Still two cameras necessary, but one camera can simply draw all required layers, no more splitting of layer rendering. So simply all game object can be rendered without causing sorting issues like in my previous approach which rendered effects last as overdraw.
    • Better FPS with increase from 320 to 340 FPS in my project
    So whats the approach: Using two cameras. The PlayerCamera renders all required layers as usual. It has post-processing disabled and renders to a RenderTexture instead of the display. It uses its own 2D renderer. A second DetachedPostProcessingCamera renders to the display. This camera has post-processing enabled, has a higher priority then the PlayerCamera, but is configured to render nothing (CullingMask). It uses its own 2D renderer and I used BackgroundType "Uninitialized", because content is fully overdrawn in opaque-like.
    upload_2023-7-30_17-53-0.png
    The FinalRenderer2D of the DetachedPostProcessingCamera has a render feature while the MainRenderer2D has nothing special:
    upload_2023-7-30_17-54-48.png
    The FinalRenderPassFeature simply renders the RenderTexture which is used by the PlayerCamera before post processing happens:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Experimental.Rendering;
    3. using UnityEngine.Rendering;
    4. using UnityEngine.Rendering.Universal;
    5.  
    6. namespace My.Game.Scripts.Graphics
    7. {
    8.   /// <summary>
    9.   /// Render pass feature for rendering the final camera displayed texture.
    10.   /// </summary>
    11.   /// <remarks>
    12.   /// This has been implemented for rendering Post Processing effects without harming
    13.   /// MSAA and render scale, because otherwise pixel flickering effects may re-occur.
    14.   /// </remarks>
    15.   public class FinalRenderPassFeature : ScriptableRendererFeature
    16.   {
    17.     private readonly RenderIntermediateTextureRenderPass _renderIntermediateTextureRenderPass;
    18.     private RenderTexture _intermediateTexture;
    19.     private bool _isEnabled;
    20.     private bool _isReady;
    21.     private Camera _registeredBaseCamera;
    22.  
    23.     /// <inheritdoc />
    24.     public FinalRenderPassFeature()
    25.     {
    26.       this._renderIntermediateTextureRenderPass = new()
    27.       {
    28.         renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing,
    29.       };
    30.  
    31.       this.SetEnabled(false, true);
    32.     }
    33.  
    34.     /// <inheritdoc />
    35.     protected override void Dispose(bool disposing)
    36.     {
    37.       this.ReleaseIntermediateTexture();
    38.       base.Dispose(disposing);
    39.     }
    40.  
    41.     /// <summary>
    42.     /// Gets or sets a value indicating whether the effects render pass feature is enabled or not.
    43.     /// </summary>
    44.     public bool IsEnabled
    45.     {
    46.       get => this._isEnabled;
    47.       set =>
    48.         // Force always because of issues how serialization and deserialization works on RenderPassFeatures
    49.         this.SetEnabled(value, true);
    50.     }
    51.  
    52.     // Here you can inject one or multiple render passes in the renderer.
    53.     // This method is called when setting up the renderer once per-camera.
    54.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    55.     {
    56.       if (this.IsEnabled && this._isReady)
    57.       {
    58.         renderer.EnqueuePass(this._renderIntermediateTextureRenderPass);
    59.       }
    60.     }
    61.  
    62.     /// <inheritdoc />
    63.     public override void Create()
    64.     {
    65.     }
    66.  
    67.     private void CreateIntermediateTexture()
    68.     {
    69.       if (this._intermediateTexture != null)
    70.       {
    71.         if (this._registeredBaseCamera)
    72.         {
    73.           this._registeredBaseCamera!.targetTexture = null;
    74.         }
    75.  
    76.         this._intermediateTexture.Release();
    77.       }
    78.  
    79.       var msaaSampleCount = UniversalRenderPipeline.asset.msaaSampleCount;
    80.  
    81.       // Render scale seems to be increased by another mechanism, so do not increase it manually
    82.       this._intermediateTexture = new RenderTexture(Screen.width, Screen.height,
    83.         GraphicsFormat.R16G16B16A16_SFloat, GraphicsFormat.None)
    84.       {
    85.         anisoLevel = 0,
    86.         // This increases effect quality, but may be undesired when having more layers rendered on the effects texture, because they get also filtered
    87.         filterMode = FilterMode.Point,
    88.         wrapMode = TextureWrapMode.Clamp,
    89.         dimension = TextureDimension.Tex2D,
    90.         antiAliasing = msaaSampleCount,
    91.         useDynamicScale = false,
    92.         useMipMap = false,
    93.         autoGenerateMips = false,
    94.         enableRandomWrite = false,
    95.         depth = 0,
    96.         memorylessMode = RenderTextureMemoryless.Depth,
    97.       };
    98.  
    99.       // Assign textures and configure appropriately
    100.       this._renderIntermediateTextureRenderPass.RenderTexture = this._intermediateTexture;
    101.  
    102.       // Update camera target texture
    103.       if (this._registeredBaseCamera)
    104.       {
    105.         this._registeredBaseCamera.targetTexture = this._intermediateTexture;
    106.       }
    107.  
    108.       this._isReady = true;
    109.     }
    110.  
    111.     /// <summary>
    112.     /// Registers a camera which records onto the texture to be processed texture.
    113.     /// </summary>
    114.     /// <param name="camera">The camera which records to the effects texture.</param>
    115.     public void RegisterBaseCamera(Camera camera)
    116.     {
    117.       if (this._registeredBaseCamera != camera)
    118.       {
    119.         if (this._registeredBaseCamera)
    120.         {
    121.           this._registeredBaseCamera.targetTexture = null;
    122.         }
    123.  
    124.         this._registeredBaseCamera = camera;
    125.  
    126.         if (this._intermediateTexture != null)
    127.         {
    128.           this._registeredBaseCamera.targetTexture = this._intermediateTexture;
    129.         }
    130.         else
    131.         {
    132.           this._registeredBaseCamera.targetTexture = null;
    133.         }
    134.       }
    135.     }
    136.  
    137.     private void ReleaseIntermediateTexture()
    138.     {
    139.       if (this._isReady)
    140.       {
    141.         if (this._registeredBaseCamera)
    142.         {
    143.           this._registeredBaseCamera.targetTexture = null;
    144.         }
    145.  
    146.         if (this._intermediateTexture != null)
    147.         {
    148.           this._intermediateTexture.Release();
    149.         }
    150.  
    151.         this._renderIntermediateTextureRenderPass.RenderTexture = null;
    152.         this._isReady = false;
    153.       }
    154.     }
    155.  
    156.     private void SetEnabled(bool isEnabled, bool forced)
    157.     {
    158.       if (this._isEnabled != isEnabled || forced)
    159.       {
    160.         if (isEnabled)
    161.         {
    162.           this.CreateIntermediateTexture();
    163.         }
    164.         else
    165.         {
    166.           this.ReleaseIntermediateTexture();
    167.         }
    168.  
    169.         this._isEnabled = isEnabled;
    170.       }
    171.     }
    172.  
    173.     private class RenderIntermediateTextureRenderPass : ScriptableRenderPass
    174.     {
    175.       public Material Material;
    176.  
    177.       public RenderTexture RenderTexture;
    178.       private RenderTargetIdentifier _currentTarget;
    179.  
    180.       /// <inheritdoc />
    181.       public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    182.       {
    183.       }
    184.  
    185.       // Here you can implement the rendering logic.
    186.       // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
    187.       // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
    188.       // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
    189.       public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    190.       {
    191.         // Skip rendering if the render texture is null or not created
    192.         if (this.RenderTexture == null || !this.RenderTexture.IsCreated())
    193.         {
    194.           return;
    195.         }
    196.  
    197.         var cmd = CommandBufferPool.Get("RenderDetachedPostProcessingEffects");
    198.         cmd.Blit(this.RenderTexture, renderingData.cameraData.renderer.cameraColorTargetHandle);
    199.         context.ExecuteCommandBuffer(cmd);
    200.         CommandBufferPool.Release(cmd);
    201.       }
    202.  
    203.       // Cleanup any allocated resources that were created during the execution of this render pass.
    204.       public override void OnCameraCleanup(CommandBuffer cmd)
    205.       {
    206.       }
    207.  
    208.       // This method is called before executing the render pass.
    209.       // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
    210.       // When empty this render pass will render to the active camera render target.
    211.       // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
    212.       // The render pipeline will ensure target setup and clearing happens in a performant manner.
    213.       public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    214.       {
    215.       }
    216.     }
    217.   }
    218. }
    To enable/disable the detached post-processing following code is sufficient:
    Code (CSharp):
    1. private void UpdatedPostProcessingEffects(in GraphicsOptions graphicsOptions)
    2. {
    3.   var finalRenderPassFeature = this.FinalRenderer2D.rendererFeatures.OfType<FinalRenderPassFeature>().Single();
    4.  
    5.   var isDetachedPostProcessingEffects = graphicsOptions.HasDetachedEffectsRendering;
    6.  
    7.   this._playerCamera.GetUniversalAdditionalCameraData().renderPostProcessing = !isDetachedPostProcessingEffects && graphicsOptions.HasPostProcessingEffects;
    8.  
    9.   this._logger.LogInformation("Setting enabled of effects render pass feature to '{EnabledState}'", isDetachedPostProcessingEffects);
    10.   this._detachedPostProcessingCamera.gameObject.SetActive(isDetachedPostProcessingEffects);
    11.   this._detachedPostProcessingCamera.GetUniversalAdditionalCameraData().renderPostProcessing = isDetachedPostProcessingEffects && graphicsOptions.HasPostProcessingEffects;
    12.   finalRenderPassFeature.RegisterBaseCamera(this._playerCamera);
    13.   finalRenderPassFeature.IsEnabled = isDetachedPostProcessingEffects;
    14. }