jobGuid 꽃미남 프로그래머 "Pope Kim"님의 이론이나 수학에 치우치지 않고 실무에 곧바로 쓸 수 있는 실용적인 셰이더 프로그래밍 입문서 #겁나친절 jobGuid "1판의내용"에 "새로바뀐북미게임업계분위기"와 "비자관련정보", "1판을 기반으로 북미취업에 성공하신 분들의 생생한 경험담"을 담았습니다. 3ds Max를 사용해서 게임용 3D 캐릭터를 셋업하는 방법
이를 위해 오랜 실무를 경험해 온 저자의 고급 노하우들이 공개
위 내용은 GameDevForever의 저자분들의 홍보를 위하여 운영진 자체적으로 올린 광고이며 일체의 수익이 없습니다.(밥좀사줘요~)
Posted by ozlael


그래픽을 표현하는데 있어서 그림자는 매우 중요합니다. 그림자가 존재함으로써 사물의 공간상 위치를 인지하기 쉽게 만들어주고 입체감을 더해주게됩니다. 그림자는 입체 공간을 구성하는데 있어 거의 필수적인 요소라해도 과언이 아닙니다.

이는 게임에 있어서도 마찬가지입니다. 그런 이유로 모든 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)가 처리되지 않는다는 단점은 존재합니다만 어짜피 라이팅이 간단한 모바일에서는 대부분 자기 그림자를 원치 않을 것입니다. 



댓글을 달아 주세요

  1. Favicon of http://gamedevforever.com 대마왕J 2015.02.23 18:24 신고  댓글주소  수정/삭제  댓글쓰기

    으앙 이거 최고 ㅠㅠㅠㅠㅠㅠㅠ

  2. Favicon of http://gamedevforever.com Silverchime 2015.02.24 15:15 신고  댓글주소  수정/삭제  댓글쓰기

    감사해요 오즈님짱

  3. 오예 2015.03.18 17:39 신고  댓글주소  수정/삭제  댓글쓰기

    메시 평면 그림자 쉐이더 소스를 더 자세히 알수 있을까요?

  4. 질문 2015.11.13 09:35 신고  댓글주소  수정/삭제  댓글쓰기

    혹시 평면 그림자에도 소프트쉐도우 처럼 좀 흐릿하게 처리 할수 있는 방법 이 있나요?

Posted by 대마왕J

둘은 상당히 비슷한 기능을 하죠. mipmap 제어라는...

그치만 공식의 작동법은 미묘하게 달라요.

lod가 shader3.0을 요구한다는 것도 다르고요...

 

프로그래머들에게 물어보면 공식에 대한 차이만 말해주니까요

자세한 느낌을 보기 위해 직접 테스트를 해 봤습니다.

 

 

 

예제를 만들어 봤습니다. 둘이 똑같아 보이죠?

근데 제 눈에는 살짝 오른쪽 텍스쳐가 추가 샘플링 된 것처럼 보입니다.

 

그래서 정말 그런가 테스트

 

 

 

 

 

오호라 과연 정말입니다.

tex2Dlod는 고정된 mipmap을 유지하지만, bias 는 이름값대로 추가로 바이어스를 걸어주고 있는 느낌입니다.

 

그럼 이번에는 값에 따른 변화를 테스트 하기 위해 다시 근접시킵니다.

 

 

 

음 겜브리오때는 밉맵을 수동 설정 가능했는데. 유니티는 방법이 없을까낭...

뭐 투덜대도 할 수 없으니까 이번에는 값을 비교해 보죠

 

지금 두 그림이 똑같아 보입니다만... 오른쪽이 밉맵 1 상태라고 보여집니다. 한 단계 낮은 이미지요.

 

어쨌거나 일단 비교.

 

 

lod/bias 값 1

 

 

lod/bias 값 1.5

 

 

lod/bias 값 2

 

 

lod/bias 값 2.5

 

 

lod/bias 값 3

 

 

lod/bias 값 3.5

 

 

lod/bias 값 4

 

 

lod/bias 값 4.5

 

 

lod/bias 값 5

 

 

lod/bias 값 5.5

 

 

 

lod/bias 값 6

 

 

음... 일단 오른쪽 bias의 값이 더 낮아 보입니다.

그 이유는... 현재 보이는 거리 때문에 밉맵이 한 단계 더 먹은 상태라서 그렇습니다.

