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 데이터를 수정한다.
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)
어디를 수정하면 되는지 찾기 위해서는 매크로를 코드로 풀어줄 필요가 있다.
매크로를 풀어주면 위의 예제 코드를 아래처럼 다시 쓸 수 있다.
// 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 이상 느리게 나오므로 상대적인 차이만 비교해 본다.
- 기본 diffuse lighting (shadow caster pass 추가 X)
- Shadow caster pass 예제 코드 추가
- 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 소요된다.
패스를 하나 추가해서, Shadow caster 기본 예제 코드를 사용하면 성능은 다음과 같다.
hitch에서 RenderForwardOpaque.Render 는 2.98 ms 소요되고, 그 중 Shadows.RenderShadowMap 은 2.34 ms 소요된다
세번째로 Custom shadow caster pass 를 사용할 경우의 성능이다.
hitch에서 RenderForwardOpaque.Render 는 3.4 ms 소요되고, 그 중 Shadows.RenderShadowMap 가 2.8 ms 소요된다.
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 개수가 늘어날수록 성능 차이가 더 심하게 나타날 수 있다.