Search Unity

Resolved Migrating Screen Ripple Effect from "OnRenderImage()" to Universal Render Pipeline (URP)

Discussion in 'Universal Render Pipeline' started by Zethros, Sep 6, 2020.

  1. Zethros

    Zethros

    Joined:
    Jan 11, 2013
    Posts:
    12
    Synopsis: In Unity 2019.2.5f1, I had a functioning Screen Ripple Effect. I've upgraded to Unity 2020.1.3f1 to gain access to URP and its many post-processing effects, such as Vignette. However, this prevents the Ripple Effect from working. Turns out OnRenderImage() no longer works in Universal Render Pipeline (URP), in favor of the new Scriptable Render Pipeline (SRP). I need to learn how to migrate the OnRenderImage() code to URP/SRP equivalent code. After many hours of research, I have failed to find an answer, so I've come here to make a post about it.

    I think I almost have it. My current idea is that maybe the shader code needs to be adjusted to satisfy URP, because when I activate the Ripple Material, it just shows up as white, rather than having the proper distortion effect.

    Can you help me?

    Example of the Ripple Effect working:


    Background: I've worked mostly with 2D games for over 6 years. Proficient in C#, decent understanding of 2D art, but very poor understanding of Shaders and how graphics is drawn on the screen (draw passes?) in general.

    Ripple Effect code: Original source for the ripple effect is from Devon O. Wolfgang http://blog.onebyonedesign.com/unity/unity-ripple-or-shock-wave-effect/

    Ripple Shader:
    Code (Boo):
    1. Shader "Hidden/Ripple"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Texture", 2D) = "white" {}
    6.         _CenterX("Center X", float) = 300
    7.         _CenterY("Center Y", float) = 250
    8.         _Amount("Amount", float) = 25
    9.         _WaveSpeed("Wave Speed", range(.50, 50)) = 20
    10.         _WaveAmount("Wave Amount", range(0, 20)) = 10
    11.     }
    12.      
    13.     SubShader
    14.     {
    15.         // No culling or depth
    16.         Cull Off ZWrite Off ZTest Always
    17.  
    18.         Pass
    19.         {
    20.             CGPROGRAM
    21.             #pragma vertex vert
    22.             #pragma fragment frag
    23.  
    24.             #include "UnityCG.cginc"
    25.  
    26.             struct appdata
    27.             {
    28.                 float4 vertex : POSITION;
    29.                 float2 uv : TEXCOORD0;
    30.             };
    31.  
    32.             struct v2f
    33.             {
    34.                 float2 uv : TEXCOORD0;
    35.                 float4 vertex : SV_POSITION;
    36.             };
    37.  
    38.             v2f vert(appdata v)
    39.             {
    40.                 v2f o;
    41.                 o.vertex = UnityObjectToClipPos(v.vertex);
    42.                 o.uv = v.uv;
    43.                 return o;
    44.             }
    45.  
    46.             sampler2D _MainTex;
    47.             float _CenterX;
    48.             float _CenterY;
    49.             float _Amount;
    50.             float _WaveSpeed;
    51.             float _WaveAmount;
    52.  
    53.             fixed4 frag(v2f i) : SV_Target
    54.             {
    55.                 fixed2 center = fixed2(_CenterX / _ScreenParams.x, _CenterY / _ScreenParams.y);
    56.                 fixed time = _Time.y *  _WaveSpeed;
    57.                 fixed amt = _Amount / 1000;
    58.  
    59.                 fixed2 uv = center.xy - i.uv;
    60.                 uv.x *= _ScreenParams.x / _ScreenParams.y;
    61.  
    62.                 fixed dist = sqrt(dot(uv,uv));
    63.                 fixed ang = dist * _WaveAmount - time;
    64.                 uv = i.uv + normalize(uv) * sin(ang) * amt;
    65.  
    66.                 return tex2D(_MainTex, uv);
    67.             }
    68.             ENDCG
    69.         }
    70.     }
    71. }
    72.  

    Ripple Implementation (script placed on MainCamera GameObject):
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class RipplePostProcessor : MonoBehaviour
    4. {
    5.     public Material RippleMaterial;
    6.     public float MaxAmount = 50f;
    7.  
    8.     [Range(0, 1)]
    9.     public float Friction = .9f;
    10.  
    11.     private float Amount = 0f;
    12.     public bool ToggleRipple; // For in-editor test firing
    13.     public void Ripple()
    14.     {
    15.         Amount = MaxAmount;
    16.         Vector2 pos = new Vector2(Screen.width, Screen.height) / 2f;
    17.         RippleMaterial.SetFloat("_CenterX", pos.x);
    18.         RippleMaterial.SetFloat("_CenterY", pos.y);
    19.     }
    20.     void Update()
    21.     {
    22.         if (ToggleRipple)
    23.         {
    24.             ToggleRipple = false;
    25.             Ripple();
    26.         }
    27.         RippleMaterial.SetFloat("_Amount", Amount);
    28.         Amount *= Friction;
    29.     }
    30.     void OnRenderImage(RenderTexture src, RenderTexture dst)
    31.     {
    32.         Graphics.Blit(src, dst, RippleMaterial);
    33.     }
    34.  
    35.     private void OnApplicationQuit()
    36.     {
    37.         RippleMaterial.SetFloat("_Amount", 0);
    38.         RippleMaterial.SetFloat("_CenterX", 0);
    39.         RippleMaterial.SetFloat("_CenterY", 0);
    40.     }
    41. }

    Attempt to migrate to SRP:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3. using UnityEngine.Rendering.Universal;
    4.  
    5. public class CustomRenderPassFeature : ScriptableRendererFeature
    6. {
    7.     class CustomRenderPass : ScriptableRenderPass
    8.     {
    9.         // This method is called before executing the render pass.
    10.         // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
    11.         // When empty this render pass will render to the active camera render target.
    12.         // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
    13.         // The render pipeline will ensure target setup and clearing happens in an performance manner.
    14.         public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    15.         {
    16.             //if (Application.isPlaying && GameplayManager.ins?.RipplePostProcessor != null)
    17.             //{
    18.             //    cmd.Blit(BuiltinRenderTextureType.CameraTarget, BuiltinRenderTextureType.None, GameplayManager.ins.RipplePostProcessor.RippleMaterial);
    19.             //}
    20.         }
    21.  
    22.         // Here you can implement the rendering logic.
    23.         // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
    24.         // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
    25.         // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
    26.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    27.         {
    28.             CommandBuffer cmd = CommandBufferPool.Get();
    29.  
    30.             if (Application.isPlaying && GameplayManager.ins?.RipplePostProcessor.Amount > 0.01f)
    31.             {
    32.                 RenderTargetIdentifier src = BuiltinRenderTextureType.CameraTarget;
    33.                 RenderTargetIdentifier dst = BuiltinRenderTextureType.CurrentActive;
    34.                 cmd.Blit(src, dst, GameplayManager.ins.RipplePostProcessor.RippleMaterial);
    35.                 cmd.SetRenderTarget(dst);
    36.             }
    37.             // execution
    38.             context.ExecuteCommandBuffer(cmd);
    39.             CommandBufferPool.Release(cmd);
    40.         }
    41.  
    42.         /// Cleanup any allocated resources that were created during the execution of this render pass.
    43.         public override void FrameCleanup(CommandBuffer cmd)
    44.         {
    45.         }
    46.     }
    47.  
    48.     CustomRenderPass m_ScriptablePass;
    49.  
    50.     public RenderPassEvent TheRenderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
    51.  
    52.     public override void Create()
    53.     {
    54.         m_ScriptablePass = new CustomRenderPass();
    55.  
    56.         // Configures where the render pass should be injected.
    57.         m_ScriptablePass.renderPassEvent = TheRenderPassEvent;
    58.     }
    59.  
    60.     // Here you can inject one or multiple render passes in the renderer.
    61.     // This method is called when setting up the renderer once per-camera.
    62.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    63.     {
    64.         renderer.EnqueuePass(m_ScriptablePass);
    65.     }
    66. }

    Some of the places I've been, looking for an answer:
     
    Last edited: Sep 6, 2020
  2. Zethros

    Zethros

    Joined:
    Jan 11, 2013
    Posts:
    12
    Update: after 12 hours, I've finally solved the issue.

    The Ripple Shader did not need to be changed.

    The Custom Render Pass Feature got renamed and received changes:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3. using UnityEngine.Rendering.Universal;
    4.  
    5. public class RippleEffect_RenderPassFeature : ScriptableRendererFeature
    6. {
    7.     class RippleEffect_RenderPass : ScriptableRenderPass
    8.     {
    9.         static readonly int TempTargetId = Shader.PropertyToID("_Temp_RippleBlit"); // You can name this anything you want
    10.  
    11.         public RenderTargetIdentifier source;
    12.         public RenderTargetHandle destination;
    13.         public int blitShaderPassIndex;
    14.         public FilterMode filterMode;
    15.  
    16.         private RenderTargetHandle m_TemporaryColorTexture;
    17.  
    18.         public RippleEffect_RenderPass(RenderPassEvent renderPassEvent, FilterMode filterMode, int blitShaderPassIndex)
    19.         {
    20.             this.renderPassEvent = renderPassEvent;
    21.             this.filterMode = filterMode;
    22.             this.blitShaderPassIndex = blitShaderPassIndex;
    23.             m_TemporaryColorTexture.Init("_TemporaryColorTexture"); // You can name this anything you want
    24.         }
    25.  
    26.         // Here you can implement the rendering logic.
    27.         // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
    28.         // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
    29.         // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
    30.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    31.         {
    32.             CommandBuffer cmd = CommandBufferPool.Get();
    33.  
    34.             if (Application.isPlaying && GameplayManager.ins?.RipplePostProcessor.Amount > RipplePostProcessor.LOWEST_AMOUNT_VALUE)
    35.             {
    36.                 RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
    37.                 opaqueDesc.depthBufferBits = 0;
    38.  
    39.                 // Can't read and write to same color target, use a TemporaryRT
    40.                 if (destination == RenderTargetHandle.CameraTarget)
    41.                 {
    42.                     cmd.GetTemporaryRT(m_TemporaryColorTexture.id, opaqueDesc, filterMode);
    43.                     Blit(cmd, source, m_TemporaryColorTexture.Identifier(), GameplayManager.ins.RipplePostProcessor.RippleMaterial, blitShaderPassIndex);
    44.                     Blit(cmd, m_TemporaryColorTexture.Identifier(), source);
    45.                 }
    46.  
    47.             }
    48.             // execution
    49.             context.ExecuteCommandBuffer(cmd);
    50.             CommandBufferPool.Release(cmd);
    51.         }
    52.     }
    53.  
    54.     RippleEffect_RenderPass m_ScriptablePass;
    55.  
    56.     /// <summary>Configures where the render pass should be injected.</summary>
    57.     public FilterMode filterMode = FilterMode.Bilinear;
    58.     public RenderPassEvent TheRenderPassEvent = RenderPassEvent.AfterRenderingTransparents;
    59.     public int blitShaderPassIndex = -1;
    60.     public override void Create()
    61.     {
    62.         m_ScriptablePass = new RippleEffect_RenderPass(TheRenderPassEvent, filterMode, blitShaderPassIndex);
    63.     }
    64.  
    65.     // Here you can inject one or multiple render passes in the renderer.
    66.     // This method is called when setting up the renderer once per-camera.
    67.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    68.     {
    69.         m_ScriptablePass.source = renderer.cameraColorTarget;
    70.         m_ScriptablePass.destination = RenderTargetHandle.CameraTarget;
    71.         renderer.EnqueuePass(m_ScriptablePass);
    72.     }
    73. }

    Ripple Post Processor
    received some unrequired efficiency changes.
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class RipplePostProcessor : MonoBehaviour
    4. {
    5.     public const float LOWEST_AMOUNT_VALUE = 0.0001f;
    6.     public Material RippleMaterial;
    7.     public float MaxAmount = 5f;
    8.  
    9.     [Range(0, 1)]
    10.     public float Friction = .9f;
    11.  
    12.     public float Amount = 0f;
    13.     private bool _update = false;
    14.  
    15.     public void Ripple()
    16.     {
    17.         Amount = MaxAmount;
    18.         Vector2 pos = new Vector2(Screen.width, Screen.height) / 2f;
    19.         RippleMaterial.SetFloat("_CenterX", pos.x);
    20.         RippleMaterial.SetFloat("_CenterY", pos.y);
    21.         _update = true;
    22.     }
    23.     void Update()
    24.     {
    25.         if (_update)
    26.         {
    27.             RippleMaterial.SetFloat("_Amount", Amount);
    28.             Amount *= Friction;
    29.             if (Amount < LOWEST_AMOUNT_VALUE)
    30.             {
    31.                 _update = false;
    32.                 Amount = 0;
    33.                 RippleMaterial.SetFloat("_Amount", Amount);
    34.             }
    35.         }
    36.     }
    37.     //// Migrated to SRP
    38.     //void OnRenderImage(RenderTexture src, RenderTexture dst)
    39.     //{
    40.     //    Graphics.Blit(src, dst, RippleMaterial);
    41.     //}
    42.  
    43.     private void OnApplicationQuit()
    44.     {
    45.         RippleMaterial.SetFloat("_Amount", 0);
    46.         RippleMaterial.SetFloat("_CenterX", 0);
    47.         RippleMaterial.SetFloat("_CenterY", 0);
    48.     }
    49. }

    Special thanks to a blog post "Post Processing in the Universal RP" by "Cyan" (https://cyangamedev.wordpress.com/2020/06/22/urp-post-processing/). There is a Blit Render Feature that Cyan shares in the blog post, which provides the bit of code necessary to copy what the camera renders as a temporary Render Texture, which then gets used by the Ripple Material (using the Ripple Shader), and ultimately rendered. Note that this Blit Render Feature is based on Unity's Universal Rendering Examples, found on GitHub (https://github.com/Unity-Technologi...ee/master/Assets/Scripts/Runtime/RenderPasses). If any of these links ever become broken, here is a clone of that Blit Render Feature code:

    Blit Render Feature
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3. using UnityEngine.Rendering.Universal;
    4. // Saved in Blit.cs
    5. public class Blit : ScriptableRendererFeature {
    6.     public class BlitPass : ScriptableRenderPass {
    7.         public enum RenderTarget {
    8.             Color,
    9.             RenderTexture,
    10.         }
    11.         public Material blitMaterial = null;
    12.         public int blitShaderPassIndex = 0;
    13.         public FilterMode filterMode { get; set; }
    14.         private RenderTargetIdentifier source { get; set; }
    15.         private RenderTargetHandle destination { get; set; }
    16.         RenderTargetHandle m_TemporaryColorTexture;
    17.         string m_ProfilerTag;
    18.        
    19.         public BlitPass(RenderPassEvent renderPassEvent, Material blitMaterial, int blitShaderPassIndex, string tag) {
    20.             this.renderPassEvent = renderPassEvent;
    21.             this.blitMaterial = blitMaterial;
    22.             this.blitShaderPassIndex = blitShaderPassIndex;
    23.             m_ProfilerTag = tag;
    24.             m_TemporaryColorTexture.Init("_TemporaryColorTexture");
    25.         }
    26.        
    27.         public void Setup(RenderTargetIdentifier source, RenderTargetHandle destination) {
    28.             this.source = source;
    29.             this.destination = destination;
    30.         }
    31.        
    32.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
    33.             CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
    34.             RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
    35.             opaqueDesc.depthBufferBits = 0;
    36.             // Can't read and write to same color target, use a TemporaryRT
    37.             if (destination == RenderTargetHandle.CameraTarget) {
    38.                 cmd.GetTemporaryRT(m_TemporaryColorTexture.id, opaqueDesc, filterMode);
    39.                 Blit(cmd, source, m_TemporaryColorTexture.Identifier(), blitMaterial, blitShaderPassIndex);
    40.                 Blit(cmd, m_TemporaryColorTexture.Identifier(), source);
    41.             } else {
    42.                 Blit(cmd, source, destination.Identifier(), blitMaterial, blitShaderPassIndex);
    43.             }
    44.             context.ExecuteCommandBuffer(cmd);
    45.             CommandBufferPool.Release(cmd);
    46.         }
    47.        
    48.         public override void FrameCleanup(CommandBuffer cmd) {
    49.             if (destination == RenderTargetHandle.CameraTarget)
    50.                 cmd.ReleaseTemporaryRT(m_TemporaryColorTexture.id);
    51.         }
    52.     }
    53.     [System.Serializable]
    54.     public class BlitSettings {
    55.         public RenderPassEvent Event = RenderPassEvent.AfterRenderingOpaques;
    56.         public Material blitMaterial = null;
    57.         public int blitMaterialPassIndex = -1;
    58.         public Target destination = Target.Color;
    59.         public string textureId = "_BlitPassTexture";
    60.     }
    61.     public enum Target {
    62.         Color,
    63.         Texture
    64.     }
    65.     public BlitSettings settings = new BlitSettings();
    66.     RenderTargetHandle m_RenderTextureHandle;
    67.     BlitPass blitPass;
    68.     public override void Create() {
    69.         var passIndex = settings.blitMaterial != null ? settings.blitMaterial.passCount - 1 : 1;
    70.         settings.blitMaterialPassIndex = Mathf.Clamp(settings.blitMaterialPassIndex, -1, passIndex);
    71.         blitPass = new BlitPass(settings.Event, settings.blitMaterial, settings.blitMaterialPassIndex, name);
    72.         m_RenderTextureHandle.Init(settings.textureId);
    73.     }
    74.     public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
    75.         var src = renderer.cameraColorTarget;
    76.         var dest = (settings.destination == Target.Color) ? RenderTargetHandle.CameraTarget : m_RenderTextureHandle;
    77.         if (settings.blitMaterial == null) {
    78.             Debug.LogWarningFormat("Missing Blit Material. {0} blit pass will not execute. Check for missing reference in the assigned renderer.", GetType().Name);
    79.             return;
    80.         }
    81.         blitPass.Setup(src, dest);
    82.         renderer.EnqueuePass(blitPass);
    83.     }
    84. }

    I hope this helps someone. I've seen a lot of unanswered Ripple Effect posts around the internet...

    Happy coding.
     
    achimmihca, NotaNaN, PutridEx and 3 others like this.
  3. loberava

    loberava

    Joined:
    Oct 20, 2018
    Posts:
    2
    Hello! I'm a novice and i'm still having a hard time trying to reproduce the effect, could you please share an example project for reference? Thanks!
     
  4. Zethros

    Zethros

    Joined:
    Jan 11, 2013
    Posts:
    12
    A git repo is now available explaining this: https://github.com/JBMiller/Unity-Ripple-Effect-Example
     
    kypronite and yinhao7700 like this.
  5. loberava

    loberava

    Joined:
    Oct 20, 2018
    Posts:
    2
  6. xpxilom

    xpxilom

    Joined:
    Aug 28, 2014
    Posts:
    30
    No funciona en 2d =(
     
  7. firdiar

    firdiar

    Joined:
    Aug 2, 2017
    Posts:
    25
  8. betomaluje

    betomaluje

    Joined:
    Mar 23, 2019
    Posts:
    18
    Hi, I'm facing the same issue on 2D and I saw that issue posted above but still can't figure out where to put the
    Code (CSharp):
    1.       for (int i = 0; i < rendererFeatures.Count; i++)
    2.             {
    3.                 if (rendererFeatures[i].isActive)
    4.                 {
    5.                     rendererFeatures[i].AddRenderPasses(this, ref renderingData);
    6.                 }
    7.             }
    I tried on "RippleEffect_RenderPassFeature" but I can't find any "rendererFeatures" since it's not inheriting on "ScriptableRenderer"
    Any help?
     
  9. xpxilom

    xpxilom

    Joined:
    Aug 28, 2014
    Posts:
    30
    Any help?
     
  10. oliran

    oliran

    Joined:
    Sep 29, 2015
    Posts:
    49
    Seriously thank you for posting this. This is the exact effect I needed but couldn't get to work in URP. Now I can go through your code and learn how it was done. THANK YOU!
     
  11. unity_3t49D9d23gG38w

    unity_3t49D9d23gG38w

    Joined:
    Jul 17, 2021
    Posts:
    1
    Please put your code and shadercode to an available website, for example github, cause your forum is not available.
     
  12. rackley

    rackley

    Joined:
    May 6, 2017
    Posts:
    8
    Works great in 2021 LTS version. Seems to break in 2022. It actually ripples the "Scene" tab but not the actual game camera. Has anyone gotten this to work in the 2022 version of the editor?