그에 반해 tex2Dlod는 거리고 뭐고 상관 안하고 mipmap 값이 강제지정됩니다. 호옹이.

이런 차이로군요.

 

 

그래서 정말 그런가 추가 테스트

 

 

 

 

다시 6단계에서부터 추가 테스트. 이번에는 필터링을 개뿌연 Box 필터링에서 그나마 날카로운  Kaiser 필터링으로 바꿔 보았습니다. 카메라를 좀 근접시키니 두 이미지가 상당히 비슷하네요 그래도 lod가 더 선명해 보이는게, 1단계 앞인 느낌입니다.

 

 

거리를 멀게 하자 오호라. 역시 bias는 거리에 따라 mipmap이 더 먹습니다.

 

그렇다는것은...

 

 

 

 

거리만 가깝다면 bias가 더 선명하다는 말도 된단 말입니다. lod는 고정이니까. 오호호라...

 

즉  lod는 무조건 고정시키는 거고 bias는 말그대로 기존 값에 bias 시킨다는 말이겠습니다.

 

이 기능은 주로 싸구려 블러 효과로 사용됩니다만, 값을 반대로 처리하면 싸구려 샤픈 효과도 가능하겠죠.

뭐 그 외에도 사실 많이 쓸 데가 있습니다. Matcap 과 같이 어떻게 저렇게 응용하면 모바일용 러프니스 흉내도 ... 케케케케케 꼼수질...

 

 

뭐 그래서 위 내용이 정말 사실인가 테스트.

이렇게 밉맵을 수동으로 제어할 수 없는 엔진에서는 텍스쳐를 작게 만들면 잘 보입니다. 데헷. 역시 꼼수.

 

 

 

네네네 증명되었습니다 역시.

 

32 *32 이미지를 가지고 밉맵을 가동시켜보니 역시 이렇게 나오는군요. 어쩔 수 없지요.

이런건 오히려 곡면일때가 더 문제죠 .

 

 

 

이렇게 되어 버리니까요.

 

Tex2Dbias를 쓰려면 텍스쳐가 작으면 일단 안 되겠군요. * 위는 Bilinear 상태입니다.

 

 

 

겸해서 추가로 알아낸 lod와 bias의 차이.

 

bias는 float으로 소숫점 단위까지 하나하나 연산해서 mipmap을 흐리는데,

lod는 그렇게 변하질 않네요.

 

lod로 하면 0.0~0.49까지는 같고

0.5~1.49 일때 변하고

1.5~2.49 일때 변하고

2.5~3.49 일때 변....

 

한다는 것입니다.

* 위의 경우는 텍스쳐가 Bilinear 로 되어 있을때였네요.

Trilinear일 경우에는 밉맵간 인터폴레이션이 되니까 저렇게 되진 않습니다.

그래도 오른쪽 아티펙트도 조금 줄긴 하지만 여전히 아티펙트는 남습니다.

 

그리고

 

lod는 0 이하의 값이 존재할 리가 없지만

bias는 - 값을 사용하면 알리아싱이 없는 가짜 샤픈 효과를 낼 수 있다는 것 정도.(물론 아주 가까우면 아무 의미가 없지만)

 재밌네요.

 

재미있는 연구였습니다. 이상.

 

 

 

* 텍스쳐를 인터페이스용으로 쓰던걸 밉맵만 생성해서 쓰다가 그만 필터링 옵션을 Trilinear로 바꾸는걸 잊고 있었군요.

그래서 Bilinear로 돌고 있었습니다. 이런 실수를!!

 

 

그래서 보너스로 Bilinear 와 Trilinear 차이를 보여드리겠습니다.

 

 

원본 이미지. 엄청 작습니다. 16*16 짜리이죠.

 

 

 

 

 

 

이게 Point Sampling. 밉맵 있음. 밉맵이 작동되는 라인이 선명하게 보입니다.

픽셀이 확대되거나 축소될때 옆 픽셀과 어떤 블렌딩도 없죠.

 

 

이게 Bilinear 샘플링 밉맵있음. 밉맵라인이 훤히 보입니다.

확대되거나 축소되는 픽셀은 옆 픽셀들과 블렌딩됩니다.

 

 

 

이게 Trilinear 샘플링 밉맵있음. 밉맵끼리 블렌딩이 되기 때문에 거의 안 보입니다.

확대되거나 축소되는 픽셀은 역시 옆 픽셀들과 블렌딩됩니다.

 

 

