Achieving a parallax effect?

I’m trying to achieve a parallax effect similar to this:



There are also several shadertoy shaders that are like this.

I’ve converted shadertoy shaders in the past but I’m not entirely sure how to convert these shaders…

Here’s my current 2D shader that creates a grid of dots that I’d like to parallax:

Shader "Unlit/Grid"
{
    Properties
    {
        _BackgroundColor ("Background Color", Color) = (0,0,0,0)
        _DotColor ("Dot Color", Color) = (1,1,1,1)

        _DotOffset("Dot Offset", Vector) = (0,0,0,0)
        _DotRadius("Dot Radius", Float) = 0.04
        _DotSpacing("Dot Spacing", Float) = 1.0
        _DotSmoothing("Dot Smoothing", Float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True"}
        Blend SrcAlpha OneMinusSrcAlpha
        Cull Off ZWrite Off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            uniform float4 _BackgroundColor;
            uniform float4 _DotColor;

            uniform float2 _DotOffset;
            uniform float _DotRadius;
            uniform float _DotSpacing;
            uniform float _DotSmoothing;

            struct appdata
            {
                float2 uv : TEXCOORD0;
                float4 vertex : POSITION;
                float3 worldPos : TEXCOORD1;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 worldPos : TEXCOORD1;
            };

            float dots( float2 p, float2 offset, float spacing, float radius, float smoothing )
            {
                // Divide into squares, create grid
                float2 dst = fmod(abs(p - offset), spacing);
                //float2 dst = opRep(abs(p - offset), spacing);

                // Create circles
                float2 c = distance(dst, float2(0.5, 0.5));
                float2 dc = fwidth(c) * smoothing;
                float2 f = smoothstep(-dc+radius, dc+radius, c);
                float result = saturate(f.x * f.y);

                return 1-result;
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 p = i.worldPos.xyz;

                fixed4 col = _BackgroundColor;

                float gridDots = dots(p, _DotOffset, _DotSpacing, _DotRadius, _DotSmoothing);
                col = lerp(col, _DotColor, gridDots);

                return col;
            }
            ENDCG
        }
    }
}

I tried doing something like this:

for(int i = 0; i < 3; i++)
{
float gridDots = dots(p, _DotOffset, _DotSpacing, _DotRadius, _DotSmoothing);
p -= (p * 0.2) * i;
col = lerp(col, _DotColor, gridDots);
}

but it does a strange effect and doesn’t offset it “into the mesh” so I guess I’m going to need the camera position in the shader or something?

Any help would be appreciated.

An infinite mirror effect Rocket League is using is relatively straight forward to achieve. It’s just using a basic parallax offset in a loop. Something like this:

// basic parallax offset function
float2 ParallaxOffsetUV(float2 uv, float3 tangentSpaceViewDir, float offsetScale)
{
  float2 uvOffset = tangentSpaceViewDir.xy / tangentSpaceViewDir.z;
  uvOffset *= offsetScale;
  return uv - uvOffset;
}

// in the shader
float4 col = 0;
for (int iter=0; iter<_NumLayers; iter++)
{
  float2 layerUV = ParallaxOffsetUV(i.uv, tangentSpaceViewDir, _LayerOffset * float(iter));
  float layerFade = 1.0 - (float(iter) / _NumLayers);
  col += tex2D(_MainTex, layerUV) * layerFade;
}

The only “tricky bit” is that tangent space view direction. For that you need to pass the world normal, and world tangent from the vertex shader to the fragment shader (and optionally the world bitangent, or reconstruct the bitangent in the fragment shader). This is going to be the same thing you would need to do to support normal mapping btw. Usually you would use those three vectors to construct a rotation matrix to transform the tangent space normal into world space. But instead you apply the transpose matrix to approximate the world to tangent space transform.

That’s a fancy way of saying you do this:

// vertex shader
o.worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
o.worldBitangent = cross(o.worldNormal, o.worldTangent) * v.tangent.w * unity_WorldTransformParams.w;

// fragment shader
float3 worldSpaceViewDir = UnityWorldSpaceViewDir(i.worldPos);
float3x3 tbn = float3x3(i.worldTangent, i.worldBitangent, i.worldNormal);
float3 tangentSpaceViewDir = mul(tbn, worldSpaceViewDir);

Put together it should look something like this:

Shader "Unlit/InfinityMirror"
{
    Properties
    {
        _Color ("Color", Color) = (0.5,0.5,0.5,1)
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
        _NumLayers ("Number of Layers", Float) = 10
        _LayerOffset ("Layer Offset Scale", Float) = 0.025
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
                float3 worldTangent : TEXCOORD3;
                float3 worldBitangent : TEXCOORD4;
            };

            sampler2D _MainTex;

            v2f vert (appdata_full v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                o.worldBitangent = cross(o.worldNormal, o.worldTangent) * v.tangent.w * unity_WorldTransformParams.w;
                return o;
            }

            float2 ParallaxOffsetUV(float2 uv, float3 tangentSpaceViewDir, float offsetScale)
            {
                float2 uvOffset = tangentSpaceViewDir.xy / tangentSpaceViewDir.z;
                uvOffset *= offsetScale;
                return uv - uvOffset;
            }

            float4 _Color;
            float _NumLayers;
            float _LayerOffset;

            fixed4 frag (v2f i) : SV_Target
            {
                float3 worldSpaceViewDir = UnityWorldSpaceViewDir(i.worldPos);
                float3x3 tbn = float3x3(i.worldTangent, i.worldBitangent, i.worldNormal);
                float3 tangentSpaceViewDir = mul(tbn, worldSpaceViewDir);

                float4 col = 0;
                for (int iter=0; iter<_NumLayers; iter++)
                {
                    float2 layerUV = ParallaxOffsetUV(i.uv, tangentSpaceViewDir, _LayerOffset * float(iter));
                    float layerFade = 1.0 - (float(iter) / _NumLayers);
                    col += tex2D(_MainTex, layerUV) * layerFade;
                }
                return col * _Color;
            }
            ENDCG
        }
    }
}
3 Likes

Thank you so much, I really appreciate the in-depth reply!

I’d also like to thank you for all of your solutions that have helped me in the past. Keep up the good work :smile: