Moire effect in LED panel shader

Hi everyone,

I translated a GLSL shader if found on the web in CG. It’s a shader that converts a texture/movie into a LED panel. See the image “LED close”
The problem is that if I look at the panel from a distance, the little dots from the LED panel become so small that Unity apparantly doesn’t know how to display it correctly. It starts with a moire pattern and if you go even further behind it becomes a total mess. See image “LED far”

The problem is a lot less if I use an image as the opacity mask for the dots, but I need to adjust the size of the dots on the fly, so an image is not enough for me. I think the textured version is mipmapped and therefore has less of the moire effect. The shader version, which I need, is not mipmapped. If anyone can give some advice how to fix this, I would greatly appreciate.


2230692--148626--LED_Far.PNG

Google for shader antialiasing techniques using fwidth/ddx functions
i.e. prideout

Hi again,

After some desperate attempts to do some derivative supersampling, I’m turning to the community again and hope someone can help me.
The LED shader I use is the following:

Shader "Custom/pixelate" {
Properties{
_ResolutionX ("ResolutionX", int) = 100
_ResolutionY ("ResolutionY", int) = 100
_Samples ("Samples", int) = 5
_Radius ("LED Radius", float) = 1.0
_MainTex ("Main Texture", 2D) = "white" {}
_ChromaColor ("Chroma color", color) = (1.0, 1.0, 1.0)
_Feather ("Feather", Range(0, 1)) = 0.0
_Size ("Size", float) = 1.0
}
    SubShader {
        Pass {
        Tags { "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma target 3.0
           
            float _ResolutionX;
            float _ResolutionY;
            float _Samples;
            sampler2D _MainTex;
            float _Radius;
            fixed4 _ChromaColor;
            float _Feather;
            float _Size;
            float2 texCoords[9];
           
            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord0 : TEXCOORD; 
            };
           
            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD;
            };

            v2f vert(appdata IN) {
                v2f OUT;
                OUT.pos = mul (UNITY_MATRIX_MVP, IN.vertex);
                OUT.uv0 = IN.texcoord0;
                return OUT;
            }

     fixed4 frag(v2f IN) : COLOR {
           
        float4 avgColor; //will hold our averaged color from our sample points
        float2 texCoordsStep = 1.0/(float2(float(_ResolutionX),float(_ResolutionX))/float(_Size)); //width of "pixel region" in texture coords
        float2 pixelRegionCoords = frac(IN.uv0.xy/texCoordsStep); //x and y coordinates within "pixel region"
        float2 pixelBin = floor(IN.uv0.xy/texCoordsStep); //"pixel region" number counting away from base case
        float2 inPixelStep = texCoordsStep/3.0; //width of "pixel region" divided by 3 (for KERNEL_SIZE = 9, 3x3 square)
        float2 inPixelHalfStep = inPixelStep/2.0;
       
        //use offset (pixelBin * texCoordsStep) from base case (the lower left corner of billboard) to compute texCoords
     texCoords[0] = float2(inPixelHalfStep.x, inPixelStep.y*2.0 + inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[1] = float2(inPixelStep.x + inPixelHalfStep.x, inPixelStep.y*2.0 + inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[2] = float2(inPixelStep.x*2.0 + inPixelHalfStep.x, inPixelStep.y*2.0 + inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[3] = float2(inPixelHalfStep.x, inPixelStep.y + inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[4] = float2(inPixelStep.x + inPixelHalfStep.x, inPixelStep.y + inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[5] = float2(inPixelStep.x*2.0 + inPixelHalfStep.x, inPixelStep.y + inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[6] = float2(inPixelHalfStep.x, inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[7] = float2(inPixelStep.x + inPixelHalfStep.x, inPixelHalfStep.y) + pixelBin * texCoordsStep;
     texCoords[8] = float2(inPixelStep.x*2.0 + inPixelHalfStep.x, inPixelHalfStep.y) + pixelBin * texCoordsStep;
    
     //take average of 9 pixel samples
     avgColor = tex2D(_MainTex, texCoords[0]) +
                         tex2D(_MainTex, texCoords[1]) +
                         tex2D(_MainTex, texCoords[2]) +
                         tex2D(_MainTex, texCoords[3]) +
                         tex2D(_MainTex, texCoords[4]) +
                         tex2D(_MainTex, texCoords[5]) +
                         tex2D(_MainTex, texCoords[6]) +
                         tex2D(_MainTex, texCoords[7]) +
                         tex2D(_MainTex, texCoords[8]);
                        
      avgColor /= float(9.0);
     
      //blend between fragments in the circle and out of the circle defining our "pixel region"
     //Equation of a circle: (x - h)^2 + (y - k)^2 = r^2
     float2 powers = pow(abs(pixelRegionCoords - 0.5),float2(2.0, 2.0));
     float radiusSqrd = pow(_Radius,2.0);
     float gradient = smoothstep(radiusSqrd-_Feather, radiusSqrd+_Feather, powers.x+powers.y);
     
      fixed4 fragcolor = lerp(avgColor, float4(0.1,0.1,0.1,0.0), gradient);
      return fragcolor;
            }

            ENDCG
        }
    }
}

If you try it you’ll see that you can very easily create an LED display. That is until you watch it from a distance. See the first post to see what I mean.
Now I tried to apply supersampling and shader antialising. I even tried the techniques from this GLSL shader.

I just can’t seem to make this work in the LED shader. Can someone change the code to implement some supersampling technique to make sure the LED display still looks OK from a distance?
Any help would be greatly appreciated.
Thanks you.

You could go back to using a texture, but have the texture be a circular gradient that you repeat per pixel and use for the LED radius calculation. You’ll get some of the beneficial aspects of mip mapping, but it might not look right. Use an A8 texture and make sure it’s authored in linear color space and not in gamma space.

Another way to handle this is analytically. Figure out how far away the pixel on screen is and compare that with the intended size of an LED. If the LED radius is smaller than a pixel make the radius larger and dim it.

Thank you for the tip. I can not use a texture because my client needs to change the radius of each LED light.
I create the circle procedurally and feather the edges based on the fwidth value of each UV region. Each LED light has it’s own UV region.
The moiré is less pronounced now, but still present. Moiré makes my LED display worthless. I’m sure a little bit of supersampling would help, but I have no idea how to implement it into the shader. I don’t know how to implement supersampling. If anyone has a golden tip for me, I would greatly appreciate it because my client is getting impatient.

I don’t think you read my post fully.

Use a circular gardient texture instead of calculating the circle from the UVs, basically:
float2 powers = pow(abs(pixelRegionCoords-0.5),float2(2.0, 2.0));
can be replaced with:
float2 powers = pow(tex2D(_CircleGradient, IN.uv0.xy/texCoordsStep), 2.0); // You don't want the frac UV here
Then you get the benefit of texture filtering and can still do dynamic LED sizes.

The analytical approach is a bit more involved, but you would use something like:
float coverage = _Radius / max(length(ddx(IN.uv0.xy/texCoordsStep)), length(ddy(IN.uv0.xy/texCoordsStep)); // probably wrong
Something like that can be used to see how big the LED is in on screen pixels. If you’re starting to get too small start making the LEDs bigger and fuzzier to simulate antialiasing.

Sorry to necrobump this almost 2yr old thread but @AnthonyAckerman did you get this fixed eventually? I’m working on the exact same problem. The shorter the LED-Pixel-Pitch (the closer the LEDs) and the smaller the LED-footprint, the more the moirée effect is visible. I’d appreciate very much if you could share how you went ahead with this back then. Thanks

I found this but can’t seem to find any contact information to get in touch with the guy who made it:

That video has a lot of moire in it as well, partially masked by the video compression, but it’s there. Alan Zucconi has an article on how to avoid the moire by fading the pixelated effect in and out based on distance.

Personally I would use derivatives instead of using distance, but distance does work and is easier for most people to get their head around even if it requires more manually tweaking.

1 Like

@bgolus thanks, but I think there’s more than just the fade between the real pixelated and a “fake” long distance view, I’m interested in more details about that shader