[Unity] URP 셰이더 아웃라인 셰이더

2023. 6. 12. 19:45Unity

728x90
반응형

이 책의 예제로 오브젝트에 대한 URP 아웃라인 셰이더가 있다.

 

 

모델은 링크에서 무료로 받을 수 있고 아웃라인 셰이더가 적용되었다.

Shader "Custom/PassTest/OutlinePassTest"
{
    Properties
    {
        _BaseMap("Base Map", 2D) = "white" {}
        _OutlineColor("Outline Color", Color) = (1, 0, 0, 1)
        _OutlineDistance("Outline Distance", Float) = 0.1
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        
        Pass
        {
            Name "TestPassNameMain"

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                return color;
            }
            ENDHLSL
        }

        Pass
        {
            Name "TestNameOutline"
            Tags {"LightMode" = "Outline"} // ForwardRenderer 애셋의 렌더 피쳐에 Render Objects를 추가한 뒤 Filters > LightMode Tags에 Outline 을 추가하면 추가 패스로 그려짐
            ZWrite Off
            Cull Front

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
            };

            half4 _OutlineColor;
            half _OutlineDistance;

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                //OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                
                // 월드 노말 방식, 카메라 거리에 따른 원근 교정 X
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                float3 normalWS = mul(UNITY_MATRIX_M, IN.normalOS.xyz);
                positionWS += normalWS * _OutlineDistance;
                OUT.positionHCS = TransformWorldToHClip(positionWS);

                /*
                // 월드 노멀 방식, 카메라 거리에 따른 원근 교정 O
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                float3 normalWS = mul(UNITY_MATRIX_M, IN.normalOS.xyz);
                float distToCam = length(_WorldSpaceCameraPos - positionWS);
                positionWS += normalWS * _OutlineDistance * distToCam;
                OUT.positionHCS = TransformWorldToHClip(positionWS);

                // 스크린 노멀 방식, 카메라 거리에 따른 원근 교정 O
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                float3 clipNormal = TransformObjectToHClip(IN.normalOS * 100); // 100을 곱하는 이유 : 1 이하로 작은 값인 노말 방향은 클립스페이스의 퍼스펙티브가 적용되면서 화면 바깥쪽에서는 방향이 뒤집히는 왜곡이 발생하므로 클립 변환 전에 방향이 뒤집히지 않을 정도로 충분히 큰 벡터로 가공
                clipNormal = normalize(float3(clipNormal.xy, 0)); // 매우 큰 방향값을 정규화
                OUT.positionHCS.xyz += normalize(clipNormal) * _OutlineDistance * OUT.positionHCS.w; // 클립공간의 w 값은 카메라 공간의 z값과 같다. 즉, 카메라로부터 버택스까지의 거리
                */
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                //half4 retColor = half4(1, 0, 0, 1);
                //retColor.rgb = IN.positionHCS.y - 200;
                //return retColor;
                return _OutlineColor;
            }
            ENDHLSL
        }
    }
}

 

 

        Pass
        {
            Name "TestPassNameMain"
        Pass
        {
            Name "TestNameOutline"
            Tags {"LightMode" = "Outline"} // ForwardRenderer 애셋의 렌더 피쳐에 Render Objects를 추가한 뒤 Filters > LightMode Tags에 Outline 을 추가하면 추가 패스로 그려짐

셰이더에는 두 개의 Pass가 사용되었으며 첫 번째 Pass는 기본적으로 그린다. 하지만 두 번째 Pass는 Tags의 LightMode 값이 렌더 파이프라인에서 미리 약속된 이름이거나 렌더러에서 추가된 이름이어야 한다. 미리 약속된 이름들은 UniversalForward, UniversalGBuffer, ShadowCaster 등이 있고 여기서는 렌더러 피쳐를 추가해서 "Outline"이라는 이름을 직접 지정해야 한다.

 

 

프로젝트의 렌더러 피쳐에서 [LightMode Tags] 항목에 Element 0으로 "Outline"이 추가되어 있는 것을 볼 수 있다.

 

 

        Pass
        {
            Name "TestNameOutline"
            Tags {"LightMode" = "Outline"} // ForwardRenderer 애셋의 렌더 피쳐에 Render Objects를 추가한 뒤 Filters > LightMode Tags에 Outline 을 추가하면 추가 패스로 그려짐
            ZWrite Off
            Cull Front

계속 이어서 보면, ZWrite Off를 하며 그려지는 아웃라인 폴리곤이 실제 Z Buffer에 존재하지 않도록 한다. Cull Front는 뒷면만 그려지게 만든다.

 

 

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
            };

이후에 나오는 Attributes 구조체의 normalOS는 Vertex Shader의 IN.normalOS에서 사용하는데, NORAML 시멘틱이 적용되었기 때문에 렌더 파이프라인에 의해 버텍스의 노멀 벡터 값이 입력되어 들어온다.

 

 

                // 월드 노말 방식, 카메라 거리에 따른 원근 교정 X
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                float3 normalWS = mul(UNITY_MATRIX_M, IN.normalOS.xyz);
                positionWS += normalWS * _OutlineDistance;
                OUT.positionHCS = TransformWorldToHClip(positionWS);

이 부분은 아웃라인을 위해 노멀 방향으로 Vertex를 이동시키는 부분이다. 월드 공간에서 노멀 방향으로 이동시키기 위해 로컬 좌표인 버텍스 좌표를 월드 공간으로 변환한다. TransformObjectToWorld 함수에 의해 오브젝트 공간의 버텍스 위치인 IN.positionOS.xyz를 월드 공간으로 변환해서 positionWS에 저장한다.

이후 오브젝트 공간의 노멀 벡터인 IN.normalOS.xyz를 UNITY_MATRIX_M 행렬을 이용해서 월드 공간의 노멀 벡터로 변환한다. TransformObjectToWorld 함수를 쓰지 않고 UNITY_MATRIX_M을 곱한 이유는 w 정보를 추가로 처리하는데 이 부분이 노멀 벡터와 맞지 않는다.

월드 공간에서 버텍스의 위치와 노멀 벡터를 더해준다. 노멀 벡터는 OutlineDistance 스칼라 변수만큼 증폭되어 더해진다.

노멀 방향으로 OutlineDistance만큼 이동한 positionWS 좌표는 TransformWorldToHClip 함수에 의해 클립 공간으로 변환되어서 버텍스 셰이더의 결과물인 OUT 변수와 함께 반환된다. 이렇게 변환된 OUT 변수는 렌더 파이프라인에 의해 픽셀 단위로 보간 되어 Fragment Shader로 전달된다.

 

 

여기서 문제는 아웃라인을 위해 확장되는 거리가 언제나 일정하기 때문에 카메라에 가까이 확대되면 두꺼워지고 멀리서 보면 얇게 보인다. 원근 교정이 필요하다. 멀어지면 두껍게, 가까우면 얇게 보정해야 한다.

                // 월드 노멀 방식, 카메라 거리에 따른 원근 교정 O
                float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
                float3 normalWS = mul(UNITY_MATRIX_M, IN.normalOS.xyz);
                float distToCam = length(_WorldSpaceCameraPos - positionWS);
                positionWS += normalWS * _OutlineDistance * distToCam;
                OUT.positionHCS = TransformWorldToHClip(positionWS);

월드 공간에서 버텍스와 카메라 거리를 측정해서 거리만큼 곱해준 방식이다. 거리가 멀어지면 큰 값을 곱해서 두꺼워진다.

 

 

                // 스크린 노멀 방식, 카메라 거리에 따른 원근 교정 O
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                float3 clipNormal = TransformObjectToHClip(IN.normalOS * 100); // 100을 곱하는 이유 : 1 이하로 작은 값인 노말 방향은 클립스페이스의 퍼스펙티브가 적용되면서 화면 바깥쪽에서는 방향이 뒤집히는 왜곡이 발생하므로 클립 변환 전에 방향이 뒤집히지 않을 정도로 충분히 큰 벡터로 가공
                clipNormal = normalize(float3(clipNormal.xy, 0)); // 매우 큰 방향값을 정규화
                OUT.positionHCS.xyz += normalize(clipNormal) * _OutlineDistance * OUT.positionHCS.w; // 클립공간의 w 값은 카메라 공간의 z값과 같다. 즉, 카메라로부터 버택스까지의 거리

모든 좌표 계산을 클립 공간에서 계산한다. 클립 공간은 카메라와의 거리를 측정하지 않아도 일정한 두께로 두꺼워지는 장점이 있지만 원근감이 적용된 공간이라 노멀 벡터 역시 버텍스가 화면 중심에서 벗어날수록 왜곡된다. 따라서 노멀 벡터를 증폭한 후 다시 정규화하는 방법을 사용한다.

 

 


참고자료

 

아티스트를 위한 유니티 URP 셰이더 입문 - YES24

셰이더(Shader)를 HLSL(High Level Shader Language)로 다루고 싶은 아티스트를 위한 입문서다. 아티스트들의 눈높이에 맞추어 기초 그래픽스 이론과 셰이더 개념을 설명하고, 이를 유니티 엔진에서 활용할

www.yes24.com

728x90
반응형