[게임 수학] 3차원 공간 맛보기

ounols·2021년 11월 11일
19

게임 수학

목록 보기
1/9
post-thumbnail
post-custom-banner

🧐 해당 파트는 게임 개발 환경을 구성하는 컴퓨터 그래픽스(Computer Graphics)를 이해하기 위한 기초 수학의 간단한 개념에 대해 설명하고 있습니다!

혹여나 이해가 잘 안되거나 잘못된 정보를 발견하시게 되었다면 관련해서 피드백 해주시면 정말 감사하겠습니다!

2차원이 아닌 바로 3차원의 공간에 대해 설명을 시작하기 때문에 정말 기본적인 게임 수학을 잘 모르신다면 이해하기 힘들 수도 있습니다. 그래도 최대한 알아보기 쉽도록 작성하고자 노력하겠습니다!

해당 시리즈의 글은 이득우 교수님의 '상상하는게임수학'을 토대로 진행됩니다. 이해가 부족한 부분은 해당 글을 통해 다시 읽어보는 것을 추천드립니다!

1. 3차원 공간의 설계

3차원... 2차원에서 축 하나가 생겼을 뿐인데 생각보다 더욱 복잡하고 수학적인 난이도에서도 좀 크게 상승하는 것 같습니다.

하지만 그만큼 매력이 있는 공간이기에 흥미만 있다면 정말 즐겁게 볼 수 있을겁니다!

1-1. 다른 3D 소프트웨어들은 어떠한 3D공간을 구성하고 있을까요?

저희가 알아볼 3D 소프트웨어들은 유니티 엔진과 블랜더3D, 그리고 초라하고 별거 없지만 제가 만든 CSEngine이라는 게임 엔진도 살짝 포함해서 알아보도록 하겠습니다..ㅎㅎ..

어쨌든 다음과 같은 프로그램에서 채택한 좌표계는 다음과 같습니다.

  • 유니티 : 왼손 좌표계 y-up
  • 블랜더3D : 오른손 좌표계 z-up
  • 자체제작 엔진 : 오른손 좌표계 y-up

1-2. 위 프로그램들은 왜 저렇게 설계가 되었을까요?

소개한 프로그램 별로 정리를 해보았습니다.

유니티는 왜 왼손 좌표계인가요?

먼저 왼손좌표계를 채택한 이유는 UX면으로 큰 이점이 있을 것으로 예상되는데 이에 대해 제가 경험했던 내용을 덧붙여 이야기를 하자면

3D 상에서 2D를 표현할 때 가끔 z축을 이용한 레이어 구분이 필요할 때가 있었습니다.
이 때 왼손 좌표계를 사용하면 화면과 멀어질 수록 z축 값이 올라가는데, 이렇게 z축값이 올라가는 것과 각 오브젝트의 레이어 순서는 서로 비슷하기 때문에 유용하게 썼던 기억이 있습니다.

🤔 그럼 왜 Y-UP 인가요?
제가 직접 유추해본 내용은 그래픽 라이브러리들과 연관이 있지 않을까 생각하고 있습니다.
유니티와 언리얼이 본격적으로 상용화 되기 전 게임업계는 자체엔진이나 따로 계약을 진행한 엔진으로 게임을 만들어 왔던걸로 알고있습니다.

이 때는 그래픽 라이브러리의 접근이 지금보다 많았을거라고 생각이 드는데
당시 유명했던 그래픽 라이브러리인 다이렉트X와 OpenGL은 y-up 좌표계를 채택하고 있습니다.

이에 따라 개발자들에게 좀 더 친숙한 y-up이 유니티와 같은 상용엔진에도 적용되지 않았을까 생각하고 있습니다.

블랜더는 왜 오른손 좌표계 Z-UP인가요?

오른손 좌표계를 채택한 이유는 인문 수학들이 대부분 2D 좌표계인 데카르트 좌표계에서 3D 좌표계로 넘어갈 때 좀 더 호환이 되는 방식이 오른손 좌표계이기 때문에 대부분 오른손 좌표계를 쓴다고 치더라도 왜 굳이 z-up일까? 이건 진짜 궁금했었습니다.

