본문 바로가기

Unity Engine

[Unity3D][Shaderlab] vertex shader 로 vertex 수정 후 shadow casting 적용하기

Custom vertex shader 내부에서 수정한 vertex 값들은 해당 pass가 끝나면 사라진다. Pass 의 렌더링을 시작할 때 vertex shader 로 전달해주는 원본 데이터 자체를 바꾸는 게 아니기 때문이다.

 

Unity 의 standard forward shading 에서, shadow cast 는 별도의 pass 에서 계산된다.

따라서 Custom vertex shader 에서 수정된 값은 shadow caster pass 로 곧바로 전달되지 않는다.

 

Custom vertex shader 에서 수정한 값에 맞게 shadow 를 그려 보자.

성능을 고려하지 않아도 된다면 가장 쉬운 방법은 shadow caster pass 에서 한번 더 vertex 를 수정하는 것이다.

Shadow casting 계산을 하기 전에, shadow caster pass 의 vert 함수 내부에서 vertex 데이터를 수정한다.

좌: custom vertex shader 의 수정값이 적용되지 않은 shadow / 우: vertex shader 와 동일하게 vertex 를 수정해서 계산한 shadow 

 

Implementation: Custom Shadow Caster

Unity3D shader tutorial 에 나와있는 shadow caster 예제 코드를 조금 변형할 것이다.

        // shadow caster rendering pass, implemented manually
        // using macros from UnityCG.cginc
        Pass
        {
            Tags {"LightMode"="ShadowCaster"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "UnityCG.cginc"

            struct v2f { 
                V2F_SHADOW_CASTER;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }

 

V2F_SHADOW_CASTER 등의 shadow caster 관련 매크로들은 UnityCG.cginc 파일에 정의되어 있다.

UnityCG.cginc 파일은 유니티엔진 설치 경로에서 확인할 수 있다.

(https://docs.unity3d.com/kr/current/Manual/SL-BuiltinIncludes.html)

UnityCG.cginc 내부의 V2F_SHADOW_CASTER 매크로 정의 부분

 

어디를 수정하면 되는지 찾기 위해서는 매크로를 코드로 풀어줄 필요가 있다.

매크로를 풀어주면 위의 예제 코드를 아래처럼 다시 쓸 수 있다.

        // shadow caster rendering pass, implemented manually
        // using macros from UnityCG.cginc
        Pass
        {
            Tags {"LightMode"="ShadowCaster"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "UnityCG.cginc"

            struct v2f { 
                // V2F_SHADOW_CASTER;
                float4 pos : SV_POSITION;
            };
			
                        
            v2f vert(appdata_base v)
            {
                v2f o;
                // TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
                o.pos = UnityApplyLinearShadowBias(o.pos);
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                // SHADOW_CASTER_FRAGMENT(i)
                return 0;
            }
            ENDCG
        }

 

Shadow 계산을 할 때 수정된 v.vertex 값을 사용하고 싶으므로, o.pos 계산 전에 v.vertex 를 수정하는 코드를 추가한다.

그 외 수정이 필요없는 매크로들은 다시 원래대로 정리해주면 아래 코드처럼 마무리 된다.

셰이더컴파일 후 테스트하면 잘 작동한다.

        // shadow caster rendering pass, implemented manually
        // using macros from UnityCG.cginc
        Pass
        {
            Tags {"LightMode"="ShadowCaster"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "UnityCG.cginc"

            struct v2f { 
                V2F_SHADOW_CASTER;
            };
			
            // Custom vertex translation function
            float4 ApplyWindMove(float4 v)
            {
                float HeightWeight = clamp(v.z, 0, 1);
                float Weight = 0.5;
                v.x += _SinTime * saturate(v.z) * Weight * HeightWeight;
                v.y += _SinTime * saturate(v.z) * -Weight * HeightWeight;

                return v;
            }
            
            v2f vert(appdata_base v)
            {
                v2f o;
                // Call custom vertex function before starting shadow calculation
                v.vertex = ApplyWindMove(v.vertex);
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }

 

Limitation: Performance

위 방식대로 구현하면 1개의 Shadow caster pass 가 추가되고, vertex 를 수정하는 함수를 두 번 호출하게 된다. 

(Object draw & Lighting pass 에서 한 번 + shadow caster 에서 한 번 = 총 2번)

 

성능 차이가 얼마나 나는지 3가지로 나누어서 확인해 보자. 

에디터에서 GPU Profiler 를 사용하면 0.3ms 이상 느리게 나오므로 상대적인 차이만 비교해 본다.

  1. 기본 diffuse lighting (shadow caster pass 추가 X)
  2. Shadow caster pass 예제 코드 추가
  3. Custom shadow caster pass 추가 (예제 코드 + custom vertex function)

 

성능 측정한 scene 의 stat 은 다음과 같다. 3가지 경우 모두 동일한 SetPass calls, Shadow casters 값을 가진다.

씬에 존재하는 거의 모든 오브젝트가 위의 shader 를 사용하였다.

 

hitch 가 생겼을 때 기본 Diffuse lighting 의 성능은 다음과 같다.

RenderForwardOpaque.Render 는 1.42 ms 소요되고, 그 중 Shadows.RenderShadowMap 은 0.78 ms 소요된다.

1. 기본 forward base lighting 성능

 

패스를 하나 추가해서, Shadow caster 기본 예제 코드를 사용하면 성능은 다음과 같다.

hitch에서 RenderForwardOpaque.Render 는 2.98 ms 소요되고, 그 중 Shadows.RenderShadowMap 은 2.34 ms 소요된다

2. 예제 shadow caster pass 를 추가했을 경우

 

세번째로 Custom shadow caster pass 를 사용할 경우의 성능이다.

hitch에서 RenderForwardOpaque.Render 는 3.4 ms 소요되고, 그 중 Shadows.RenderShadowMap 가 2.8 ms 소요된다.

3. Custom shadow caster pass 를 사용할 경우

shadow caster pass 가 하나 추가되었을 때, Shadows.RenderShadowMap 소요 시간이 3배 이상 증가하였다. (0.78 -> 2.80 ms)

Custom shadow caster pass 를 사용할 경우, vertex shader 에서 하나의 함수를 추가로 호출하면 Shadows.RenderShadowMap 의 소요 시간이 약 0.5 ms 증가했다. 실제로는 0.2 ~ 0.3 ms 정도 증가할 것이다.

 

처리해야 하는 vertex 개수가 많아지고, shadow caster 의 draw call 개수가 늘어날수록 성능 차이가 더 심하게 나타날 수 있다.