Home

Awesome

Customizable Post-processing Stack for Universal Render Pipeline

This package adds the ability to create custom post-processing effects for the universal render pipeline in a manner similar to PPSv2 and HDRP's Custom Post Process. It is supposed to be a replacement for Unity's PPSv2 till URP internally supports custom post-processing effects.

Note: You can already add you custom effects to URP by inheriting from the ScriptableRendererFeature and ScriptableRenderPass classes. I personally find this to be a hassle and that is why I wrote this package merely for convenience. I also took it as a chance to pick up the features I like from every post-processing solution I used in Unity.

Screenshots

Scene-Screenshot-1 Scene-Screenshot-2

The screenshots uses the following builtin effects:

For the custom effects, they contain the following:

Other custom effects in samples but not used in screenshots:

Compatibility

Note: There is a branch with support for URP 8.2.0 (Unity 2020.1) which you can find here.

Features

Features that are almost untested:

Known Issues

Features I hope to implement

How To Install

Follow the instructions from the Unity manual on Installing from a Git URL and insert the url:

https://github.com/yahiaetman/URPCustomPostProcessingStack.git

The package also contains 8 example effects included as a sample.

Tutorial

For the sake of simplicity, we will create a grayscale effect.

Effect Renderer and Volume Component Script

First, lets create two C# classes. Create a script called GrayscaleEffect.cs and in the script, write the following:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.Universal.PostProcessing;

// Define the Volume Component for the custom post processing effect 
[System.Serializable, VolumeComponentMenu("CustomPostProcess/Grayscale")]
public class GrayscaleEffect : VolumeComponent
{
    [Tooltip("Controls the blending between the original and the grayscale color.")]
    public ClampedFloatParameter blend = new ClampedFloatParameter(0, 0, 1);
}

// Define the renderer for the custom post processing effect
[CustomPostProcess("Grayscale", CustomPostProcessInjectionPoint.AfterPostProcess)]
public class GrayscaleEffectRenderer : CustomPostProcessRenderer
{
    // A variable to hold a reference to the corresponding volume component
    private GrayscaleEffect m_VolumeComponent;

    // The postprocessing material
    private Material m_Material;
    
    // The ids of the shader variables
    static class ShaderIDs {
        internal readonly static int Input = Shader.PropertyToID("_MainTex");
        internal readonly static int Blend = Shader.PropertyToID("_Blend");
    }
    
    // By default, the effect is visible in the scene view, but we can change that here.
    public override bool visibleInSceneView => true;

    /// Specifies the input needed by this custom post process. Default is Color only.
    public override ScriptableRenderPassInput input => ScriptableRenderPassInput.Color;
    
    // Initialized is called only once before the first render call
    // so we use it to create our material
    public override void Initialize()
    {
        m_Material = CoreUtils.CreateEngineMaterial("Hidden/PostProcess/Grayscale");
    }

    // Called for each camera/injection point pair on each frame. Return true if the effect should be rendered for this camera.
    public override bool Setup(ref RenderingData renderingData, CustomPostProcessInjectionPoint injectionPoint)
    {
        // Get the current volume stack
        var stack = VolumeManager.instance.stack;
        // Get the corresponding volume component
        m_VolumeComponent = stack.GetComponent<GrayscaleEffect>();
        // if blend value > 0, then we need to render this effect. 
        return m_VolumeComponent.blend.value > 0;
    }

    // The actual rendering execution is done here
    public override void Render(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, ref RenderingData renderingData, CustomPostProcessInjectionPoint injectionPoint)
    {
        // set material properties
        if(m_Material != null){
            m_Material.SetFloat(ShaderIDs.Blend, m_VolumeComponent.blend.value);
        }
        // set source texture
        cmd.SetGlobalTexture(ShaderIDs.Input, source);
        // draw a fullscreen triangle to the destination
        CoreUtils.DrawFullScreen(cmd, m_Material, destination);
    }
}

As you can see, the code consists of two classes: a volume component and a renderer.

The name of the file must match the Volume Vomponent class name to comply with Unity's serialization rules.

NOTE: The renderer must be tagged by a CustomPostProcessAttribute to be taken consideration by the custom post-process renderer feature. The attribute has the following parameters:

Design notes

Volume components and Renderers are decoupled from each other which presents many possibilities while implementing your effects. For example:

  1. You can have a one-to-one relationship between your volume component and your renderer (as we did above).
  2. You can have a renderer without any corresponding volume components if it does not need any data from the volumes.
  3. You can have a renderer that reads from multiple volume components (see GrayAndInvertEffect.cs as an example).
  4. You can have multiple renderers read from the same volume component(s).

Option #3 is especially useful for writing uber effect shaders that can do multiple effects in the same blit to enhance performance.

Effect Shader

Now back to coding. We need to write the shader code. Create a shader file with any name you like (I prefer Grayscale.shader) and replace its content with the following code:

Shader "Hidden/PostProcess/Grayscale"
{
    HLSLINCLUDE
    #include "Packages/com.yetman.render-pipelines.universal.postprocess/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"

    TEXTURE2D_X(_MainTex);

    float _Blend;

    float4 GrayscaleFragmentProgram (PostProcessVaryings input) : SV_Target
    {
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

        float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
        float4 color = LOAD_TEXTURE2D_X(_MainTex, uv * _ScreenSize.xy);
        
        // Blend between the original and the grayscale color
        color.rgb = lerp(color.rgb, Luminance(color.rgb).xxx, _Blend);
        
        return color;
    }
    ENDHLSL

    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Pass
        {
            HLSLPROGRAM
            #pragma vertex FullScreenTrianglePostProcessVertexProgram
            #pragma fragment GrayscaleFragmentProgram
            ENDHLSL
        }
    }
    Fallback Off
}

Since vertex shaders rarely contain any logic specific to the effect, we use a default vertex shader FullScreenTrianglePostProcessVertexProgram which is included in:

Packages/com.yetman.render-pipelines.universal.postprocess/ShaderLibrary/Core.hlsl

The fragment shader is where the magic happens. It reads the original pixel color, calculates the grayscale color and blends the two colors together.

Editor Steps

Now that we have our custom effect, we need to add a CustomPostProcess renderer feature to the ForwardRenderer asset as seen in the next image. You can also add a SceneNormals renderer feature if you want to use scene normals in an effect (such as Edge Detection).

Add Renderer Feature

The CustomPostProcess renderer feature contains 3 lists that represent 3 injection points in the ScriptableRenderer as seen in the next image. The three injection points are:

The effect order in the lists is the same order in which they are executed (from top to bottom). You can re-order the effects as you see fit.

Note: Any effect must be added to the renderer feature to be rendered, otherwise, it will be ignored. So we added GrayScale to its list After Post Process as seen in the next image.

Add GrayScale to After Post Process

Then create a volume in the scene (or use an existing volume) and add a Grayscale Effect volume component to its profile as seen in the next image.

Add Grayscale to Volume

Now you can override the Blend parameter and see the view becoming grayscale.

Modify Blend

We didn't explain some stuff here but you can see more in the samples:

Issues & Pull Requests

If you find any bugs, kindly open an issue. I would be glad to catch as many bugs as possible before using it in a game. Thanks in advance :blush:.

If you have any feature requests, also open an issue. Also, Pull requests are very welcome.

I apologize in advance if I couldn't reply in a timely manner but I will do my best to reply as soon as I can.

Contact

If you would like to contact me, DM me on Twitter: @yetmania

License

MIT License