[2026_01_07] Unity UI Mask를 활용한 역마스크(Cutout Mask)

2026. 1. 7. 21:19TIL

문제 상황

튜토리얼 시스템을 구현하면서 화면 전체를 어둡게(Dimmed) 처리하되, 특정 UI만 밝게 하이라이트하여 클릭 가능하게 만들어야 했다.

해결 방법

1. Stencil Buffer를 활용한 Cutout Mask

일반적인 Mask는 마스크 영역만 보이게 하지만, Stencil Buffer의 CompareFunction.NotEqual을 사용하여 역마스크(마스크 영역은 숨기고 나머지만 보임)를 구현했다.

 
 
csharp
public class CutoutMaskUI : Image
{
    public override Material materialForRendering
    {
        get
        {
            Material material = new Material(base.materialForRendering);
            material.SetInt("_StencilComp", (int)CompareFunction.NotEqual);
            return material;
        }
    }
}

2. UI 계층 구조

TutorialOverlay
├── RaycastBlocker (투명, 전체 화면 클릭 차단)
├── MaskImage (타겟 UI 위치/크기로 이동)
│   └── Dimmed (CutoutMaskUI, 화면 전체 크기)
├── GuideText
└── Arrow

3. 핵심 문제와 해결

문제: Dimmed가 Mask의 자식이라 Mask가 이동하면 Dimmed도 따라 이동해서 화면 전체를 못 덮음

해결: Dimmed를 Mask 이동 방향의 반대로 상쇄 이동시켜 화면 중앙에 고정

 
 
csharp
// Mask가 (100, 50)으로 이동
maskImage.anchoredPosition = localPos;

// Dimmed를 반대 방향으로 (-100, -50) 이동
dimmedBackground.rectTransform.anchoredPosition = -localPos;

// 결과: Dimmed의 실제 화면 위치 = (100, 50) + (-100, -50) = (0, 0)

핵심 코드

 
 
csharp
private void UpdateMask(RectTransform target)
{
    maskImage.gameObject.SetActive(true);

    // 1. 타겟 UI의 월드 코너 좌표 가져오기
    Vector3[] targetCorners = new Vector3[4];
    target.GetWorldCorners(targetCorners);

    // 2. Tutorial Canvas 로컬 좌표로 변환
    RectTransform canvasRect = tutorialCanvas.GetComponent<RectTransform>();
    Vector2[] localCorners = new Vector2[4];
    
    for (int i = 0; i < 4; i++)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            canvasRect, 
            RectTransformUtility.WorldToScreenPoint(null, targetCorners[i]), 
            null, 
            out localCorners[i]
        );
    }

    // 3. Mask 위치와 크기 설정
    Vector2 localPos = (localCorners[0] + localCorners[2]) / 2f;
    float width = Mathf.Abs(localCorners[3].x - localCorners[0].x);
    float height = Mathf.Abs(localCorners[1].y - localCorners[0].y);

    maskImage.anchoredPosition = localPos;
    maskImage.sizeDelta = new Vector2(width, height);

    // 4. Dimmed를 반대 방향으로 상쇄 이동 (핵심!)
    dimmedBackground.rectTransform.anchoredPosition = -localPos;
}

추가 구현 사항

  • 타겟만 클릭 가능하게: 타겟 UI에 별도 Canvas를 추가하여 sortingOrder를 높게 설정
  • 다른 Canvas 간 좌표 변환: RectTransformUtility.ScreenPointToLocalPointInRectangle 활용
  • 실제 렌더링 크기 계산: GetWorldCorners()로 LayoutElement, DOTween 등이 반영된 실제 크기 계산

배운 점

  1. Unity UI Mask는 Stencil Buffer 기반이라 역마스크 구현이 가능하다
  2. 부모-자식 관계의 RectTransform은 좌표가 상대적이므로 반대 이동으로 상쇄 가능하다
  3. 서로 다른 Canvas 간 UI 위치를 맞출 때는 월드 좌표를 거쳐 변환해야 한다