Skip to main content
typo
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373
Shader "Unlit/POVDisplay"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // Render with transparent objects, after the opaque pass.
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        LOD 100

        // Don't write to the depth buffer.
        ZWrite Off
        // Additive blending (add light/glow).
        Blend One One

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Shift our texture coordinates so 0 is in the center,
                // and we go to -1 ... +1 at the edges.
                o.uv = (v.uv - 0.5f) * 2.0f;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // Vary this parameter to control how many rings of light you get.
                const float ledCount = 32;
                                
                // Simulate the strip of LEDs spinning around very fast.
                float headAngle = _Time.y * 7.0f;

                // Compute the angle of the pixel we're rendering.
                float angle = atan2(i.uv.y, i.uv.x);
                
                // Compute how recently we've been illuminated.
                // 0 = head is about to reach us (1 full cycle behind).
                // 1 = head just reached us.
                float difference = frac(headAngle + angle / (2.0f * 3.141592653589f));

                // The approaching LED head shines light on the faded parsparts nearby,
                // so brighten them back up to 1.
                if (difference < 0.1f)
                    difference = 1.0f - 9.0f * difference;

                // Small timing errors make the display seem to twist/wobble,
                // so we'll distort our angle a bit to mimic this.
                angle += 0.01f * sin(_Time.y * 2.0f + difference * 1.0f);

                // Compute our radius in "LED space"
                float radius = length(i.uv) * ledCount;

                // Round to the nearest LED.
                float rounded = round(radius);

                // Compute a sample point in our texture, using our distorted angle
                // and rounded radius.
                float2 samplePoint = float2(cos(angle), sin(angle))*rounded/ledCount;               
                
                // Sample the RenderTexture at this position.
                fixed4 col = tex2D(_MainTex, samplePoint / (2.0f) + 0.5f);
                // Square the colour to increase saturation, like a pure LED light.
                col *= col;

                // The center of the LED is brighter. Fade it in the gaps between rings.
                float brightness = (radius - rounded) * 2.0f;
                brightness = 1.0f - brightness * brightness;
                
                // Brighten/darken the colour based on proximity to the ring,
                // and lag behind the spinning head.
                return col * brightness * lerp(9.0f, 1.0f, difference);
            }
            ENDCG
        }
    }
}
Shader "Unlit/POVDisplay"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // Render with transparent objects, after the opaque pass.
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        LOD 100

        // Don't write to the depth buffer.
        ZWrite Off
        // Additive blending (add light/glow).
        Blend One One

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Shift our texture coordinates so 0 is in the center,
                // and we go to -1 ... +1 at the edges.
                o.uv = (v.uv - 0.5f) * 2.0f;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // Vary this parameter to control how many rings of light you get.
                const float ledCount = 32;
                                
                // Simulate the strip of LEDs spinning around very fast.
                float headAngle = _Time.y * 7.0f;

                // Compute the angle of the pixel we're rendering.
                float angle = atan2(i.uv.y, i.uv.x);
                
                // Compute how recently we've been illuminated.
                // 0 = head is about to reach us (1 full cycle behind).
                // 1 = head just reached us.
                float difference = frac(headAngle + angle / (2.0f * 3.141592653589f));

                // The approaching LED head shines light on the faded pars nearby,
                // so brighten them back up to 1.
                if (difference < 0.1f)
                    difference = 1.0f - 9.0f * difference;

                // Small timing errors make the display seem to twist/wobble,
                // so we'll distort our angle a bit to mimic this.
                angle += 0.01f * sin(_Time.y * 2.0f + difference * 1.0f);

                // Compute our radius in "LED space"
                float radius = length(i.uv) * ledCount;

                // Round to the nearest LED.
                float rounded = round(radius);

                // Compute a sample point in our texture, using our distorted angle
                // and rounded radius.
                float2 samplePoint = float2(cos(angle), sin(angle))*rounded/ledCount;               
                
                // Sample the RenderTexture at this position.
                fixed4 col = tex2D(_MainTex, samplePoint / (2.0f) + 0.5f);
                // Square the colour to increase saturation, like a pure LED light.
                col *= col;

                // The center of the LED is brighter. Fade it in the gaps between rings.
                float brightness = (radius - rounded) * 2.0f;
                brightness = 1.0f - brightness * brightness;
                
                // Brighten/darken the colour based on proximity to the ring,
                // and lag behind the spinning head.
                return col * brightness * lerp(9.0f, 1.0f, difference);
            }
            ENDCG
        }
    }
}
Shader "Unlit/POVDisplay"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // Render with transparent objects, after the opaque pass.
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        LOD 100

        // Don't write to the depth buffer.
        ZWrite Off
        // Additive blending (add light/glow).
        Blend One One

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Shift our texture coordinates so 0 is in the center,
                // and we go to -1 ... +1 at the edges.
                o.uv = (v.uv - 0.5f) * 2.0f;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // Vary this parameter to control how many rings of light you get.
                const float ledCount = 32;
                                
                // Simulate the strip of LEDs spinning around very fast.
                float headAngle = _Time.y * 7.0f;

                // Compute the angle of the pixel we're rendering.
                float angle = atan2(i.uv.y, i.uv.x);
                
                // Compute how recently we've been illuminated.
                // 0 = head is about to reach us (1 full cycle behind).
                // 1 = head just reached us.
                float difference = frac(headAngle + angle / (2.0f * 3.141592653589f));

                // The approaching LED head shines light on the faded parts nearby,
                // so brighten them back up to 1.
                if (difference < 0.1f)
                    difference = 1.0f - 9.0f * difference;

                // Small timing errors make the display seem to twist/wobble,
                // so we'll distort our angle a bit to mimic this.
                angle += 0.01f * sin(_Time.y * 2.0f + difference * 1.0f);

                // Compute our radius in "LED space"
                float radius = length(i.uv) * ledCount;

                // Round to the nearest LED.
                float rounded = round(radius);

                // Compute a sample point in our texture, using our distorted angle
                // and rounded radius.
                float2 samplePoint = float2(cos(angle), sin(angle))*rounded/ledCount;               
                
                // Sample the RenderTexture at this position.
                fixed4 col = tex2D(_MainTex, samplePoint / (2.0f) + 0.5f);
                // Square the colour to increase saturation, like a pure LED light.
                col *= col;

                // The center of the LED is brighter. Fade it in the gaps between rings.
                float brightness = (radius - rounded) * 2.0f;
                brightness = 1.0f - brightness * brightness;
                
                // Brighten/darken the colour based on proximity to the ring,
                // and lag behind the spinning head.
                return col * brightness * lerp(9.0f, 1.0f, difference);
            }
            ENDCG
        }
    }
}
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373

