Here's an example of how we can mimic this appearance in a Unity scene:
My strategy is:
First, compose the image that the POV display is trying to project.
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 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
}
}
}
If we scale down our _Time.y
input, we can see what this looks like in slow-mo:
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.