UGUI에서 Mask나 RectMask2D 없이, MaskableGraphic의 정점 생성만으로 역 마스크(구멍 뚫린 마스크)를 구현하고 클릭 투과까지 처리하는 HollowOutMask 컴포넌트를 만드는 방법을 소개합니다.
UGUI에서 부분적으로 하이라이트를 주고 싶은 장면, 특히 튜토리얼 중 유도 UI에서는 "특정 버튼만 밝게 보이고, 나머지 영역을 반투명으로 덮는" 연출이 자주 쓰입니다.
일반적으로는 Mask나 RectMask2D를 사용하지만, 이들에는 몇 가지 과제가 있습니다.
Mask: 스텐실 버퍼를 사용하므로, 드로우 코스트가 높음RectMask2D: 사각형 영역만 다룰 수 있고, 구멍 뚫기 표현(역 마스크) 는 불가능그래서 이번에는 MaskableGraphic을 상속하고, 완전한 프로그램 제어로 "역 마스크(구멍 뚫린 마스크)"를 생성하는 방법을 소개합니다.
최종적으로는, 하이라이트 부분은 클릭 가능하고, 다른 부분은 차단되는 고성능 HollowOutMask 컴포넌트를 구현해 보겠습니다.
| 기능 | 설명 |
|---|---|
| 시각 효과 | 자신의 RectTransform 영역 내에 반투명 마스크를 그리면서 지정한 RectTransform 부분을 "도려내기" |
| 레이캐스트 처리 | 구멍 부분은 클릭이 통과하고, 마스크 부분은 클릭을 차단 |
| 동적 추적 | 대상 오브젝트가 움직이거나 스케일이 변해도 도려낸 영역이 추적 |
이 모든 것을 Mask 계열 컴포넌트 없이, 정점 조작만으로 구현합니다.
HollowOutMask는 MaskableGraphic을 상속하고, OnPopulateMesh()를 오버라이드합니다.
상상해 봅시다.
마스크 전체를 나타내는 "바깥쪽 사각형(Outer Rect)"과, 도려낼 "안쪽 사각형(Inner Rect)"이 있다고 합시다.
이 두 사각형을 조합하여, 바깥쪽을 칠하면서 안쪽을 비우려면, 8개의 정점과 8장의 삼각형이 필요합니다.
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
if (_isTargetNull)
{
base.OnPopulateMesh(vh);
return;
}
// Outer Rect(自身のRectTransform基準)
float outerLx = rectTransform.rect.xMin;
float outerBy = rectTransform.rect.yMin;
float outerRx = rectTransform.rect.xMax;
float outerTy = rectTransform.rect.yMax;
vh.AddVert(new Vector3(outerLx, outerTy), color, Vector2.zero);
vh.AddVert(new Vector3(outerRx, outerTy), color, Vector2.zero);
vh.AddVert(new Vector3(outerRx, outerBy), color, Vector2.zero);
vh.AddVert(new Vector3(outerLx, outerBy), color, Vector2.zero);
// Inner Rect(穴部分)
float innerLx = _targetMin.x;
float innerBy = _targetMin.y;
float innerRx = _targetMax.x;
float innerTy = _targetMax.y;
vh.AddVert(new Vector3(innerLx, innerTy), color, Vector2.zero);
vh.AddVert(new Vector3(innerRx, innerTy), color, Vector2.zero);
vh.AddVert(new Vector3(innerRx, innerBy), color, Vector2.zero);
vh.AddVert(new Vector3(innerLx, innerBy), color, Vector2.zero);
// Triangles(外側と内側の間を埋める)
vh.AddTriangle(0, 1, 5); vh.AddTriangle(5, 4, 0);
vh.AddTriangle(1, 2, 6); vh.AddTriangle(6, 5, 1);
vh.AddTriangle(2, 3, 7); vh.AddTriangle(7, 6, 2);
vh.AddTriangle(3, 0, 4); vh.AddTriangle(4, 7, 3);
}
이렇게 하면, **중앙이 투명한 ‘링 형태(도넛형) 폴리곤’**을 생성할 수 있습니다.
포인트는 다음과 같습니다.
HollowOutMask의 핵심은, 대상의 위치나 크기가 변했을 때 도려낸 부분을 자동으로 갱신하는 점입니다.
void LateUpdate()
{
bool selfRectChanged = (_lastSelfRect != rectTransform.rect);
bool targetMatrixChanged = (_lastTargetMatrix != _target.localToWorldMatrix);
if (selfRectChanged || targetMatrixChanged)
{
ForceRefresh();
}
}
LateUpdate인가?UGUI의 레이아웃 갱신(LayoutGroup 등)은 Update 중에 이루어지므로,
LateUpdate에서 추적함으로써 **"한 프레임 지연"이나 "불일치"**를 방지합니다.
_target.localToWorldMatrix는 Transform의 모든 변경(위치·회전·스케일)을 반영하는 행렬입니다.
이를 캐시와 비교하여, 최소 비용으로 변화를 감지할 수 있습니다.
private void ForceRefresh()
{
_lastSelfRect = rectTransform.rect;
if (_isTargetNull) return;
_lastTargetMatrix = _target.localToWorldMatrix;
_target.GetWorldCorners(_targetWorldCorners);
Matrix4x4 selfWorldToLocal = rectTransform.worldToLocalMatrix;
Vector3 vMin = new(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 vMax = new(float.MinValue, float.MinValue, float.MinValue);
for (int i = 0; i < 4; i++)
{
Vector3 localPoint = selfWorldToLocal.MultiplyPoint3x4(_targetWorldCorners[i]);
vMin = Vector3.Min(vMin, localPoint);
vMax = Vector3.Max(vMax, localPoint);
}
_targetMin = vMin;
_targetMax = vMax;
SetVerticesDirty();
}
이를 통해, 월드 좌표 → 자신의 로컬 좌표 변환을 정확하게 수행하고,
Canvas에 "정점 데이터 갱신이 필요"함을 통지합니다.
마지막으로, 구멍 부분을 클릭이 통과하도록 설정합니다.
public bool IsRaycastLocationValid(Vector2 screenPos, Camera eventCamera)
{
if (!isActiveAndEnabled) return true;
if (_isTargetNull) return true;
// 구멍 부분은 Raycast 비활성(클릭 통과)
return !RectTransformUtility.RectangleContainsScreenPoint(_target, screenPos, eventCamera);
}
EventSystem으로부터의 "이 점은 명중했는가?"라는 물음에 대해:
false) → 아래 버튼까지 도달true) → 클릭 차단이라는 메커니즘이 됩니다.
| 요소 | 해설 |
|---|---|
| Mask나 RectMask2D를 쓰지 않음 | 퍼포먼스 개선과 유연성 확보 |
| 정점을 직접 생성 | 임의 형상의 마스크를 구현 |
| LateUpdate + 행렬 비교 | 빠르고 정확한 위치 추적 |
| ICanvasRaycastFilter | 구멍 부분의 클릭 투과를 간결하게 구현 |
이 방법을 사용하면,
"특정 UI를 하이라이트하면서, 다른 부분을 어둡게 덮는" 연출을, 경량이면서도 완전 제어로 구현할 수 있습니다.