본문 바로가기
IT

[Unity] 아웃 라인(Outline) 그리고 그림자 (shadow) 만들기 (component + shader 코드) / preserve aspect / cull transparent mesh

by 배애앰이 좋아 2025. 4. 13.
반응형

 

안녕하세요 

오늘은 유니티로 게임 개발하다가 유용한 정보를 찾게 되어서 작성해보게 되었습니다.

바로 아웃 라인 (outline) 기능과 그림자 (shadow) 기능입니다!
보통 아웃 라인 경우, 무언가를 강조할 때 많이 사용합니다. 특히 버튼 등 2D UI 쪽에 많이 적용되는데 기존에는 이런 효과를 만들기 위해서는 unity shader 를 사용했어야 했는데 이 shader 쪽을 활용할려면 유니티 외 언어도 다르고 다르게 공부해야해서 어려웠습니다.

 

근데 알고보니 유니티 내에서 컴포넌트로 제공하는 것을 이제야 알았다는! 옛날 버전에도 있었는데 왜 진작에 몰랐는지.. 이래는 아는 만큼 고생을 덜 하는 것 같습니다. 저처럼 아웃라인 기능을 사용하고 싶고 쉽게 만들 수 방법을 찾는 사람들을 위해 이 글을 작성해봅니다. 

 


아주 간단하게 아웃라인을 그리고 싶은 이미지 게임 오브젝트 컴포넌트에 outline 컴포넌트를 검색해서 추가해주면 됩니다. 그럼 위에처럼 뜨는데 색상, 두께 등 조절할 수 있습니다. 그리고 코드로 outline.enabled = true; outline.enabled = false; 를 통해 껐다가 켰다가 조정하시면 됩니다.

 

여기서 unity > outline > use graphic alpha 경우, Outline 컴포넌트가 UI 요소 (예: Text, Image 등)의 알파값을 사용하여 외곽선을 그릴지 여부를 결정하는 속성입니다. 활성화되면, UI 요소의 알파값(투명도)을 외곽선 효과에 반영해 UI 요소의 텍스트나 이미지를 포함한 그래픽의 투명한 부분을 고려하여 외곽선을 그립니다. 

 

예를 들어, 텍스트나 이미지의 투명한 부분이 있다면, 외곽선이 그려지지 않거나, 투명한 부분에서는 외곽선이 나타나지 않게 됩니다. 반대로 비활성화 하게 되면, UI 요소의 알파값과 상관없이 외곽선이 항상 그려집니다. 즉, 텍스트나 이미지가 투명한 부분을 가지고 있더라도 외곽선이 항상 표시됩니다.

 


생각보다 잘 만들어져서 원리가 궁금했는데 외곽선 두께를 키우니 그 원리가 들어났습니다.
바로 해당 이미지를 여러장 복사해서 뒤에 넣는 방법입니다. 

 


그 때문에 외곽선이 굵어질 수록 이런 꼼수가 잘 보이고 기본 이미지에 영향을 받기 때문에 원하는 대로 색상이 안 나오는 경우도 있는 것 같습니다.

참고적으로 기존에 제가 외곽선을 그리고 위해 썼던 shader 코드 공유해드립니다. 
다만, 이 코드는 위와 같은 문제는 없지만 두껍게 잘 그려지지 않아서 위와 같은 방식으로 바꾸게 되었습니다.

 

