HDRP Custom Pass - Depth Issues

Hey Guys,

I’ve been having some trouble with custom passes with the new HDRP and was hoping I could get some help making sense of it. I’m trying to do a fullscreen shader pass in an attempt to create a pixel perfect selection feature. I’ve managed to get the feature working but the rendertexture that is being created by the buffer, that the shader is being applied to, doesn’t seem to have per pixel depth sorting, which messes with the selection. I’m not sure if the issue is related to the shader, where the shader pass itself doesn’t include depth, or if its the buffer not including depth when created.

Below is the code used for the custom pass as well as the code for the highlight shader, much of this code is just modified versions of the HDRP custom pass examples. I’ve also attached screenshots of the rendertextures being created by the buffer to show the depth issue. As you can see, when zoomed out the objects render incorrectly and when zoomed in they seem to render correctly.

Apologises, the code is pretty messy as I’ve been constantly fiddling with it.

Custom Pass Code:

class HighlightPass : CustomPass {
    [SerializeField] private LayerMask interactionLayers;
    [SerializeField] private Material replacementMat;

    [ColorUsage(false, true)]
    public Color        outlineColor = Color.black;
    public float        thickness = 4;

    Material                fullscreenOutline;
    MaterialPropertyBlock   outlineProperties;
    ShaderTagId[]           shaderTags;
    RTHandle                buffer;
    [SerializeField] private RenderTexture renderTexture;


    protected override void Setup(ScriptableRenderContext renderContext, CommandBuffer cmd)
    {
        fullscreenOutline = CoreUtils.CreateEngineMaterial(Shader.Find("FullScreen/Highlight"));
        outlineProperties = new MaterialPropertyBlock();
        //
        // List all the materials that will be replaced in the frame
        shaderTags = new ShaderTagId[3]
        {
            new ShaderTagId("Forward"),
            new ShaderTagId("ForwardOnly"),
            new ShaderTagId("SRPDefaultUnlit")
           
        };

        buffer = RTHandles.Alloc(
            Vector2.one, TextureXR.slices, dimension: TextureXR.dimension, enableRandomWrite: true,
            colorFormat: GraphicsFormat.R8G8B8A8_SRGB,           
            useDynamicScale: true, name: "Buffer"
        );

        renderTexture = buffer.rt;
    }

    void DrawOutlineMeshes(ScriptableRenderContext renderContext, CommandBuffer cmd, HDCamera hdCamera, CullingResults cullingResult, RenderTargetIdentifier buffer)
    {
        var result = new RendererListDesc(shaderTags, cullingResult, hdCamera.camera)
        {
            // We need the lighting render configuration to support rendering lit objects
            rendererConfiguration = PerObjectData.LightProbe | PerObjectData.LightProbeProxyVolume | PerObjectData.Lightmaps,
            renderQueueRange = RenderQueueRange.all,
            sortingCriteria = SortingCriteria.BackToFront,
            excludeObjectMotionVectors = false,
           // overrideMaterial = replacementMat,
            //overrideMaterialPassIndex = 3,
            layerMask = interactionLayers
        };
       
        CoreUtils.SetRenderTarget(cmd, buffer, ClearFlag.Color);
        HDUtils.DrawRendererList(renderContext, cmd, RendererList.Create(result));
    }

    protected override void Execute(ScriptableRenderContext renderContext, CommandBuffer cmd, HDCamera camera, CullingResults cullingResult)
    {
        DrawOutlineMeshes(renderContext, cmd, camera, cullingResult, buffer);
        SetCameraRenderTarget(cmd);

        // if(InputManager.Instance == null) {
        //     return;
        // }

        //Color selectionColour = InputManager.Instance.SelectionColor;

        //outlineProperties.SetVector("_MousePosition", Input.mousePosition);
        //outlineProperties.SetColor("_Identifier", selectionColour);
        //outlineProperties.SetColor("_OutlineColor", outlineColor);
       // outlineProperties.SetFloat("_Thickness", thickness);
        outlineProperties.SetTexture("_Buffer", buffer);

        CoreUtils.DrawFullScreen(cmd, fullscreenOutline, outlineProperties, shaderPassId: 0);
    }

    Texture2D ToTexture2D(RenderTexture rTex) {
        Texture2D tex = new Texture2D(rTex.width, rTex.height);
        RenderTexture.active = rTex;
        tex.ReadPixels(new Rect(0, 0, rTex.width, rTex.height), 0, 0);
        tex.Apply();
        return tex;
    }

