Work, Study/Unity

241129 투명 이미지를 마스크로 사용하기

Waltwaez 2024. 11. 29. 00:42

1. 선요약

이런 UI를 만들어보겠다..고 섣불리 접근했다가 하루 종일 고생했다.

 

결과물

 

2. 내용

유니티에서 스텐실 마스크 라는 것을 이용한다. 아래의 과정을 거침.

 

  1. 마스크 오브젝트가 먼저 렌더링, 스텐실 버퍼에 특정한 값을 기록한다.
  2. 이후 렌더링되는 오브젝트들은 스텐실 버퍼의 값을 확인한다.
  3. 설정 규칙(스텐실 테스트)에 따라 픽셀을 그릴지 말지를 결정한다.

 

유니티에서는 셰이더의 UI/Default 에서 이용할 수 있다.

 

2.1. 2개의 머티리얼 만들기

스텐실 마스크를 적용하기 위해서는 우선 어떤 이미지를 올리면서 동시에 그 이미지에 스텐실 버퍼 라는 것을 올려야 한다.

나중에 렌더링되는 이미지는 같은 픽셀의 스텐실 버퍼 값을 확인해서 조건에 따라 자신이 가진 이미지를 렌더링할지 말지 결정한다.

 

우선, 스텐실 버퍼를 작성하는 머티리얼인 StencilWrite 을 작성한다.

enum으로 각 요소를 만들어놔서 괜히 헷갈리는데,

  • Stencil Comparison = 8 이라면 항상 픽셀에 스텐실 버퍼 값 `Stencil ID`를 그린다.
  • Stencil Operation = 2 라면 해당 버퍼의 값을 `Stencil ID` 값으로 대체한다.

 

쉽게 말하면 나중에 렌더링될 요소들이 그 위치에 대한 렌더링 여부를 결정하는 맵을 그려놓는 것이다.

 

그리고 나중에 오는 오브젝트에 붙는 머티리얼인 StencilRead 을 작성한다.

  • Stencil Comparison = 5 라면 스텐실 버퍼 값이 참조값과 다른 경우에만 픽셀을 그린다.
  • 즉, 이미 스텐실 버퍼 값이 있는 곳에는 이 머티리얼이 붙은 이미지는 렌더링되지 않는다.
  • Stencil Operation = 0 이라면 버퍼 값을 변경하지 않는다. 즉 읽기 역할만 한다.

 

추가로, RenderQueue 도 쓰기 요소가 먼저 배치되고 그 다음에 읽기 요소가 렌더링되도록 쓰기를 하나 더 앞에 둔다.

 

개인적으로 준비한 예를 들자면,

속이 투명한 원형 스프라이트와 직사각형 스프라이트를 넣고

Circle 에 `StencilWrite` 머티리얼을, `Square`에 `StencilRead` 머티리얼을 배치하면

아래처럼 나타난다.

 

2.2. 라인을 따라 마스킹되도록 수정하기

여기선 살짝 야매(?)를 부렸는데, 원 스프라이트의 원 내부의 알파값이 0이 아니게끔 만들어놨다.

아주 미미한 알파값을 갖도록 스프라이트를 만들었는데, 이를 이용해서 셰이더에서 알파값이 0인 구간은 제거하도록 구현했다.

 

인스펙터에서는 해당 설정을 건드릴 수 없는 것으로 보여서 셰이더를 새로 만들고 머티리얼을 할당했다.

물론 난 셰이더에 관한 문법을 모르기 때문에 Claude한테 맡겼다.

 

  • Create > Shader 으로 셰이더를 새로 만들고 아래의 스크립트를 복붙.
Shader "UI/StencilWrite" {
    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
    }

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

        Stencil {
            Ref 1               // 스텐실 버퍼에 쓸 값
            Comp Always         // 항상 통과
            Pass Replace        // 스텐실 버퍼의 값을 Ref값으로 교체
            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"

            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
            };

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

            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 frag(v2f IN) : SV_Target {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
                
                // 알파값이 0.01 미만인 픽셀은 완전히 투명하게 처리하고 스텐실 버퍼에도 쓰지 않음
                if(color.a < 0.01)
                    discard;
                    
                return color;
            }
            ENDCG
        }
    }
}

 

이후 이 셰이더로 만든 머티리얼을 다시 Circle 에 할당하면

이런 식으로 원도 나타나면서 외곽선을 따라 마스킹이 되도록 구현할 수 있다.

이미지들의 화면과 색이 달라 보이는 건 이미지들이 비슷한 시간대에 찍힌 게 아니라서 그렇다.

이거저거 만지다보니..

 

... 정리하고 나니까 뭔가 쉽게 된 느낌이라 허무하지만

저 셰이더 스크립트를 받아내기까지 겁나 많은 시행착오를 겪으며 하루를 보냈다.

'Work, Study > Unity' 카테고리의 다른 글

241224 회전하는 화살 구현하기  (0) 2024.12.24
레이캐스트 관련 정리  (0) 2024.08.24
Awake, OnValidate, Initialize 차이점  (0) 2024.07.29
직렬화, 프로퍼티와 필드  (0) 2024.07.29