그리고 구글링을 해보니 이거와 관련한 토론이 이미 해외포럼에 진행되었었고, 위 사진이 그 포럼의 내용입니다. 해당 포럼은 한달동안만 활성화 된 토론인데도 불구하고 29개의 의견이 달려있는 상태입니다. 해당 포럼의 링크를 통해 한번 확인해보는 것을 추천드립니다.

결론부터 말하자면... 명쾌한 결론은 없었습니다!
나름 공학적인 접근방식에서 Z-UP이 거의 표준처럼 사용된다고 작성된 글도 있긴 한데 잘 모르겠네요...
그래도 토론의 전제는 각자의 관점을 존중하며 왜 이런 좌표계를 사용했는지에 대한 이야기가 오고 가는 것이라 보면 좋을 것 같습니다.

왜 자체 제작한 게임엔진은 오른손 좌표계 Y-UP 인가요?

사실...이건 OpenGL이 해당 좌표계를 채택해서 그대로 쓰고 있었습니다!
특별한 이유는 없습니다..하핫...ㅎㅎ

2. 3차원 공간의 트랜스폼

2-1. 이동변환과 크기변환 행렬

이동변환은 Identity Matrix를 기반으로 ww축만 변경됩니다. 게다가 한 축이 변경되면 다른 축은 변경되지 않습니다. 따라서 크기변환은 Identity Matrix 범위 내에서 변경됩니다.

이에 따라 이 두가지 변환은 차원마다 행렬식이 추가되기만 하면 끝나기 때문에 알아보기 편합니다!
3차원 기준의 해당 변환식은 다음과 같습니다.

이동변환크기변환
T=[100tx010ty001tz0001]T=\left[\begin{matrix}1&0&0&t_x\\0&1&0&t_y\\0&0&1&t_z\\0&0&0&1\\\end{matrix}\right]S=[sx0000sy0000sz00001]S=\left[\begin{matrix}s_x&0&0&0\\0&s_y&0&0\\0&0&s_z&0\\0&0&0&1\\\end{matrix}\right]

2-2. 표준기저벡터의 변화를 사용한 회전 변환 행렬

회전은 하나의 축을 변경하면 대부분의 축이 변경됩니다.
이에 따라 차원이 높아질 수록 해당 축의 회전은 더욱 넓은 범위의 축들을 변경하게 됩니다.

💡 예시로 2D 공간의 회전(zz축 회전)은 x,yx, y축만 영향을 끼치지만
3D 공간의 회전(x,yx, y축 회전)은 zz축에도 영향을 끼칩니다.

상세한 변환식의 내용은 아래에서 설명하도록 하고
위 그림처럼 변환된 표준 기저 벡터인 x,y,zx', y', z' (위 그림 상 x1,x2,x3x'_1, x'_2, x'_3) 값은