마지막으로 Anisotropic 샘플링 * 16

기울어진 각도를 인식해서 비례로 샘플링하기 때문에 기울어졌을때 가장 선명하게 샘플링되지만 가장 느립니다.

 

언제 한 번 샘플링도 알기 쉽게 정리를 해야..

 

댓글을 달아 주세요

  1. 김원기 2016.05.03 06:11 신고  댓글주소  수정/삭제  댓글쓰기

    정말 최고입니다. 감사합니다.

Posted by 대마왕J

카메라에는 보이는 영역, 원뿔 .. 은 아니고 사각뿔 영역이 있습니다.

이 영역을 View frustom 뷰 프러스텀 이라고 하고, 이 영역 안에 있으면 화면에 보이는 걸로 치고 렌더링합니다.

 

http://en.wikipedia.org/wiki/Viewing_frustum

 

 

네 뭐 좋습니다. 안보이면 안그리고 딱 좋지요.

보이면 그리는 겁니다.

 

하지만

 

 

이따만한 오브젝트가 있을 때, (2만 폴리곤이군요)

 

 

 

그 와중에 화면에 요따시만큼만 보일 때, 어떻게 될까요?

 

우선, 이 오브젝트는 보이는 건가요? 안보이는 건가요?

 

... 네 보이는 겁니다. 큰 오브젝트의 폴리곤 하나라도 보이면 이 오브젝트는 '보이는 겁니다'

 

CPU는 매 프레임마다 이 화면에 어떤 오브젝트가 있는지를 검사하고 있습니다. '일' 을 하고 있는거지요. 일을 하면 당연히 자원이 듭니다. 그런 입장에서 보았을때, 이 2만 폴리의 플렌은 반드시 '보이는' 겁니다. 네 뭐 그렇지요.

어디 진짜인가 실험해 보죠. 플레이를 해 봅니다.

 

 

 

네 오브젝트는 하나이므로 드로우콜은 1입니다. 뭐 오케이

그리고 폴리곤은... 여전히 2만입니다!!! 그렇습니다. 끄트머리 하나가 보였는데도, 이 2만 폴리 플렌은 다 보인다고 계산되고 있습니다. 픽셀 드로잉 연산은 좀 벗어날 수 있어도 버텍스 연산은 얄짤없이 전부 계산하고 있습니다.

 

이것을 제가 강의할때 주로 하는 비유로 말하면, '만리장성의 벽돌 하나만 보여도 만리장성은 다 보이는 거랑 같다' 라는 겁니다.

그렇다면 방법은 없을까요? 네 방법이 없지는 않습니다.

 

 

 

오브젝트를 이렇게 디테치 시키는 겁니다. 맥스에서.

 

그렇게 되면 총 9개의 오브젝트가 되는 거지요!!! 그렇게 하고 다시 끄트머리만 보면...?

 

 

 

짜잔

 

 

폴리곤이 1/10으로 줄었습니다. 만세!!

대충 맞죠 . 9개로 나눴으니까 말이죠. 이렇게 보이는 녀석만 보이게 되는 겁니다.

 

 

만세

 

 

정말 좋아진 것 같습니다. 와아 그럼 여러개로 막 나누면 좋을까요? 더 자질구레하게 나누면 좋을 거잖아요

 

물론 그렇지는 않습니다. 이렇게 나누면 처리해야 하는 폴리곤은 줄어드는 대신...

 

 

 

전부 다 한번에 보자, 드로우 콜이 9가 되었습니다!!!  

폴리곤 양보다 더 무서운, 드로우 콜이 늘어나거든요. 이렇게 전체를 볼 때는 말입니다.

즉 이렇게 한 화면에 들어오는걸 여러 개로 자르는건 오히려 더 퍼포먼스의 낭비가 일어나게 됩니다.

 

 

 

 

이럴때는 차라리 안 나누는게 더 좋아요. 한 화면에 전부 보인다면 말입니다. 이럴때는 앞에 썼던 방법이 오히려 좀 더 효율적입니다.

 

 

 

결론내리다면, 기본적으로 '드로우 콜을 줄여야 하는데, 그렇다고 폴리곤이 너무 많아지지 않게끔 너무 안보이는건 잘라라' 가 기본 조건이란 말이지요. 적절한 드로우 콜과 적절한 폴리곤 양의 배분을 위해, 배경에서 이걸 자르는 크기를 잡는건 매우 경험이 필요한 기술입니다.

