1. 선요약
이런 UI를 만들어보겠다..고 섣불리 접근했다가 하루 종일 고생했다.
결과물
2. 내용
유니티에서 스텐실 마스크 라는 것을 이용한다. 아래의 과정을 거침.
- 마스크 오브젝트가 먼저 렌더링, 스텐실 버퍼에 특정한 값을 기록한다.
- 이후 렌더링되는 오브젝트들은 스텐실 버퍼의 값을 확인한다.
- 설정 규칙(스텐실 테스트)에 따라 픽셀을 그릴지 말지를 결정한다.
유니티에서는 셰이더의 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 |