그래픽을 표현하는데 있어서 그림자는 매우 중요합니다. 그림자가 존재함으로써 사물의 공간상 위치를 인지하기 쉽게 만들어주고 입체감을 더해주게됩니다. 그림자는 입체 공간을 구성하는데 있어 거의 필수적인 요소라해도 과언이 아닙니다.
이는 게임에 있어서도 마찬가지입니다. 그런 이유로 모든 3D 게임에는 어떠한 형태로든 그림자가 존재합니다. 그리고 당연히 유니티에서도 그림자 기능을 제공해줍니다. 사용법 역시 체크박스만 몇 번 해주면 되는 식으로 되어있어 손쉽게 그림자를 활성화시킬 수 있습니다. (공식 메뉴얼 : http://docs.unity3d.com/Manual/class-MeshRenderer.html)
유니티에서 제공해주는 그림자는 그림자맵(ShadowMap) 기법을 사용하고있습니다. 이 기법은 모든 굴곡에 대응하고 자기 그림자(self-shadow)가 처리되는 등 높은 퀄리티를 보여주고 있지만 문제는 성능입니다. ShadowMap 기법은 그림자의 깊이를 저장하는 버퍼를 만들고난 뒤 픽셀 셰이더(Pixel Shader)에서 깊이를 비교하는 과정을 거칩니다. 또한, 넓은 영역을 커버하기위해 여러 구역으로 나누기도하고(cascade) 계단 현상을 없애기 위해 여러번 샘플링하여 필터링을 처리하는 등 부가적인 행위들이 추가됩니다. 그러다보니 랜더링 비용 중 그림자가 많은 부분을 차지하게 됩니다. PC에서는 이러한 셰도우맵 기법을 사용하기에 충분한 성능이 나오지만 모바일 장치에서 셰도우맵을 사용하는 것은 무리입니다.
이 글에서는 유니티에서 기본적으로 제공해주는 쉐도우맵 방식을 사용하지 않고 좀 더 저렴한 방식으로 그림자를 표현하는 것에 대하여 이야기하고자 합니다.
원형(circle) 평면 그림자
성능 문제때문에 모바일 장치에서는 그림자를 동그라미로 간단하게 표현하는 것이 일반적입니다. 원형의 텍스쳐를 사용하는 평면을 만들고 그걸 케릭터 밑에 배치하는 것이지요. 형태는 너무나도 간단하긴하지만 이 마저도 없는것과 있는것의 차이는 큽니다.
이미지 : 레이븐
구현도 간단하거니와 비용도 많이 들지 않습니다만 평면이 아니고서는 표현이 불가능하다는 단점은 있습니다. 산이나 계단같이 평면이 아닌 구역에서는 그림자가 평면인게 티나기때문에 이질감이 있는 것이지요. 하지만 요즘 대부분의 모바일 게임은 평면상에서 이루어지고, 탑뷰등 카메라가 고정되어있으므로 널리 쓰이고 있습니다.
화면상에 작게만 보이거나 그림자 영역이 티가 많이 나지 않으면 이러한 간단한 동그라미 형태만으로도 충분합니다. 하지만 케릭터가 클로즈업되는 등 그림자의 화면 비중이 커지면 다소 어색해보일수도 있습니다. 이러한 경우는 케릭터 전체 크기의 동그라미가 아닌 발 크기의 동그라미 두개를 각각의 발 위치에 붙여서 이질감을 완화시킬 수도 있긴 합니다.
이미지 : 아직 미출시 데헷
그래도 아직 부족해보이긴 합니다. 엄밀히 말하자면 바닥과 발 사이의 AO를 표현해준 것이지 케릭터 전체의 그림자를 표현해준 것이 아니기 때문입니다. 완벽해보이려면 케릭터의 형태 그대로를 따라서 바닥에 그림자가 맺혀야합니다. 즉, 실시간 그림자가 필요합니다.
렌더 타겟 텍스쳐(Render Target Texture)
렌더 텍스쳐를 활용하면 손쉽게 이를 해결할 수 있습니다.렌더 텍스쳐는 카메라가 바라보는 장면을 화면에 바로 그려주는 것이 아니라 텍스쳐로 그려주는 기능을 제공해줍니다. ( 공식 메뉴얼 : http://docs.unity3d.com/Manual/class-RenderTexture.html) 그러기 위해서는 오브젝트에 정상적으로 그리는 메시 오브젝트 외에도 그림자로 그릴 용도의 메시 오브젝트를 추가해서 그림자로 그려줘야합니다.
우선 메인 카메라 외에 그림자용 카메라를 생성합니다. 그림자용 레이어를 추가해서 그림자용 메시만 그리도록 설정해놓고 Target Texture를 하나 생성하여 설정해줍니다. 그림자용으로 추가한 메시 역시 레이어를 설정해놓고 검은색으로 드리도록 매터리얼을 설정해줍니다. 그러면 Target Texture에는 다음과 같이 그림자 오브젝트가 그려집니다.
케릭터 주변의 적절한 위치에 평면을 설치한 후, 그림자용으로 만든 Target Texture를 평면에 셋팅해주면 그림자가 완성됩니다.
Target Texture의 해상도만 높으면 텍스쳐 필터링덕에 자연스레 부드러운 그림자(soft shadow)도 가능해진다는 이점이 있습니다. 하지만 현실적으로는 메모리 문제 때문에 해상도를 높게 잡을 수가 없습니다. 그러다보니 타겟 텍스쳐의 해상도문제로 그림자에 계단 현상이 발생하게 됩니다. 또한, 케릭터가 점프하는 등 에니메이션에 따라 일부 영역을 벗어나면 그림자가 잘려나가는 등 부작용이 많으므로 완벽한 해결책이 되지는 못합니다.
메시 평면 그림자
여기서 잠깐 그림자가 무엇인지에 대해 짚고 넘어가고자 합니다. 우리가 일반적으로 생각하는 그림자라는 것은 어떤 사물에 의해서 빛이 차폐되는 현상을 뜻합니다. 즉, 빛이 사물에 닿으면 그 사물 뒤의 영역에는 빛이 닿지 않는 것이지요.
이미지 출처 : http://news.mynavi.jp/column/graphics/020/?route=blog
그 말인 즉슨, 어떤 캐릭터의 그림자를 평면에 표현하기위해서는 케릭터의 형태를 빛의 방향으로 평면에 투영시켜서 그리면 된다는 것입니다. 이 경우 메시의 원형으로 그대로 그리는 것이 아니라 빝의 방향으로 평면에 투영시켜주는 버텍스 변형이 필요합니다. 따라서, 이 메시는 유니티에서 기본적으로 제공해주는 셰이더 말고 커스텀한 셰이더를 사용하여야 합니다. 이 그림자를 표현해주기 위한 셰이더를 만들기 위해서는 약간의 수학을 끼얹어야 합니만 복잡한 수학은 아니므로 큰 걱정은 안하셔도 됩니다.
일단, 다음과 같이 어느 한 점 P가 있고 광원 방향이 L이라고 하였을 때, 평면에 점P가 투영되는 위치는 점P'입니다. 점P와점P'을 이으는 선을 H라 하고 점P를 평면과 수직으로 이으는 선을 O라고 합니다. 선분 H와 O의 각을 θ라고 하였을 때 cosθ는 L과 단위화된(normalized)O의 내적입니다(L은 이미 단위화 되어있음). 이미 O는 평면과 수직인 상태이므로 최종적으로 cosθ와 L.y는 동일합니다. 그러면 아래 그림과 같이 빗변을 L, 맞변을 L.y로 삼는 빨간 삼각형을 이룰 수 있습니다.
그러면 삼각비에 의해서 L:Ly = H:O가 되고 L은 단위화되어 길이가 1이므로 H = O/L.y 입니다. 즉, P'= P + O/L.y입니다. 이를 셰이더 코드로 표현하면 다음과 같습니다.
float4 vPosWorld = mul( _Object2World, v.vertex);
float4 lightDirection = -normalize(_WorldSpaceLightPos0);
float opposite = vPosWorld.y - _PlaneHeight;
float cosTheta = -lightDirection.y; // = lightDirection dot (0,-1,0)
float hypotenuse = opposite / cosTheta;
float3 vPos = vPosWorld.xyz + ( lightDirection * hypotenuse );
o.pos = mul (UNITY_MATRIX_VP, float4(vPos.x, _PlaneHeight, vPos.z ,1));
이 셰이더를 적용하여 검은색으로 그리면 오브젝트의 실루엣을 따르는 그림자가 평면에 나타나게됩니다. 확실히 단순한 동그라미로만 그리는 것 보다는 자연스러운 그림자에 가깝게 보입니다.
케릭터 모델 : https://www.assetstore.unity3d.com/kr/#!/content/22840
보완
그러나 현 상태만으로는 한가지 문제가 있습니다. 그림자를 좀 더 자연스럽게 보이게 하기 위해 알파블렌딩(alpha blending)을 적용하면 다음과 같이 그림자 내부에 아티팩트가 생깁니다. 평면에 오브젝트를 블렌딩하여 그릴 때 같은 위치에 여러 면이 겹쳐져서 그려지기 때문입니다. 아래 그림에서는 몸통이나 머리 위에 팔이 덧그려지면서 실루엣 내 그림자 농도가 달라지는 현상이 생겨버립니다.
이러한 형상을 없애려면 이미 그림자가 그려진 픽셀에는 덧 그려지지 않게 하는 작업이 필요합니다. 이를 위해서는 스텐실(stencil) 버퍼를 활용하면 됩니다. 스텐실 버퍼는 일종의 마스킹(masking)의 개념입니다. 이미 그려진 픽셀은 또 그려지지 않도록 마스킹처리하는 것입니다. ( 공식 메뉴얼 : http://docs.unity3d.com/Manual/SL-Stencil.html) 그림자를 그리는 셰이더의 pass에 다음과 같이 스텐실을 사용하도록 선언해주면 중복된 픽셀에 그리지 않게 됩니다.
Stencil {
Ref 0
Comp Equal
Pass IncrWrap
ZFail Keep
}
스텐실을 적용하고나면 다음과 같이 블렌딩을 적용해도 아티팩트 없이 동일한 농도의 그림자를 그릴 수 있게됩니다.
주의사항
이토록 평면이라는 전제만 존재한다면 그림자를 비교적 좋은 성능을 보장하면서도 손쉽게 표현 할 수 있는 방법들이 많습니다. 다만, 메시 그림자같은 경우는 메시를 한번 더 그리게 되므로 폴리곤이 적은 그림자용 메시를 별도로 마련하는 것이 좋을 것입니다. LOD를 사용한다면 LOD용 모델을 사용하면 되므로 추가적인 작업의 부담은 생기지 않을 것입니다. 또 다른 주의점으로는, 자기 그림자(self shadow)가 처리되지 않는다는 단점은 존재합니다만 어짜피 라이팅이 간단한 모바일에서는 대부분 자기 그림자를 원치 않을 것입니다.
'프로그래밍' 카테고리의 다른 글
구조체 멤버 정렬 (Struct Member Alignment) (0) | 2015.08.04 |
---|---|
차세대 그래픽 API (0) | 2015.02.24 |
템플릿 특수화 ( Template Specialization ) (2) | 2015.02.05 |
C# Boxing과 Unboxing (1) | 2015.01.30 |
Unity Cloud Data 소개 (3) | 2015.01.15 |