그러므로

 

'만리장성은 적절한 크기로 잘라야 합니다'

 

 

 

이것을 '프러스텀 컬링' 이라고 하고, 가장 가벼운 지형 최적화 방법중 하나입니다. 유니티 터레인이나 언리얼 랜드스케이프에서도 적용되어 있는 방법이지요 (보통 동적 LOD와 함께 동작할 수 있도록 적절한 사이즈의 청크로 잘라져 있습니다. 안보이지만 안보이지만)

그리고 뭐 모든 엔진의 기본이기도 합니다.

http://hoidu.tistory.com/78

 

 

 

물론 여기에 추가로 버텍스가 안보이면 버텍스를 하나하나 정말 잘라버리는 '클리핑' 이란 기술도 있습니다.

그렇지만 그 기술은... 상식적으로도 컬링보다 더 무겁습니다! ㄷㄷㄷ

그도 그럴 것이, 보이는 버텍스는 남겨두고 안보이는 버텍스를 없애버리게 되면 일단 매 프레임 전체 '버텍스' 의 충돌을 검사해야 하는 데다가 (덜덜덜)

보이는 버텍스를 두고 안보이는 버텍스를 지우면 이번엔 그 중간의 폴리곤이 날라가게 되어서 뚫려 보이게 됩니다 (덜덜덜2)

그러다보니까 클리핑 기술에는 반드시 버텍스를 추가 생성해서 면을 만들어 버리는 기술까지 같이 들어가 있습니다.... (덜덜덜3)

 

 

http://gamercampuk.tumblr.com/post/22716607528/a-brief-history-of-graphics-and-rendering

 

 

이걸 실시간으로 처리한다면 그야말로 엄청난 부하가 유발되는 작업이 되기 때문에 PC 에서도 잘 안쓰는 기법이지요.

(마영전의 옷 처리도 클리핑 플렌을 미리 설치해 놔서 실시간 계산양을 줄였을 정도...)

 

 

 

 

결론적으로 최적화의 기본은 "안보이게 하는 것"이지만, 그 안보이게 하는 기술 자체가 무거운 경우가 많아서 참으로 요령있게 사용해야 하는 영역이라는 겁니다. 그럼 총총

댓글을 달아 주세요

  1. Favicon of http://jeahnote.tistory.com DevAthena 2016.12.05 12:16 신고  댓글주소  수정/삭제  댓글쓰기

    좋은글 감사합니다.

Posted by 친절한티스

타입은 다른데 같은 작업을 하는 클래스나 함수를 구현을 할때 템플릿을 사용합니다. 아래와 같이 말이죠.


template<typename T>
class CFoo
{
public:
	T Add(T lhs, T rhs)
	{
		return lhs + rhs;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	CFoo<int> integer;
	std::cout << integer.Add(3, 5) << std::endl;

	CFoo<float> floating;
	std::cout << floating.Add(2.5f, 8.2f) << std::endl;

	return 0;
}



여기까지는 계획대로 잘 되는거 같습니다. 그렇다면 Add 함수에 아래와 같이 문자열을 넣어서 두 문자열이 합쳐지게 만드려면 어떨까요?


CFoo<char*> szStr;
std::cout << szStr.Add("Hello, ", "World!!") << std::endl;


결과는 error C2110: '+' : 두 포인터를 더할 수 없습니다. 라는 메시지와 함께 에러가 나버립니다. 두 문자열이 합쳐지게 하려면 char* 타입용 CFooCharPointer 클래스를 따로 구현해야 할까요? 아니면 클래스 전체를 구현하기는 귀찮으니 char* 인자를 받는 AddCharPointer 함수만 추가로 구현해줄까요? 


비슷한 일을 하는 클래스를 또 만드는 것도, 다른 타입의 템플릿 클래스에서는 굳이 사용안할 멤버 함수를 추가하는 것도, 둘다 그리 좋은 모습은 아닙니다. 이런 상황의 해결책으로 템플릿 특수화 기능을 사용하면 됩니다. 특정 타입에서만 다르게 동작하는 함수를 만들수 있는 것이죠. 위의 상황에서 int나 float 같은 일반적인 타입에서는 기존에 정의한 Add 함수대로 동작하고, char* 같은 예외 타입이 들어오면 특수화 시킨 함수를 호출 시키게 하면 되는 것입니다.


template<typename T>
class CFoo
{
public:
	T Add(T lhs, T rhs)
	{
		return lhs + rhs;
	}
};

// char* 타입용 특수화 함수
template<> char* CFoo<char*>::Add(char* lhs, char* rhs)
{
	char *sz = new char[128];
	memset(sz, 0, sizeof(sz));
	strcat(sz, lhs);
	strcat(sz, rhs);
	return sz;
}

int _tmain(int argc, _TCHAR* argv[])
{
	CFoo<int> integer;
	std::cout << integer.Add(3, 5) << std::endl;

	CFoo<float> floating;
	std::cout << floating.Add(2.5f, 8.2f) << std::endl;

	CFoo<char*> szStr;
	char *resultString = szStr.Add("Hello, ", "World!!");
	std::cout << resultString << std::endl;
	delete[] resultString;

	return 0;
}

CFoo에 위와 같이 char* 타입에 대해 함수를 재정의 해줍니다. 이렇게 할 경우 char* 타입으로 템플릿을 생성했을 경우 Add 함수를 호출 하면, 특수화된 Add 함수가 호출되고, 결과는 아래와 같이 원하는대로 출력되는 것을 확인할 수 있습니다.



참고 : https://msdn.microsoft.com/ko-kr/library/c401y1kb.aspx

PS : CFoo<std::string> 을 사용하면 char* 특수화를 안해도되지만, 예를 위해 위와 같이 샘플 코드를 작성해보았습니다.

댓글을 달아 주세요

  1. Favicon of http://gamedevforever.com 끼로 2015.02.05 17:45 신고  댓글주소  수정/삭제  댓글쓰기

    티스형 최고.. 이제 여긴 티스형 개인블로그가 되버린듯 ㅠㅠ

Posted by 친절한티스

요즘 유니티를 이용해 C#으로 코딩을 하시는 분들이 많은데, 간혹 면접을 보다보면 Boxing과 Unboxing에 대해 제대로 이해하지 못하는 분들이 많더군요. 이를 C/C++의 캐스팅 같은 것으로 생각하고 마구잡이로 사용하시는 분들이 많은듯 합니다. 하지만 C/C++의 캐스팅과 다르게 C#의 Boxing은 그 과정에서 개체 할당이 일어 나기 때문에 성능 이슈가 생길수 있습니다.


int num1 = 123;
object obj = num1;      // boxing
int num2 = (int)obj;    // unboxing


C#에서 사용되는 모든 타입들은 object 에서 직/간접적으로 상속을 받게 됩니다. 즉, 최상위 부모 클래스 같은 존재지요. 이것이 C/C++에서 였다면, 위의 num1에서 obj로의 대입은 단순 업캐스팅으로 변환되는 거기 때문에 큰 문제는 없을 겁니다. 하지만 C#에서는 다릅니다. C#에서는 이를 Boxing이라 부르며 아래와 같이 힙 Heap에 새로운 개체를 할당하고 그 곳에 값을 복사하게 됩니다. 



C++ 코드로 보자면 아래와 같은 상황인겁니다.


int num1 = 123;
int *obj = new int;
(*obj) = num1;

이 때문에 MSDN에서도 Boxing, Unboxing에 대해 아래와 같이 조언하고 있습니다.

  • System.Collections.ArrayList 같은 제네릭이 아닌 컬렉션 클래스의 예와 같이 많은 수의 boxing이 필요한 경우에는 값 형식을 사용하지 않는 것이 좋습니다. System.Collections.Generic.List 같은 제네릭 컬렉션을 사용하면 값 형식의 boxing을 방지할 수 있습니다. boxing 및 unboxing 과정에는 많은 처리 작업이 필요합니다. 값 형식을 boxing할 때는 완전히 새로운 개체가 만들어져야 하며, 이러한 작업은 간단한 참조 할당보다 최대 20배의 시간이 걸립니다. unboxing을 할 때는 캐스팅 과정에 할당 작업보다 4배의 시간이 걸릴 수 있습니다.


반대로 Unboxing의 경우 Boxing과는 다르게 대입하려는 타입을 명시적으로 지정해주어야 합니다. Bxoing 되어있는 타입과 명시한 타입이 같으면, 해당 객체의 값을 복사합니다.



C#은 C/C++에 비해 타입에 엄격하기 때문에 Boxing 타입과 다른 타입으로 Unboxing을 시도하면 InvalidCastException 같은 에러를 냅니다.


참조 : MSDN Boxing 및 Unboxing

댓글을 달아 주세요

