WinCNT

URP에서 Cutout(Alpha Test) 구현해보기(feat. Alpha to Coverage) 본문

Unity/URP or Shader 관련

URP에서 Cutout(Alpha Test) 구현해보기(feat. Alpha to Coverage)

WinCNT_SSS 2023. 12. 6. 12:11

서론

URP 환경에서 AlphaTest 셰이더를 구현하면서 알게 된 내용을 정리해보고자 한다


Cutout? Alpha Test? Alpha Clipping?

이번에 조사하면서 알게된 점은 이 셰이더가 정말 다양한 명칭으로 불린다는 것이었다

일단 이번에 구현하려고 하는 것은 알파 값으로 지정된 부분은 그리지 않고, 그 외의 부분은 그리는 셰이더이다

예를 들어, 다음과 같은 텍스처가 있다고 했을 때

좌) RGB(초록색으로 나오는 이유는 몰?루) / 우) 알파 값

 

다음과 같이 출력하는 것이 목표이다(아래는 디폴트 머테리얼을 사용한 결과)

 

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 스크롤에서는 완전 이상하게 보이게 되니 주의해서 사용하자

좌) "RenderType"="TransparentCutout” / 우) "RenderType"="Opaque"


Alpha Blending과의 차이

Cutout에서의 알파는 투명도를 나타내지 않는다

해당 픽셀을 그릴지 안 그릴지를 나타내는 값이며, 따라서 Cutout에는 반투명이 없다

 

Cutout는 알파 소팅이 일어나지 않기 때문 알파 블렌딩에 비해 더 가볍다는 장점이 있다

하지만 알파 테스트를 하는 외곽선 부분에서 계단 현상이 심해질 수 있다는 단점이 있다

(알파 채널의 값과 Cutoff 값으로 어느 정도 조절할 순 있겠지만 한도가 있음)


Cutoff에서 계단 현상은 어쩔 수 없나?

그럼 알파 테스트의 계단 현상을 어떻게 할 수는 없을까?

이에 대한 답으로 Alpha to Coverage이 있다며, 아래의 사이트를 사수님께서 알려주셨다(야호!)

Anti-aliased Alpha Test: The Esoteric Alpha To Coverage

 

Anti-aliased Alpha Test: The Esoteric Alpha To Coverage

Aliasing is the bane of VR. We have several tools we use to try to remove, or at least reduce aliasing in real time graphics today. Tools…

bgolus.medium.com

일단 위의 사이트를 정리하면서 구현해보자


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 매뉴얼

 

ShaderLab 커맨드: AlphaToMask - Unity 매뉴얼

GPU에서 알파 투 커버리지 모드를 활성화하거나 비활성화합니다.

docs.unity3d.com

그 결과는 다음과 같다

좌) AlphaToMask Off / 우) AlphaToMask On

 

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

 

Anti-aliased Alpha Test: The Esoteric Alpha To Coverage

Aliasing is the bane of VR. We have several tools we use to try to remove, or at least reduce aliasing in real time graphics today. Tools…

bgolus.medium.com

Rendering Plants with Smooth Edges - Wolfire Games Blog

 

Rendering Plants with Smooth Edges - Wolfire Games Blog

When we are rendering many objects on screen at the same time, we only have a few hundred polygons to allocate to each individual plant. We use intersecting alpha-mapped planes to give the impression of much greater detail. Here is a picture of what our de

blog.wolfire.com

Unity - Manual: ShaderLab: legacy alpha testing

 

Unity - Manual: ShaderLab: legacy alpha testing

ShaderLab: legacy lighting ShaderLab: legacy texture combining ShaderLab: legacy alpha testing Note: The ShaderLabUnity’s language for defining the structure of Shader objects. More infoSee in Glossary functionality on this page is legacy, and is documen

docs.unity3d.com

유니티 반투명, 스텐실 개념 익히기

 

유니티 반투명, 스텐실 개념 익히기

목차

rito15.github.io

Deferred rendering and other project Settings for PC VR games

 

Deferred rendering and other project Settings for PC VR games

When developing VR for semi-powerful PCs, can I use deferred rendering, and in general give my graphics all of the bells and whistles afforded to...

forum.unity.com