Shader "Unlit/OutLine"
{
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _MainColor ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
        _OnOutline ("Outline (1=True)", Range (0, 1)) = 0
        _OutlineColor ("Outline Color", Color) = (1.0, 0.0, 0.0, 1.0)
        _OutlineThickness ("Outline Thickness", Range(1, 10)) = 2.0 // 아웃라인 두께를 조절하는 변수
    }
    
    CGINCLUDE
    
    #include "UnityCG.cginc"
    
    sampler2D _MainTex;
    uniform float4 _MainTex_TexelSize;
    fixed4 _MainColor;
    fixed4 _MainTex_ST;
    
    fixed _OnOutline;
    fixed4 _OutlineColor;
    float _OutlineThickness;  // 두께 변수
    
    struct v2f {
        fixed4 pos : SV_POSITION;
        fixed2 uv : TEXCOORD0;
        fixed4 vertexColor : COLOR;
    };
    
    v2f vert(appdata_full v) {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);   
        o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
        o.vertexColor = v.color * _MainColor;
        return o; 
    }
    
    fixed4 frag(v2f i) : COLOR {
        if (_OnOutline == 1) {
            // 아웃라인 두께에 따른 주변 픽셀 샘플링 범위 설정
            float thickness = _OutlineThickness;  // 아웃라인 두께
            
            // 두께에 맞게 주변 픽셀을 샘플링
            fixed s00 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness, -thickness)).a;
            fixed s01 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness / 2.0, -thickness)).a;
            fixed s02 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(0, -thickness)).a;
            fixed s03 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness / 2.0, -thickness)).a;
            fixed s04 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness, -thickness)).a;

            fixed s10 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness, -thickness / 2.0)).a;
            fixed s11 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness / 2.0, -thickness / 2.0)).a;
            fixed s12 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(0, -thickness / 2.0)).a;
            fixed s13 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness / 2.0, -thickness / 2.0)).a;
            fixed s14 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness, -thickness / 2.0)).a;

            fixed s20 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness, 0)).a;
            fixed s21 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness / 2.0, 0)).a;
            fixed s22 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(0, 0)).a;
            fixed s23 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness / 2.0, 0)).a;
            fixed s24 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness, 0)).a;

            fixed s30 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness, thickness / 2.0)).a;
            fixed s31 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness / 2.0, thickness / 2.0)).a;
            fixed s32 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(0, thickness / 2.0)).a;
            fixed s33 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness / 2.0, thickness / 2.0)).a;
            fixed s34 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness, thickness / 2.0)).a;

            fixed s40 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness, thickness)).a;
            fixed s41 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(-thickness / 2.0, thickness)).a;
            fixed s42 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(0, thickness)).a;
            fixed s43 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness / 2.0, thickness)).a;
            fixed s44 = tex2D(_MainTex, i.uv + _MainTex_TexelSize * float2(thickness, thickness)).a;

            // Sobel 필터 적용 (X, Y 방향)
            fixed sobelX = s00 + 2 * s10 + 3 * s20 + 2 * s30 + s40
                         - (s04 + 2 * s14 + 3 * s24 + 2 * s34 + s44);
            fixed sobelY = s00 + 2 * s01 + 3 * s02 + 2 * s03 + s04
                         - (s40 + 2 * s41 + 3 * s42 + 2 * s43 + s44);

            fixed edgeSqr = (sobelX * sobelX + sobelY * sobelY);
            _OutlineColor.a = edgeSqr;

            // 아웃라인을 그릴 때, 경계선과 함께 투명하지 않은 부분을 강제로 아웃라인 처리
            if (tex2D(_MainTex, i.uv.xy).a < 0.5 || edgeSqr > 0.1)  // 경계선 외에도 일정 강도의 투명도를 가진 부분도 처리
                return _OutlineColor;
        }

        return tex2D(_MainTex, i.uv.xy) * i.vertexColor;
    }

    ENDCG

    SubShader {
        Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
        Cull Off
        Lighting Off
        ZWrite Off
        ZTest Always
        Fog { Mode Off }
        Blend SrcAlpha OneMinusSrcAlpha

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest
            ENDCG
        }
    }
    FallBack Off
}

 


그 다음은 그림자 넣기입니다.
이 기능도 마찬가지로 이미지가 있는 게임 오브젝트에 shadow component를 넣어주면 됩니다.

 


동일하게 색상, 두께, 알파값 반영 여부를 조절할 수 있습니다. 
결과는 위와 같이 나타납니다. 마찬가지로 이미지를 복사해서 만들기 때문에 이미지의 색상 등 영향을 받습니다.

그리고 위에 부분들을 알아가면서 추가적으로 새롭게 알게 된 부분도 정리해봅니다. 

1. unity image > image type > simple > preserve aspect 속성은 UI 이미지가 표시될 때, 이미지의 원본 가로세로 비율을 유지하면서 크기를 조정할 수 있게 해주는 옵션입니다.
체크하게 되면, 이미지의 가로세로 비율을 유지하며 크기를 조정해줍니다. 만약 체크하지 않으면, 이미지가 강제로 지정된 RectTransform의 크기에 맞춰 늘어나거나 압축되어 이미지 왜곡이 일어납니다.

2. canvas renderer > cull transparent meshCanvas Renderer에서 투명한 부분을 처리하는 방법을 제어하는 속성입니다. 이 옵션을 사용하면 투명한 부분의 메시를 렌더링에서 제외하여 성능을 최적화해준다고 합니다. Cull Transparent Mesh가 활성화되면, 렌더러는 투명한 부분을 제외하고 불투명한 부분만 렌더링하며 Cull Transparent Mesh가 비활성화되면, 렌더러는 투명한 픽셀을 포함한 모든 부분을 렌더링하니 비용이 증가하게 됩니다. 투명한 부분이 많은 이미지를 넣게 될 경우, 같이 컴포넌트를 넣어두면 더 최적화할 수 있어서 좋은 것 같습니다.

여기까지 글 읽어주셔서 감사드리며 역시 아는 만큼 다양한 기능들을 쉽고 빠르게 만들 수 있는 것 같습니다. 

반응형

댓글