  1. Favicon of http://ldh9451.tistory.com 따분한놈 2015.01.30 11:03 신고  댓글주소  수정/삭제  댓글쓰기

    감사합니다 친절하시네요 ㅎㅎ

Posted by ozlael

만일 여러분이 게임을 출시했는데 난이도가 넘 어렵다는 평이 많다고 가정해봅시다. 그래서 주인공의 공격력과 방어력을 조절해야 합니다. 근데 로직은 변한 것 하나 없고 단순히 수치만 변경했는데도 빌드를 새로하고 다시 업로드를 해야하네요. 안드로이드는 업로드하면 그나마 몇시간 후면 반영되는데 아이폰은 일주일을 꼬박 기다려야하네요. 뭐 별수 있나요 눈물을 머금고 기다려서 난이도 패치를 끝냈습니다. 그 기념으로 일시적으로 아이템 드랍률을 올려주는 이벤트를 하려고 합니다. 이번에도 로직은 바뀌는 것 없이 단순 데이터 수치만 바뀌는건데도 빌드를 다시 해야하네요. 안드로이드와 아이폰간의 빌드 반영 딜레이 뿐만 아니라 업데이트 받은 유저와 아닌 유저의 차이도 골치아프겠네요.

물론 클라이언트-서버 구조로 되어 있는 게임은 이런 고민이 필요 없습니다. 애초에 중요 로직과 데이터들이 다 서버에 있으니까요. 하지만 보통 로직 서버를 따로 두지 않는 캐쥬얼 인디 게임들이라면 이런 고민이 절실합니다. 


Unity Cloud Data(https://data.cloud.unity3d.com)

이번 글에서는 그러한 고민을 해결해 줄 수 있는 Unity Cloud Data 서비스를 소개하려고 합니다. Unity Cloud Data (이하 UCD)는 서버-클라이언트 구조가 아닌 게임도 중요 데이터들을 서버에 둘 수 있도록 해주는 서비스입니다. 데이터만 서버에서 제어하면 클라이언트에서는 그 값을 가져다 사용하기때문에 따로 추가적인 패치가 필요가 없습니다. 그렇게되면 앞서 언급한 공격력 방어력 아이템 드랍률 등의 단순 수치들을 수정해주는 문제가 아주 간결해지는 것이지요. 데이터 연동 서버도 유니티에서 제공해주고 있으므로 따로 따로 서버를 마련할 필요도 없습니다. 


실습 ㄱㄱ !

그럼 간단히 같이 실습해보면서 확인해볼까요? 유니티 튜토리얼중에 Space Shooter라는 샘플 게임이 있습니다. 우선 이 샘플을 다운로드받아주세요. (https://www.assetstore.unity3d.com/kr/#!/content/13866)

튜토리얼 영상도 있어요 : http://unity3d.com/learn/tutorials/projects/space-shooter

게임 자체는 매우 간단해요. 운석과 적 비행기가 계속 나오고 플레이어 비행기를 움직이면서 총알을 발사하는 간단한 룰입니다. WASD로 이동하고 마우스 버튼으로 발사합니다. Space Shooter 에셋을 import한 후 Assets/Done/Done_Scenes에 있는 Done_Main Scene을 열어주세요. Game Controller 오브젝트에 있는 Done_Game Controller script에서 적들이 등장하는 갯수와 주기 등등을 제어하고 있습니다. 이 값들을 서버에 연동하여 변경하는 작업을 확인해볼까 합니다.


UCD 페이지 설정

우선은 UCD 게임 프로젝트 별로 서버를 설정해줘야 합니다. UCD(https://data.cloud.unity3d.com)에 가서 로그인 후 다음과 같은 절차를 따라주세요.

  1. 우선, Create New Project 버튼을 클릭해서 새 프로젝트를 생성합니다. 이 프로젝트는 게임 별로 따로 존재하여야합니다. 일단은 Test01이라고 이름 지었습니다. 
  2. 프로젝트를 생성하고나면 데이터 시트를 생성해줘야 합니다. 방금 만든 Test01 프로젝트로 들어가서 페이지 상단에 있는 Create New Mesater Sheet라고 적혀있는 녹색 버튼을 눌러서 시트를 생성해줍니다. 저는 gamectrl이라고 이름을 지었습니다.
  3. 그 후, Access Token을 발급해줘야합니다. Test01 프로젝트로 페이지 하단에 있는 Create New Editor Token이라고 적혀있는 녹색 버튼을  클릭해서 새 토큰을 생성해줍니다.
이제 Organization ID, Project ID, Editor Token, Sheet 등 서버 페이지에서 필요한 셋팅은 모두 완료하였습니다. 

이제 Unity Cloud Data Plugin 다운로드받읍시다. 다운로드 페이지(https://data.cloud.unity3d.com/#/download)로가서 다운로드를 받고 패키지를 import합니다. UCD를 import하고나면 Assets/UnityCloudData가 생긴것을 확인 가능합니다.


유니티 에디터 작업

이제 Hierarchy에서 Create > Create Empty를 수행해서 빈 오브젝트를 만듭니다. 저는 이 오브젝트 이름을 UCD로 지었습니다. 이 오브젝트에 Add Component를 수행해서 Unity Cloud Data > Cloud Data Manager 컴포넌트를 추가해줍니다. 앞서 생성했던 Organization ID, Project ID, Access Token등을 기입해주시면 됩니다. Access Token 바로 아래 있는 Create New Sheet 버튼을 수행하여 Cloud Data Sheet 컴포넌트를 생성하고 Path에는 앞서 생성한 시트의 이름을 적어줍니다. 그 후 Refresh from Unity Cloud Data를 수행하여 이상이 없는 지 확인합니다.

이제 본격적으로 수치를 연동해봅니다. Hierarchy에서 Game Controller 오브젝트를 선택합니다. Done_GameController의 Hazard Count, Spawn Wait, Wave Wait를 연동하고자 합니다. 스크립트를 열고 아래 표시한 부분을 수정 및 추가해줍니다. 코드 변경은 이게 끝입니다. MonoBehaviour대신 UCD 기능이 있는 CloudDataMonoBehaviour를 상속받도록 수정하고, 서버와 연동하고싶은 변수에 CloudDataField 태그를 붙여줍니다. sheetPath는 앞서 생성한 gamectrl로 설정해줍니다만 생략할 수도 있습니다.

그 후 Game Controller의 Inspector를 확인해보면 변수들이 Cloud Data Fields로 바뀌어 있습니다. 그 아래 있는 Save to Cloud Data Sheet를 클릭하면 Saving new fields 라는 메시지로 바뀌며 UCD 서버로 데이터가 연동됩니다. 

다시 UCD의 test01>gamectrl 페이지를 새로고침하면 데이터가 연동되어있는 것이 보입니다.

이제 모든 것이 완료되었습니다. 유니티로 돌아가서 게임을 수행시킵니다. 그 후 gamectrl페이지의 수치를 변경(hazardcount:50, spawnwait:0.01, wavewait:0.5) 후 Save Changes를 눌러 값을 저장해줍니다. 다시 유니티로 돌아가서 게임오버 후 R키를 눌러서 Scene이 re-load되면 적들의 빈도 및 밀도가 변경되는 것을 확인 가능합니다. 



마치며

앞서 언급했듯이, MMORPG 등, 중요 로직은 이미 서버에서 처리되고 있는  서버-클라이언트 구조의 게임에서는 UCD가 필요는 없습니다. 하지만 따로 서버가 없는 게임에서는 매우 유용하게 쓰일 것 같습니다.다만, 아직은 Alpha 서비스 단계라서 라이브 서비스 중인 게임에서는 사용하기에는 조심스럽습니다. 하지만, 이제 개발 착수단계인 프로젝트에서는 사용 여부를 고려해보심이 어떨까 합니다.

감사합니다.


* 아직 테스트 단계라 원활한 접속이 되지 않을 수 있습니다.

댓글을 달아 주세요

  1. Favicon of http://blog.naver.com/accadia777 힙돌이동남아 2015.11.10 10:57 신고  댓글주소  수정/삭제  댓글쓰기

    좀 오래된 게시물이라, 알파버전의 테스트 단계라서 엑세스 신청 메일을 보내긴 했는데, 금방 답신이 오나요?

  2. peridot 2016.01.09 15:10 신고  댓글주소  수정/삭제  댓글쓰기

    언제쯤 정식 서비스가 될까요? 혹시 서비스가 폐지될 가능성도 있나요?



티스토리 툴바