본문 바로가기

[unity] 2D Circle Mask Shader

앤디가이 2022. 5. 27.

Unity에서 Circle 형태의 Masking을 처리하는 Shader에 대해서 알아보자.

 

1. 기존 Masking 사용 방법

앱 개발 중 동그란 썸네일 및 동그란 아이콘 이미지를 구현해야 되는 상황이 생겼다.

간단하게 샘플을 만들어 보면, Icon 이미지와 Mask 이미지를 준비한다.

unity masking
unity masking

유니티 프로젝트에서 Canvas와 Panel을 만들고 아래와 같이 자식 구조를 해준다.

중요한 점은 MaskImage 게임오브젝트에 Mask 컴포넌트가 붙어 있어야 된다.

Unity Mask 자식 구조
자식 구조

실행해보면, 동그랗게 잘 마스킹이 잘 된 것을 확인할 수 있다.

Mask Image

이렇게 구성했을 경우 batch가 7이 된다.

 

 

2. Shader를 사용한 Masking 방법

Unity 프로젝트에서 Create-> Shader 를 클릭한 후 아무 셰이더를 1개 만들어 준다.

이름을 UICircleShader 로 변경해준다.

그다음, Create-> Material을 생성한 후 UICircleMaterial로 이름을 만들어 준다.

UICircleShader.shader를 더블 클릭하여, 코드를 열어 준 후. 기존 코드는 삭제.

아래 코드를 넣어준 후 컴파일해준다.

/* circle mask shader */

Shader "UI/CircleMask"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15
        
        _RadiusX ("Radius X", Range(0,1)) = 1
        _RadiusY ("Radius Y", Range(0,1)) = 1
        
        _ScaleX ("Scale X", Float) = 1
        _ScaleY ("Scale Y", Float) = 1

        _AntialiasThreshold ("Antialias Threshold", Range(0, 0.9999)) = 0.96

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

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

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

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

            #pragma multi_compile __ UNITY_UI_CLIP_RECT
            #pragma multi_compile __ UNITY_UI_ALPHACLIP

            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;
                float2 originalTexcoord : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            fixed4 _Color;
            float4 _ClipRect;
            float4 _MainTex_ST;
            float _RadiusX;
            float _RadiusY;
            float _ScaleX;
            float _ScaleY;
            float _AntialiasThreshold;

            float circleDelta(float2 texcoord) {
                if (_RadiusX <= 0.0001 || _RadiusY <= 0.0001) return 0;
                float x = (2 * (texcoord.x - 0.5)) / _RadiusX;
                float y = (2 * (texcoord.y - 0.5)) / _RadiusY;
                float value = x * x + y * y;
                
                return smoothstep(1.0, _AntialiasThreshold, value);
            }

            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.originalTexcoord = v.texcoord;
                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

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

            fixed4 frag(v2f IN) : SV_Target
            {
                // scale before circle clipping
                float2 size = float2(1 / _ScaleX, 1 / _ScaleY);
                float2 halfSize = size * 0.5;
                float2 center = float2(0.5, 0.5);
                half4 color = tex2D(_MainTex, IN.texcoord * size + center - halfSize) * IN.color;
    
                color.a *= circleDelta(IN.texcoord);

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

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                return color;
            }
        ENDCG
        }
    }
}

 

컴파일이 완료된 후. UICircleMaterial의 Shader를 UI/CircleMask로 변경해준다.

 

다시 Scene에 캔버스를 만들고, 패널 밑에 UIImage 객체를 하나 만들어 준다.

Image의 source는 Icon을 넣어준 후, material 항목에 UICircleMaterial을 설정해 준다.

UICircleMask Material 적용
UICircleMask Material 적용

실행해보면, 마스크 컴포넌트 없이도, 동그랗게 잘 마스킹된 아이콘을 볼 수 있다!!

셰이더로 마스킹 한 아이콘
셰이더로 마스킹 한 아이콘

여기서 중요한 점은! Batches 가 7-> 5로 줄어든 점이다.

UI 요소가 많고 특히 Masking 컴포넌트를 사용하면 이 Batch가 많아지고 드로우 콜이 많아지면서 퍼포먼스가 떨어진다.

 

지금은 한 개의 아이콘만 적용했지만, 화면에 들어가는 아이콘이 많아질수록 퍼포먼스 격차는 더 늘어날 것이다.

셰이더를 잘 활용하여, 앱의 퍼포먼스를 확실히 높일 수 있다!

 

댓글