    protected override void Cleanup()
    {
        CoreUtils.Destroy(fullscreenOutline);
        buffer.Release();
    } // Cleanup code
   
}

Highlight Shader Code:

Shader "FullScreen/Highlight"
{
    HLSLINCLUDE

    #pragma vertex Vert

    #pragma target 4.5
    #pragma only_renderers d3d11 ps4 xboxone vulkan metal switch

    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/RenderPass/CustomPass/CustomPassCommon.hlsl"

    TEXTURE2D_X(_Buffer);
    float2 _MousePosition;
    //float3 _Identifier;
    float4 _OutlineColor;
    int _Thickness;


    #define v2 1.41421
    #define c45 0.707107
    #define c225 0.9238795
    #define s225 0.3826834

    #define MAXSAMPLES 8
    // Neighbour pixel positions
    static float2 samplingPositions[MAXSAMPLES] =
    {
        float2( 1,  1),
        float2( 0,  1),
        float2(-1,  1),
        float2(-1,  0),
        float2(-1, -1),
        float2( 0, -1),
        float2( 1, -1),
        float2( 1, 0),
    };

    bool CompareFloat1(float1 a, float1 b) {
        float1 MIN_NORMAL = 1.17549435E-38;
        float1 epsilon = 0.9999999999999999999999999999999999999999999999999999999999999999999999999999999;
        float1 absA = abs(a);
        float1 absB = abs(b);
        float1 diff = abs(a - b);

        if (a == b) { // shortcut, handles infinities
            return true;
        } else if (a == 0 || b == 0 || absA + absB < MIN_NORMAL) {
            // a or b is zero or both are extremely close to it
            // relative error is less meaningful here
            return diff < (epsilon * MIN_NORMAL);
        } else { // use relative error
            return diff / (absA + absB) < epsilon;
        }

    }

    bool CompareFloat3(float3 a, float3 b) {
        if (CompareFloat1(a.r, b.r)) {
           if(CompareFloat1(a.g, b.g)){
               if(CompareFloat1(a.b, b.b)){
                   return true;
               }
           }
        }
        return false;
    }

    // The PositionInputs struct allow you to retrieve a lot of useful information for your fullScreenShader:
    // struct PositionInputs
    // {
    //     float3 positionWS;  // World space position (could be camera-relative)
    //     float2 positionNDC; // Normalized screen coordinates within the viewport    : [0, 1) (with the half-pixel offset)
    //     uint2  positionSS;  // Screen space pixel coordinates                       : [0, NumPixels)
    //     uint2  tileCoord;   // Screen tile coordinates                              : [0, NumTiles)
    //     float  deviceDepth; // Depth from the depth buffer                          : [0, 1] (typically reversed)
    //     float  linearDepth; // View space Z coordinate                              : [Near, Far]
    // };

    // To sample custom buffers, you have access to these functions:
    // But be careful, on most platforms you can't sample to the bound color buffer. It means that you
    // can't use the SampleCustomColor when the pass color buffer is set to custom (and same for camera the buffer).
    // float4 SampleCustomColor(float2 uv);
    // float4 LoadCustomColor(uint2 pixelCoords);
    // float LoadCustomDepth(uint2 pixelCoords);
    // float SampleCustomDepth(float2 uv);

    // There are also a lot of utility function you can use inside Common.hlsl and Color.hlsl,
    // you can check them out in the source code of the core SRP package.
    float4 FullScreenPass(Varyings varyings) : SV_Target
    {
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(varyings);
        float depth = LoadCameraDepth(varyings.positionCS.xy);
        PositionInputs posInput = GetPositionInput(varyings.positionCS.xy, _ScreenSize.zw, depth, UNITY_MATRIX_I_VP, UNITY_MATRIX_V);
        float3 viewDirection = GetWorldSpaceNormalizeViewDir(posInput.positionWS);
        float4 color = float4(0.0, 0.0, 0.0, 0.0);

        // Load the camera color buffer at the mip 0 if we're not at the before rendering injection point
        if (_CustomPassInjectionPoint != CUSTOMPASSINJECTIONPOINT_BEFORE_RENDERING)
            color = float4(CustomPassSampleCameraColor(posInput.positionNDC.xy, 0), 1);

        // Add your custom pass code here
        float2 uv = posInput.positionNDC.xy * _RTHandleScale.xy;
       
        //float4 buffer = SAMPLE_TEXTURE2D_X_LOD(_Buffer, s_linear_clamp_sampler, uv, 0);

        PositionInputs mouseInput = GetPositionInput(_MousePosition.xy, _ScreenSize.zw, depth, UNITY_MATRIX_I_VP, UNITY_MATRIX_V);

        float4 _Identifier = SAMPLE_TEXTURE2D_X_LOD(_Buffer, s_linear_clamp_sampler, mouseInput.positionNDC.xy, 0);

        float4 buffer = SAMPLE_TEXTURE2D_X_LOD(_Buffer, s_linear_clamp_sampler, posInput.positionNDC.xy, 0);

        if(_Identifier.r==0 && _Identifier.g==0 && _Identifier.b==0) {
            return buffer.a = 0;
            return buffer;
        }

        //buffer.rgb = test.rgb;
        buffer.a = 0;

        if(buffer.r == _Identifier.r && buffer.g == _Identifier.g && buffer.b == _Identifier.b){
           
            for(int x=0; x<_Thickness+1; x++) {
           
                float3 o = float3(_ScreenSize.zw, 0);

                for (int i = 0; i < MAXSAMPLES; i++)
                {
                    float2 uvN = posInput.positionNDC.xy + _ScreenSize.zw * samplingPositions[i] * x;
                    float4 neighbour = SAMPLE_TEXTURE2D_X_LOD(_Buffer, s_linear_clamp_sampler, uvN, 0);
                   
                    half3 delta = abs(neighbour.rgb - _Identifier.rgb);
                    if (length(delta) > 0.01)
                    {
                        buffer.rgb = _OutlineColor.rgb;
                        buffer.a = 1;
                        break;
                    }
                }
            }

        }

        // return float4(0,0,0,0);

        // //buffer.a = 1;
       
        // float3 delta = abs(buffer.rgb - _Identifier.rgb);
   
        // if(length(delta) < 0.01568628) {

        //     for(int x=0; x<_Thickness+1; x++) {
           
        //         float3 o = float3(_ScreenSize.zw, 0);

        //         for (int i = 0; i < MAXSAMPLES; i++)
        //         {
        //             float2 uvN = uv + _ScreenSize.zw * samplingPositions[i] * x;
        //             float4 neighbour = SAMPLE_TEXTURE2D_X_LOD(_Buffer, s_linear_clamp_sampler, uvN, 0);
                   
        //             delta = abs(neighbour.rgb - _Identifier.rgb);
        //             if (length(delta) > 0.01568628)
        //             {
        //                 buffer.rgb = _OutlineColor.rgb;
        //                 buffer.a = 1;
        //                 break;
        //             }
        //         }
        //     }
        // }
       
        // if(buffer.r == _Identifier.r && buffer.g == _Identifier.g && buffer.b == _Identifier.b){
        //     buffer.rgb = float4(0,0,1,1);
        //     buffer.a = 1;
        // } else {
        //     buffer.a = 0;
        // }
       

        // if (Luminance(buffer.rgb) < 0.01)
        // {
        //     buffer.a = 1;
        // }
       
        return buffer;
    }



    ENDHLSL

    SubShader
    {
        Pass
        {
            Name "Custom Pass 0"

            ZWrite On
            ZTest Always
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off

            HLSLPROGRAM
                #pragma fragment FullScreenPass
            ENDHLSL
        }
    }
    Fallback Off
}

Screen Shots:

When zoomed out renders with depth issue:


Hello,

You indeed don’t have any depth buffer bound when you render the objects you want to outline, at this line:

        CoreUtils.SetRenderTarget(cmd, buffer, ClearFlag.Color);

This only binds the color buffer so the shader can’t perform any depth tests. This is fine for most of the outline stuff because they are usually visible behind other objects but if you want it, you just have to add a depth buffer in the SetRenderTarget. There is an example on how to do it here: https://github.com/alelievr/HDRP-Custom-Passes/blob/master/Assets/CustomPasses/Blur/SlightBlur.cs#L131

1 Like

Thank you so much for the response, really appreciate it! Added in the depth buffer and it worked! For those interested these were the changes that enabled depth to work in our build.

Firstly created the depth buffer in the setup method;

depthBuffer = RTHandles.Alloc(
            Vector2.one,
            colorFormat: GraphicsFormat.R16_UInt, useDynamicScale: true,
            name: "Blur Depth Mask", depthBufferBits: DepthBits.Depth16
        );

Then added a stateblock within the RenderListDesc

stateBlock = new RenderStateBlock(RenderStateMask.Depth){ depthState = new DepthState(true, CompareFunction.LessEqual)}

Finally, a key component was also setting the ClearFlag to ClearFlag.All.

2 Likes