Here's an example of how we can mimic this appearance in a Unity scene:

Example POV Display

My strategy is:

  1. First, compose the image that the POV display is trying to project.

  2. Render that image, with a shader filter that adds artifacts that look like a POV display.

On the left is step 1. I've positioned some sprites representing my clock face and hands. That way it's easy to update the displayed image: I can just rotate the game objects representing the hands.

I created a new RenderTexture in my assets folder, called "Clock Face". Because the POV display is quite low-res, you can get away with a tiny texture here.

Next I made a Camera that looks at these sprites, and outputs the combined image to the RenderTexture. You can put the sprites in a layer that only that camera sees, so they're invisible to your main scene camera.

Lastly I made a quad to display the POV version. I gave that quad a new material, with a custom shader, using the RenderTexture as its Main Texture input.

Here's the shader I cooked up:

Shader "Unlit/POVDisplay"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // Render with transparent objects, after the opaque pass.
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        LOD 100

        // Don't write to the depth buffer.
        ZWrite Off
        // Additive blending (add light/glow).
        Blend One One

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Shift our texture coordinates so 0 is in the center,
                // and we go to -1 ... +1 at the edges.
                o.uv = (v.uv - 0.5f) * 2.0f;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // Vary this parameter to control how many rings of light you get.
                const float ledCount = 32;
                                
                // Simulate the strip of LEDs spinning around very fast.
                float headAngle = _Time.y * 7.0f;

                // Compute the angle of the pixel we're rendering.
                float angle = atan2(i.uv.y, i.uv.x);
                
                // Compute how recently we've been illuminated.
                // 0 = head is about to reach us (1 full cycle behind).
                // 1 = head just reached us.
                float difference = frac(headAngle + angle / (2.0f * 3.141592653589f));

                // The approaching LED head shines light on the faded pars nearby,
                // so brighten them back up to 1.
                if (difference < 0.1f)
                    difference = 1.0f - 9.0f * difference;

                // Small timing errors make the display seem to twist/wobble,
                // so we'll distort our angle a bit to mimic this.
                angle += 0.01f * sin(_Time.y * 2.0f + difference * 1.0f);

                // Compute our radius in "LED space"
                float radius = length(i.uv) * ledCount;

                // Round to the nearest LED.
                float rounded = round(radius);

                // Compute a sample point in our texture, using our distorted angle
                // and rounded radius.
                float2 samplePoint = float2(cos(angle), sin(angle))*rounded/ledCount;               
                
                // Sample the RenderTexture at this position.
                fixed4 col = tex2D(_MainTex, samplePoint / (2.0f) + 0.5f);
                // Square the colour to increase saturation, like a pure LED light.
                col *= col;

                // The center of the LED is brighter. Fade it in the gaps between rings.
                float brightness = (radius - rounded) * 2.0f;
                brightness = 1.0f - brightness * brightness;
                
                // Brighten/darken the colour based on proximity to the ring,
                // and lag behind the spinning head.
                return col * brightness * lerp(9.0f, 1.0f, difference);
            }
            ENDCG
        }
    }
}

If we scale down our _Time.y input, we can see what this looks like in slow-mo:

Slow POV Clock

Advancing our virtual "LED Head" faster than the display framerate makes the brightest part of the sweep jump around almost randomly, giving that distinctive flickering appearance.