x(x1)=(xx,xy,xz)y(x2)=(yx,yy,yz)z(x3)=(zx,zy,zz)x'_{(x'_1)} = (x'_x, x'_y, x'_z) \\ y'_{(x'_2)} = (y'_x, y'_y, y'_z) \\ z'_{(x'_3)} = (z'_x, z'_y, z'_z)

라고 설명할 수 있으며, 아래와 같은 식으로 적용할 수 있습니다.

R=[xxyxzx0xyyyzy0xzyzzz00001]R=\left[\begin{matrix}x'_x&y'_x&z'_x&0\\x'_y&y'_y&z'_y&0\\x'_z&y'_z&z'_z&0\\0&0&0&1\\\end{matrix}\right]

😮 잠깐! TRSTRS행렬인데... 왜 막상 적용하려고 하면 SRTSRT순서인가요?

관련 질문 글 : Why do we call it TRS and MVP, instead of SRT and MVP or TRS and PVM?

이것에 대해 진지하게 정말 고민을 많이 했습니다. 처음엔 에이 완전 거꾸로 곱셈은 아니겠지... 했는데 완전 거꾸로 곱셈을 하는 상황이였습니다!

그러다 위 링크를 발견하게 되는데, 내용은 TRSTRSSRTSRT 표기에 대해 혼동하는 내용의 질문이였습니다.

DirectX와 OpenGL의 차이점을 설명한 글. 제가 제작 중인 엔진은 OpenGL이지만 최신 버전의 OpenGL은 어느정도 커스텀 된 렌더링 파이프라인이 가능한 덕분에 SRTSRT 순서로 적용하고 있습니다.

일단 정리된 내용으로는 행렬의 곱셈 순서는 전치행렬의 특성으로 인해 완전히 거꾸로 계산이 되며, 이와 같이 곱셈 순서가 서로 상반되어도 멀쩡하게 렌더링이 되는 것 입니다.

3. 오일러 각 회전

3-1. 오일러 각은 왜 필요할까?

위에서 설명한 회전행렬을 그대로 적용하기엔 변환할 축마다 x,y,zx, y, z값을 일일이 작성하고 적용하기엔 너무 노가다성이 큽니다! 하지만 우리에겐 오일러 각이라는 친구가 있습니다!

오일러 각은 3D상에서 x,y,zx, y, z처럼 3개의 축을 사용하는데
한눈에 알아보기 쉽고 앞서 배웠던 회전 행렬로 적용이 용이한 엄청난 개념이 있는 덕분에 회전을 쉽게 적용할 수 있습니다.
(물론 행렬 모두 적용하고 난 뒤에 쉽다는 뜻이였습니다...!)

오일러 각이 아니였으면... 엔진에서도 일일이 벡터값을 넣어줬을 것이라는 생각에 너무 아찔하네요!

3-2. 언리얼 엔진의 회전은 Rotator라는 특수한 구조체를 따로 쓰는 이유?

언리얼은 정말 잘 만든 엔진입니다! 그래픽 이론의 경량화를 통해 게임에 적용하는 엄청난 일을 주도하고, 무려 오픈소스이기 때문에 참고할 수 있는 내용이 정말 많습니다.

그러나 좌표계를 설명하자면...흠... 왼손 좌표계에 z up이라... 좀 많이 특이합니다.
이런 좌표계를 그대로 회전축에도 적용한다면... 아마 혼란스러움이 배가 되지 않을까 생각이 듭니다.

언리얼도 그렇게 생각했는지 회전축의 개념만큼은 x,y,zx, y, z가 아닌 yaw,pitch,rollyaw, pitch, roll의 개념으로 적용합니다.

하지만 에디터에선 직관적인 표기를 우선 시 해서인지 에디터 표기만큼은 x,y,zx, y, z로 표기한다고 합니다!

💡 언리얼은 이와같이 따로 회전축 관련 구조체를 적용했기 때문에 회전 범위도 00~360360까지만 적용할 수 있습니다. 다시 말하면 아예 구조체 따로 만든 김에 Clamp도 적용했다는 뜻입니다!

3-3. 오일러 각 회전을 구성하는 회전변환 행렬

오일러 각은 삼각함수를 통해 좌표값으로 변환이 가능합니다.
이러한 방식으로 앞서 선보였던 회전 변환 행렬에서 회전 기준을 오일러 각으로 적용하기만 하면 됩니다.

간단한 예시로 xx축을 θ\theta만큼 회전하는 것을 들어보겠습니다.
xx축은 xx'를 기준으로 돌리는 것이기 때문에 xx를 제외한 나머지 좌표값에 알맞는 벡터를 넣습니다.

x=(1,0,0)y=(0,cosθ,sinθ)z=(0,sinθ,cosθ)x' = (1, 0, 0) \\ y' = (0, \cos\theta, \sin\theta) \\ z' = (0, -\sin\theta, \cos\theta)

zyz'_y만 사인 값이 음수인가요?


위 그림과 같이 xx축이 시계방향으로 회전한다면 각 사분면 기준에서
zz축은 yy축의 양수로 회전하고 있고
yy축은 zz축의 음수로 회전하고 있습니다.
이런 방식으로 회전이 진행되기 때문에 zyz'_y값은 음수가 됩니다.

이러한 표준기저벡터를 회전 행렬에 적용을 한다면 최종적으로 다음과 같이 나타납니다.

Rx=[1000cosθsinθ0sinθcosθ]R_x=\left[\begin{matrix}1&0&0\\0&cos{\theta}&-sin{\theta}\\0&sin{\theta}&cos{\theta}\\\end{matrix}\right]

이와 같은 원리로 Ry,RzR_y, R_z를 아래와 같이 적용할 수 있습니다.

Ry=[cosθ0sinθ010sinθ0cosθ]          Rz=[cosθsinθ0sinθcosθ0001]R_y=\left[\begin{matrix}cos{\theta}&0&sin{\theta}\\0&1&0\\-sin{\theta}&0&cos{\theta}\\\end{matrix}\right] ~~ ~~ ~~ ~~ ~~ R_z=\left[\begin{matrix}cos{\theta}&-sin{\theta}&0\\sin{\theta}&cos{\theta}&0\\0&0&1\\\end{matrix}\right]

RyR_ysinθ-sin\theta값이 들어있는 위치가 다른가요?
저는 엔진에서 구현할 때 회전행렬 그대로 대충 집어넣다보니 뭔가 y축 회전의 z축만 따로 다른 방향으로 이동하던 기억이 있습니다.

이유는 모르겠지만 y축만 음수를 지정하는 상황이 다르다~ 이정도로만 생각하고 대수롭지 않게 적용했는데 왜 그런지 고민을 안하고 아 얘는 이런 공식이구나~ 하는 주입식 코딩의 문제점이 이 질문을 통해 이렇게 드러나버렸습니다....하핫....

일단 다시 본론으로 돌아와서 RyR_y만 다른 이유는 아래와 같습니다.
회전 행렬에서의 값 계산 순서를 xxyyzzxxyyzz 이런 식으로 예시를 들어보겠습니다.

여기서 RxR_x는 순서 상 yz(yz)yz (y'_z)평면에 음수가 들어가야 합니다.
그리고 RzR_z는 순서 상 xy(xy)xy(x'_y)평면에 음수가 들어가야 합니다.
그리고 문제의 RyR_y는 이전 순서 규칙과 같이 zx(zx)zx(z'_x)평면에 들어가야 하죠

이렇게 당연한 순서 규칙을 행렬로 보다보니 이런 불상사가 생겨버린 듯 합니다!

3-4. 세 회전 변환 행렬을 곱한 최종행렬

위에서 보시다시피 Rx,Ry,RzR_x, R_y, R_z는 각자 호환이 되지 않습니다!
따라서 각 축의 회전을 곱셈을 통해 적용을 해야합니다. 그럼 다음과 같은 식이 나타납니다.

R=Rx Ry RzR=R_x\cdot\ R_y\cdot\ R_z

3-5. 최종 행렬을 통해 각 표준기저벡터의 크기는 모두 1임을 증명해봅시다.

회전은 하나의 축을 기준으로 나머지 축을 오일러각에 의한 삼각함수 값 만큼 변환이 됩니다.
이 말의 뜻은 3개의 좌표값 중 반드시 하나는 0이고 나머지 2값은 각각 cos,sin\cos, \sin값이 들어갑니다.

다음과 같이 같은 각도인 θ\theta를 기준으로 sin\sincos\cos값을 더한 길이는 원을 구성하는 반지름에 해당되므로 r=x2+y2r = \sqrt{x^2 + y^2} 즉 벡터의 크기를 구하는 공식에 대입하면 1이 됩니다. (3차원의 구 지름을 구하는 공식도 이와 같은 원리입니다.)

따라서 이러한 성질의 표준기저벡터를 계속 곱해도 벡터의 크기는 무조건 1이 됩니다.

3-6. 최종 행렬을 통해 각 표준기저벡터가 모두 직교함을 증명해봅시다.

일단 zz축을 예시로 들어보겠습니다. zz축 회전은 다음과 같은 표준기저벡터의 형태를 띕니다.

x=(cosθ,sinθ,0)y=(sinθ,cosθ,0)z=(0,0,1)x' = (cos\theta, sin\theta, 0) \\ y' = (-sin\theta, cos\theta, 0) \\ z' = (0, 0, 1)
xy=cosθsinθ+sinθcosθ+0=0yz=0+0+0=0zx=0+0+0=0x'\cdot y' = -cosθsinθ + sinθcosθ + 0 = 0 \\ y'\cdot z' = 0 + 0 + 0 = 0 \\ z'\cdot x' = 0 + 0 + 0 = 0

여기서도 모든 내적값이 00이 됩니다.
이러한 성질의 표준기저벡터를 가진 행렬을 계속 곱해도 결국 내적값은 모두 00이 되기 때문에 회전 행렬은 모든 각도에 대한 표준기저벡터는 모두 직교합니다.

4. 카메라 행렬

4-1. 3차원 뷰 공간의 +z+z축은 왜 카메라 뒤편을 향할까?

3차원 공간에서의 카메라와 월드의 관점은 서로 반대로 놓여있습니다.위의 그림처럼 표현된다고 볼 수 있습니다.

먼저 카메라가 어떻게 적용되는지를 확인해봐야겠습니다.
월드에서의 좌표가 어떻든 카메라 관점에서 봤을 때 카메라는 세상의 좌표와 다르게 반대로 바라보는 모습이 될겁니다.

결국 서로 반대되는 좌표계로 인해 xxyy값이 예상과 다르게 반대로 뒤집힐 것입니다.

따라서 원래 좌표계를 사용하고자 한다면 카메라를 180°180\degree 회전시켜서 +z+z축이 카메라 뒤편을 향하게 하면 우리가 원하는 좌표계로 물체가 나타납니다.

4-2. 3차원 카메라의 트랜스폼으로부터 뷰 변환 행렬을 직접 구하기

앞서 작성한 것 처럼 카메라는 반대 방향으로 돌려서 적용을 하게 됩니다.
이는 평범하게 계산된 TRTR행렬을 반대로 돌리는 역행렬로 적용한 것과 같습니다.

이를 식으로 표현하자면 V=(TR)1=R1T1V=(TR)^{-1}=R^{-1}T^{-1} 가 됩니다.

이 식을 자세히 풀어보면

V=[xxxyxz0yxyyyz0zxzyzz00001][100tx010ty001tz0001]V = \left[\begin{matrix}x_x&x_y&x_z&0\\y_x&y_y&y_z&0\\z_x&z_y&z_z&0\\0&0&0&1\\\end{matrix}\right] * \left[\begin{matrix}1&0&0&-t_x\\0&1&0&{-t}_y\\0&0&1&{-t}_z\\0&0&0&1\\\end{matrix}\right]

위와 같이 적용을 하게됩니다.

4-3. 뷰 변환 행렬은 모델링 행렬과 다르게 왜 이동을 먼저 할까?

// 자체 엔진의 일반적인 모델링 행렬값을 반환하는 코드
return scale * rotation * translation * parentMatrix;

자체 엔진의 일반적인 모델링 행렬값을 반환하는 코드

모델링 행렬은 대체적으로 SRTS\cdot R\cdot T 과 같은 순서로 렌더링됩니다.
그런데 왜 뷰 변환 행렬은 이와 반대로 진행을 할까요?

이에 대한 해답은 역행렬의 성질에 있습니다. 뷰 행렬은 TRTR행렬의 역행렬이기 때문에 아래와 같은 식을 적용하고 있습니다.

V=(TR)1V=(TR)^{-1}

그리고 이 식을 살짝 분해하면 역행렬의 성질로 인해 T,RT, R의 곱셈 위치가 바뀝니다.

(TR)1=R1T1(TR)^{-1}=R^{-1}T^{-1}

따라서 뷰 행렬은 각 T,RT, R의 역행렬로 곱셈을 시도할 경우 모델링 행렬과 다르게 거꾸로 곱셈을 해주게 됩니다.

profile
(게임 엔진 프로그래머가 되고싶은) 게임 클라이언트 프로그래머
post-custom-banner

4개의 댓글

comment-user-thumbnail
2021년 11월 11일

좋은 글 적어주셔서 감사합니다. ^^

1개의 답글
comment-user-thumbnail
2021년 11월 17일

도움이 많이 되었습니다 감사합니다!

1개의 답글