0
\$\begingroup\$

I am making a game for android using unity. The problem is that the resolution of the UI drops drastically when the canvas resolution is changed(for testing purposes).

Joystick(UI component) with resolution set to 1920 x 1080:

1920 x 1080 Resolution

Joystick with resolution set to 800 x 480:

800 x 480 Resolution

It's obvious that this happens due to changes in the resolution of the canvas, but is there a way that does not affect the quality of UI components while changing the canvas resolution?

Like different devices have different resolutions, and many games still have clear UI on these devices.

Import settings(Right) and the inspector(Left) of the sprite: enter image description here

As suggested in one of the comments below, I tried generating min maps(Import Settings > Advanced > Generate Min Maps). It looks better but still not on the point.

At 800 x 480 after generating min maps: At 800 x 480 after generating min maps

My Intention: Different mobile devices have different screen sizes and hence, different resolutions. So, if a device has the resolution 1920x1080, the UI would look fine, but if with a different resolution, the UI would either shrink or expand to fit the screen size. That is fine but this expansion and contraction would affect the quality of UI. A UI component that would fit clearly with devices of different resolutions is my intention.

Any alternatives would also be appreciated.

(This is a 3D game)

\$\endgroup\$
1
  • \$\begingroup\$ Did you try the suggestions starting with "To avoid this..." in DMGregory's answer? \$\endgroup\$
    – Pikalek
    May 29 at 2:00

1 Answer 1

2
\$\begingroup\$

The lower quality you're observing is due to texture sampling artifacts.

The sprite's texture stores what colour each pixel should be when the sprite is displayed at exactly its original size. When you shrink the sprite to cover fewer pixels, you can cause spatial aliasing - you're sampling the underlying texture's signal at a lower frequency than the highest frequencies it contains.

Mipmapping can help with this, by pre-computing smaller versions of the sprite, so the graphics card can select the one closest to the actual display size and reduce aliasing. (This also improves texture cache performance and can actually speed up rendering, despite the extra memory cost)

But mipmapping and bilinear filtering work by averaging nearby pixels, which can make the result look slightly fuzzy.

To avoid this, the best thing you can do in general is to author sprites at their intended display resolution, and display them at that size. Unity has features that will allow you to switch between sprite assets depending on your display resolution, to ensure you're using the best match for your current display.

For single-colour UI sprites like this though, we can take another approach and render them using a signed distance field. In this form, instead of storing the colour to draw at each point, the sprite texture stores each pixel's distance inside or outside the shape to be drawn, measured from the closest edge. This distance field interpolates and scales better than a raw colour image. We then use some shader math to determine how close each screen pixel is to the edge of the shape, and correctly anti-alias it.

The distance field can look like this:

Distance field sprites

To make this, I created the shapes I wanted in a vector drawing program, and set the stroke around the shapes to be a black-to-white gradient.

Animation showing anti-aliasing

Each frame, the shader interprets those gradients vs the sprite's scaling to dynamically draw an anti-aliased edge in the right place.

Here's an SDF shader compatible with Unity's sprite and UI image renderers:

Shader "SDF/Sprite"
{
    Properties
    {
        [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
        _Color("Tint", Color) = (1,1,1,1)
        _Dilate("Dilate", Range(-0.45, 0.45)) = 0
        [MaterialToggle] PixelSnap("Pixel snap", Float) = 0
        [HideInInspector] _RendererColor("RendererColor", Color) = (1,1,1,1)
        [HideInInspector] _Flip("Flip", Vector) = (1,1,1,1)


        [HideInInspector] _StencilComp("Stencil Comparison", Float) = 8
        [HideInInspector] _Stencil("Stencil ID", Float) = 0
        [HideInInspector] _StencilOp("Stencil Operation", Float) = 0
        [HideInInspector] _StencilWriteMask("Stencil Write Mask", Float) = 255
        [HideInInspector] _StencilReadMask("Stencil Read Mask", Float) = 255
        [HideInInspector] _ColorMask("Color Mask", Float) = 15
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "PreviewType" = "Plane"
            "CanUseSpriteAtlas" = "True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment SDFSpriteFrag
            #pragma target 2.0
            #pragma multi_compile_instancing
            #pragma multi_compile_local _ PIXELSNAP_ON
            #pragma multi_compile _ ETC1_EXTERNAL_ALPHA

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;
            float _Dilate;


            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                OUT.color = v.color * _Color;
                return OUT;
            }



            fixed4 SDFSpriteFrag(v2f IN) : SV_Target
            {
                fixed4 c = IN.color;

#ifdef UNITY_UI_CLIP_RECT
                c.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
                

                float a = tex2D(_MainTex, IN.texcoord).r;

                float2 gradient = float2(ddx(a), ddy(a));

                float gradientLength = length(gradient);

                float pixelsFromEdge = (a - 0.5f + _Dilate) / gradientLength;

                c.a *= smoothstep(0, 1, pixelsFromEdge + 0.5f);

                c.rgb *= c.a;
                return c;
            }
            ENDCG
        }
    }
}

This lets you get crisp vector-like edges for single-colour sprites, over a wider range of resolutions than a single conventional sprite texture. It also makes it easier to apply special effects, like dilating or thinning the shapes, adding glows or shadows, etc.

Text Mesh Pro uses this technique to support font rendering in Unity.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .