[SOLVED] URP: Screen effects for Single Pass VR (Sobel, Blur, ...)

Hey there,

I’m trying to get some shaders I wrote to work with Single Pass VR with URP. It works in Multi-Pass already and I’m happy with the result. I just can’t make it work with Single Pass…

I guess the problem I’m having here is that I cannot sample the UV correctly, I’ve tried a couple of things but it just won’t render in Single Pass on my Oculus Quest.

Shader "Unlit/SobelFilter"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderPipeline" = "UniversalPipeline" "RenderType"="Opaque" }
        LOD 200
       
        Pass
        {
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
             
            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
           
            CBUFFER_START(UnityPerMaterial)
            half4 _MainTex_ST;
            float2 _MainTex_TexelSize;
            CBUFFER_END;
           
            float _Intensity;
            float _Weight;
            float _Blend;
            half4 _OutlineColor;
            half4 _BackgroundColor;
            float _Threshold;
            float _SobelValue;
            float _ShowGrayscale;
           
            struct Attributes
            {
                float4 positionOS       : POSITION;
                float2 uv               : TEXCOORD0;
            };

            struct Varyings
            {
                float2 uv        : TEXCOORD0;
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_OUTPUT_STEREO
            };
           
            float2 UnityStereoScreenSpaceUVAdjust(float2 uv, float4 scaleAndOffset)
            {
                return uv.xy * scaleAndOffset.xy + scaleAndOffset.zw;
            }
           
            float intensity(float4 color){
                return sqrt((color.x*color.x)+(color.y*color.y)+(color.z*color.z));
            }
           
            float sobel(float stepx, float stepy, float2 uv)
            {
                float tleft = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(-stepx, stepy)));
                float left = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(-stepx, 0)));
                float bleft = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(-stepx, -stepy)));
                float top = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(0, stepy)));
                float bottom = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(0, -stepy)));
                float tright = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(stepx, stepy)));
                float right = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(stepx, 0)));
                float bright = intensity(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(stepx, -stepy)));
               
                float x = (_SobelValue/2 * tleft) + (_SobelValue * left) + (_SobelValue/2 * bleft) - (_SobelValue/2 * tright) - (_SobelValue * right) - (_SobelValue/2 * bright);
                float y = -(_SobelValue/2 * tleft) - (_SobelValue * top) - (_SobelValue/2 * tright) + (_SobelValue/2 * bleft) + (_SobelValue * bottom) + (_SobelValue/2 * bright);
               
                float xlum = dot(x, float3(0.2126729, 0.7151522, 0.0721750));
                float ylum = dot(y, float3(0.2126729, 0.7151522, 0.0721750));
               
                float color = sqrt((xlum*xlum) + (ylum*ylum));
               
                if(_ShowGrayscale == 1){
                    return color;
                }
               
                // Replacing all values bellow threshold to black and white above (greyscale to b&w)
                if( color <= _Threshold ) { color = 0; }
                else { color = 1; }
               
                return color;
            }
           
            half4 applyColors(half4 s) {
                // Replacing blacks with background color and whites with outline color
                if (s.r == 0) {
                    s = _BackgroundColor;
                } else if (s.r == 1) {
                    s = lerp(_BackgroundColor, _OutlineColor, _Intensity);
                }
                return s;
            }
           
            Varyings vert(Attributes input)
            {
                Varyings output = (Varyings)0;
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
                output.vertex = vertexInput.positionCS;
                output.uv = input.uv;
               
                return output;
            }
            half4 frag (Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
               
                // Apply Sobel Filter
                half4 s = sobel(_MainTex_TexelSize.x, _MainTex_TexelSize.y, UnityStereoScreenSpaceUVAdjust(input.uv, _MainTex_ST));
                half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, UnityStereoScreenSpaceUVAdjust(input.uv, _MainTex_ST));
               
                // Apply outline and background color
                half4 r = applyColors(s);   
               
                if(_ShowGrayscale == 1){
                    return s;
                }
                   
                // blending with normal texture and weight
                r = lerp(r, r * color, _Blend);
                r = lerp(color, r, _Weight);       
                return r;
                   
            }
           
            #pragma vertex vert
            #pragma fragment frag
           
            ENDHLSL
        }
    }
    FallBack "Diffuse"
}

Solved : The solution is actually really straightforward and I think Unity should spend some time actually documenting the URP Shader macros a bit more, I found the solution on a forum post. There’s nothing about it anywhere on the internet.

The trick is to use TEXTURE2D_X and SAMPLE_TEXTURE2D_X instead of TEXTURE2D and SAMPLE_TEXTURE2D. I also replaced my method UnityStereoScreenSpaceUVAdjust(uv, _MainTex_ST) with UnityStereoTransformScreenSpaceTex(uv).

These macros with the X as prefix are specifically designed for single pass VR

3 Likes

Can you offer some guidance on my blur shader?

I’m running in to the same issue with the Built in Renderer. The blur works in multi-pass but not in single-pass. I’m running a blur filter for each individual eye. When applied the left eye is gray and the right eye is black. Here’s my shader code:

Shader "Hidden/BlurEffectConeTap" {
    Properties { _MainTex ("", any) = "" {} }
    CGINCLUDE
    #include "UnityCG.cginc"
    struct v2f {
        float4 pos : SV_POSITION;
        half2 uv : TEXCOORD0;
        half2 taps[4] : TEXCOORD1;
    };
    sampler2D _MainTex;
    half4 _MainTex_TexelSize;
    half4 _MainTex_ST;
    half4 _BlurOffsets;
    v2f vert( appdata_img v ) {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);

        o.uv = v.texcoord - _BlurOffsets.xy * _MainTex_TexelSize.xy;
#ifdef UNITY_SINGLE_PASS_STEREO
        // we need to keep texel size correct after the uv adjustment.
        o.taps[0] = UnityStereoScreenSpaceUVAdjust(o.uv + _MainTex_TexelSize * _BlurOffsets.xy * (1.0f / _MainTex_ST.xy), _MainTex_ST), _MainTex_ST);
        o.taps[1] = UnityStereoScreenSpaceUVAdjust(o.uv - _MainTex_TexelSize * _BlurOffsets.xy * (1.0f / _MainTex_ST.xy), _MainTex_ST), _MainTex_ST);
        o.taps[2] = UnityStereoScreenSpaceUVAdjust(o.uv + _MainTex_TexelSize * _BlurOffsets.xy * half2(1, -1) * (1.0f / _MainTex_ST.xy), _MainTex_ST);
        o.taps[3] = UnityStereoScreenSpaceUVAdjust(o.uv - _MainTex_TexelSize * _BlurOffsets.xy * half2(1, -1) * (1.0f / _MainTex_ST.xy), _MainTex_ST);
#else
        o.taps[0] = o.uv + _MainTex_TexelSize * _BlurOffsets.xy;
        o.taps[1] = o.uv - _MainTex_TexelSize * _BlurOffsets.xy;
        o.taps[2] = o.uv + _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
        o.taps[3] = o.uv - _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
#endif
        return o;
    }
    half4 frag(v2f i) : SV_Target {
        half4 color = tex2D(_MainTex, i.taps[0]);
        color += tex2D(_MainTex, i.taps[1]);
        color += tex2D(_MainTex, i.taps[2]);
        color += tex2D(_MainTex, i.taps[3]);
        return color * 0.25;
    }
    ENDCG
    SubShader {
         Pass {
              ZTest Always Cull Off ZWrite Off

              CGPROGRAM
              #pragma vertex vert
              #pragma fragment frag
              ENDCG
          }
    }
    Fallback off
}