일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 3d
- 벡터
- working set
- Private Bytes
- Three(Two) Tone Shading
- Cell Shader
- ASW(Application SpaceWarp)
- 프로그래밍 기초
- Toon Shader
- VR
- URP
- C언어
- Cartoon Rendering
- URP로 변경
- 메모리 누수
- Rim Light
- OculusMotionVectorPass
- Virtual Byte
- Specular
- 게임 수학
- ColorGradingLutPass
- Cell Look
- 개인 바이트
- 가상 바이트
- 작업 집합
- Windows Build
- AppSW
- Today
- Total
WinCNT
URP에서 Cutout(Alpha Test) 구현해보기(feat. Alpha to Coverage) 본문
URP에서 Cutout(Alpha Test) 구현해보기(feat. Alpha to Coverage)
WinCNT_SSS 2023. 12. 6. 12:11서론
URP 환경에서 AlphaTest 셰이더를 구현하면서 알게 된 내용을 정리해보고자 한다
Cutout? Alpha Test? Alpha Clipping?
이번에 조사하면서 알게된 점은 이 셰이더가 정말 다양한 명칭으로 불린다는 것이었다
일단 이번에 구현하려고 하는 것은 알파 값으로 지정된 부분은 그리지 않고, 그 외의 부분은 그리는 셰이더이다
예를 들어, 다음과 같은 텍스처가 있다고 했을 때
다음과 같이 출력하는 것이 목표이다(아래는 디폴트 머테리얼을 사용한 결과)
DirectX에서는 Alpha Test, OpenGL에서는 Cutout이라고 부른다고 하는데, 일단 여기서는 Cutout라고 부르기로 한다
(Alpha Test는 좀 더 광의적인 의미도 있고, Alpha Clipping는 URP의 Lit셰이더에서의 이름이니…)
참고로 Cutoff라는 이름으로도 가끔 불리기는 하는데, 정확히는 알파 값을 조정하는 Threshold의 내부 프로퍼티인 경우가 많았다
우선 구현!
1차적인 구현은 정말 단순하다
태그랑 프래그먼트 셰이더를 조금 수정하면 된다
SubShader
{
Tags
{
"Queue"="AlphaTest"
"RenderType"="TransparentCutout"
"RenderPipeline"="UniversalPipeline"
}
Pass
{
Name "Universal Forward"
Tags
{
"LightMode"="UniversalForward"
}
Cull [_CullMode]
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// 생략...
// ...
CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
CBUFFER_END
// 버텍스 셰이더는 생략...
// ...
float3 frag(v2f i) : SV_Target
{
float4 color = tex2D(_MainTex, i.uv);
clip(color.a - _Cutoff);
// 라이트 처리 등등은 생략...
// ...
return color;
}
ENDHLSL
}
}
Queue태그를 AlphaTest로 하면 Render Queue가 2450로 설정되며, Geometry와는 별도의 렌더 큐를 가지게 된다
(불투명 오브젝트를 그리고나서 컷오프된 오브젝트를 그리는 것이 효과적이기 때문에 별도의 큐를 가진다)
참고로 RenderType는 (이제는 거의 레거시라고 하는)대체 셰이더에 필요한 태그이다
컷오프 셰이더에는 통례적으로 TransparentCutout를 설정한다
사실 거의 레거시라고 해서 아무런 영향이 없는 것은 아니고, _MainTex나 [MainTexture]가 붙은 텍스처가 있으면 씬 뷰에서 다음과 같이 영향을 준다
일반적으로 있는게 좋은데 UV 스크롤에서는 완전 이상하게 보이게 되니 주의해서 사용하자
Alpha Blending과의 차이
Cutout에서의 알파는 투명도를 나타내지 않는다
해당 픽셀을 그릴지 안 그릴지를 나타내는 값이며, 따라서 Cutout에는 반투명이 없다
Cutout는 알파 소팅이 일어나지 않기 때문 알파 블렌딩에 비해 더 가볍다는 장점이 있다
하지만 알파 테스트를 하는 외곽선 부분에서 계단 현상이 심해질 수 있다는 단점이 있다
(알파 채널의 값과 Cutoff 값으로 어느 정도 조절할 순 있겠지만 한도가 있음)
Cutoff에서 계단 현상은 어쩔 수 없나?
그럼 알파 테스트의 계단 현상을 어떻게 할 수는 없을까?
이에 대한 답으로 Alpha to Coverage이 있다며, 아래의 사이트를 사수님께서 알려주셨다(야호!)
Anti-aliased Alpha Test: The Esoteric Alpha To Coverage
일단 위의 사이트를 정리하면서 구현해보자
Alpha to Coverage
일단 Alpha to Coverage는 Multi Sampling Anti-Aliasing(MSAA, 다중 샘플링 앤티 앨리어싱MSAA)과 같이 사용하는 기법 중 하나이다
우선 참고 사이트의 내용을 바탕으로 매우 간략하게 설명하자면, MSAA는 우선 깊이 커버리지 샘플로 외곽선인지 아닌지를 검출하고, 외곽선일 경우에는 색상을 샘플링하는 방식이다
그래서 화면 전체에 대해서 샘플링하는 SSAA보다 가볍지만, 외곽선이 아닌 부분에 대해서는 안티 앨리어싱이 적용되지 않는다
아무튼! Alpha to Coverage는 MSAA에서 적용 가능한 기법이다
Alpha to Coverage를 On으로 하면, 외곽선일 경우 샘플링하기 위해 저장하는 색상에 알파 값을 적용한다
4x MSAA인 경우에는 4개의 샘플을 생성하니 0~4개까지의 5단계의 반투명도를 얻을 수 있다
(물론 그냥 알파 블렌드를 사용하면 256단계의 반투명도를 얻을 순 있지만…)
설명이 이리저리 길어졌지만 유니티에서 Alpha to Coverage를 설정하는 방법은 매우 간단하다
셰이더에 AlphaToMask On를 추가하면 된다
ShaderLab 커맨드: AlphaToMask - Unity 매뉴얼
그 결과는 다음과 같다
5배로 확대해보니 외곽선 부분에 샘플링이 적용된 것을 확인할 수 있었다
Alpha to Coverage(Sharpened)
하지만 여기서 끝이 아니다!
Alpha to Coverage를 On으로 하는 것만으로는 품질이 그다지 향상되지 않는다
특히 윤곽선 부분이 심하게 뭉개지는 경우가 발생한다(참고 사이트의 이미지)
여기서 필요한 게 fwidth()란 함수이다
MS의 공식 문서를 찾아보면 ,fwidth()는 특정 값의 편미분(…)의 절대값을 반환하는 함수이며,abs(ddx(x)) + abs(ddy(x))와 같은 함수라고 한다
너무 어렵게 설명되어 있어서 뭐라는 거지…라고 할 수 있는데, 요컨대 주변 픽셀과의 변화량을 반환하는 함수라고 생각하면 될 것 같다
사실 필자도 자세히는 몰라서 실제로 어떤 값이 반환되는지 실험해봤다
float3 frag(v2f i) : SV_Target
{
float4 color = tex2D(_MainTex, i.uv);
return fwidth(color.a);
}
그러자 다음과 같이 알파 값의 경계선이 깔끔하게 출력됐다!
여기까지 왔다면 Ben Golus 형님이 있을 뿐…fwidth() 사용해서 소스 코드를 수정했다
AlphaToMask On를 추가하고 프래그먼트 셰이더의 리턴 값의 타입을 float4로 바꾸고, clip 대신 리턴 값의 알파를 이용하는 등등
SubShader
{
Tags
{
"Queue"="AlphaTest"
"RenderType"="TransparentCutout"
"RenderPipeline"="UniversalPipeline"
}
Pass
{
Name "Universal Forward"
Tags
{
"LightMode"="UniversalForward"
}
Cull [_CullMode]
AlphaToMask On
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// 생략...
// ...
CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
CBUFFER_END
// 버텍스 셰이더는 생략...
// ...
float4 frag(v2f i) : SV_Target
{
float4 color = tex2D(_MainTex, i.uv);
// clip(color.a - _Cutoff);
color.a = (color.a - _Cutoff) / max(fwidth(color.a), 0.0001) + 0.5;
// 라이트 처리 등등은 생략...
// ...
return color;
}
ENDHLSL
}
}
그 결과는 다음과 같다
수정 전이랑 비교했을 때 윤곽선 부분이 더욱 부드러워진 것이 보인다
참고 사이트에 의하면 다음과 같은 품질 차이가 발생한다고 한다
Alpha to Coverage(Mipmap)
하지만 Ben Golus 형님의 말에 의하면 Alpha to Coverage는 밉맵에도 대응해야 한다고 한다
잘 생각해보면 알파 값도 밉 매핑으로 간략화될텐데, Cutoff의 경우에는 아티팩트가 눈에 띌 것 같다
실제로 참고 사이트의 예시를 보면 알파 블렌딩하고 심하게 차이났다
이에 대한 대응으로 제일 간단한 것은 텍스처의 설정에서 Mip Maps Preserve Coverage라는 항목을 체크하는 것이다
이 방법은 미리 설정하는 것이기 때문에 따로 비용이 들지 않고 간편하다는 장점이 있다
그 외에는 프래그먼트 쉐이더에서 조정하는 방법이 있다
이쪽은 연산 비용이 들긴 하지만 그렇게 비싸진 않다고 한다
SubShader
{
Tags
{
"Queue"="AlphaTest"
"RenderType"="TransparentCutout"
"RenderPipeline"="UniversalPipeline"
}
Pass
{
Name "Universal Forward"
Tags
{
"LightMode"="UniversalForward"
}
Cull [_CullMode]
AlphaToMask On
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// 생략...
// ...
CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
half _Cutoff;
half _MipmapScale;
CBUFFER_END
float CalcMipLevel(float2 texture_coord)
{
float2 dx = ddx(texture_coord);
float2 dy = ddy(texture_coord);
float delta_max_sqr = max(dot(dx, dx), dot(dy, dy));
return max(0.0, 0.5 * log2(delta_max_sqr));
}
// 버텍스 셰이더는 생략...
// ...
float4 frag(v2f i) : SV_Target
{
float4 color = tex2D(_MainTex, i.uv);
color.a *= 1 + max(0, CalcMipLevel(i.uv * _MainTex_TexelSize.zw)) * _MipmapScale;
// clip(color.a - _Cutoff);
color.a = (color.a - _Cutoff) / max(fwidth(color.a), 0.0001) + 0.5;
// 라이트 처리 등등은 생략...
// ...
return color;
}
ENDHLSL
}
}
아래는 소스 코드로 구현한 결과이다
아래는 URP Lit 쉐이더의 컷아웃(왼쪽)과 코드로 구현한 컷아웃(오른쪽)을 비교한 영상이다
잘 보면 왼쪽에서 아티팩트가 더 잘 보이는 것 같은 느낌이 든다
화질이 좋기 않기 때문에 잘 안 보일 순 있다
마무리
컷아웃(알파 쉐이더)을 사용하면 외곽선이 자글자글해지는 건 어쩔 수 없는 트레이드 오프라고 생각하고 있었는데, 이번을 통해 Alpha to Coverage로 완화할 수 있다는 사실을 알게 되었다
물론 MSAA 사용이 전제이기 때문에 일반적으로 디퍼드 렌더링인 경우에는 쓸 수 없겠지만…
VR의 경우, 현재로써는 포워드 렌더링이 주류이고, VR이 아니더라고 모든 프로젝트가 디퍼드 렌더링 중심은 아닐테니 알고 있어서 손해는 없을 것 같다
참고로 닭이 먼저냐 달걀이 먼저냐의 이야기이긴 한데, VR 프로젝트에서 포워드 렌더링을 채택하는 것은 MSAA 사용할 수 있기 때문이라고도 한다
(FXAA나 SMAA가 VR의 앤티 앨리어싱에 특히 도움이 안 된다고도 함)
참고 사이트
Anti-aliased Alpha Test: The Esoteric Alpha To Coverage
Rendering Plants with Smooth Edges - Wolfire Games Blog
Unity - Manual: ShaderLab: legacy alpha testing
Deferred rendering and other project Settings for PC VR games
'Unity > URP or Shader 관련' 카테고리의 다른 글
MatCap 셰이더를 만들어보자 (0) | 2024.01.15 |
---|---|
UniTask를 이용한 스크립트로 포스트 프로세싱이 아닌 Fade In, Fade Out를 구현해보기 (0) | 2023.12.15 |
커스텀 셰이더의 머티리얼을 Bakery(라이트 맵 생성 에섯)으로 Bake하기(feat. Meta Pass) (0) | 2023.10.12 |
URP에서 SpriteRenderer는 SRP Batcher가 작동하지 않았던 이유 조사 (0) | 2023.09.28 |
유니티의 Built-in Shader의 소스 코드를 다운로드 하기 (0) | 2023.07.18 |