Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

게임공장

[Learn OpenGL 번역] 2-7. 시작하기 - 변환(Transformations) 본문

OpenGL

[Learn OpenGL 번역] 2-7. 시작하기 - 변환(Transformations)

짱승_ 2018. 7. 15. 19:57

변환(Transformations)

시작하기/변환(Transformations)

  우리는 이제 오브젝트를 생성하고 컬러를 입히고 텍스처를 이용하여 세밀하게 표현하는 방법을 알고 있지만 정적인 오브젝트이기 때문에 여전히 흥미롭지 못합니다. 각 프레임에서 그들의 vertex를 수정하고 버퍼를 재구성하여 움직이게 해볼 것입니다. 하지만 이는 번거롭고 프로세싱 파워를 꽤 많이 요구합니다. 오브젝트를 변환(transform) 하는 방법에는 좋은 방법들이 있습니다. 이는 (여러개의) 행렬(matrix) 객체들을 사용하는 방법입니다.


행렬은 아주 강력한 수학적 개념입니다. 처음에 보기에는 어려워보이지만 일단 익숙해지면 아주 유용하게 사용할 수 있습니다. 행렬을 다룰 때 우리는 약간의 수학적 개념들을 알아야하고 수학적 지식을 가지고 있는 독자들을 위해 추가 자료들을 제공할 것 입니다.


하지만 transformation을 완벽하게 이해하기 위해서는 먼저 행렬을 다루기에 앞서서 벡터에 대해 알아야 합니다. 이번 장의 중점은 나중에 필요한 기본 수학적 지식을 제공하는 것입니다. 이 부분이 어렵다면 능력이 되는대로 이해하려고 노력하시고 필요할 때마다 이 장을 다시 읽어보시기 바랍니다.

벡터

  가장 기본적인 정의로서 벡터는 방향 그 이상도 아닙니다. 벡터는 방향(direction)크기(magnitude)(강도나 길이로 알려져 있는)를 가지고 있습니다. 보물지도의 길이라고 생각하면 됩니다. '왼쪽으로 10걸음, 북쪽으로 3걸음, 오른쪽으로 5걸음'. 여기서 '왼쪽'은 벡터에서의 방향이고 '10걸음'은 벡터에서의 크기입니다. 따라서 이 보물지도에서 길은 3개의 벡터를 가지고 있습니다. 벡터는 어떠한 차원이든 가질 수 있지만 우리는 일반적으로 2차원, 4차원으로 사용할 것입니다. 만약 벡터가 2차원을 사용하고 있다면 평면(2D 그래픽을 생각해보세요)에서의 방향을 나타내고 3차원을 사용하고 있다면 3D 공간에서의 방향을 나타냅니다.


  아래에서 벡터는 2D 그래프의 (x,y)를 화살표로 나타나는 것을 볼 수 있습니다. 벡터를 2D에서 보여주는 것이 직관적(3D에 비교해서)이기 때문에 2D 벡터를 z 좌표가 0인 3D 벡터로 생각할 수 있습니다. 벡터는 방향을 나타내기 때문에 벡터의 원점의 값은 변경하지 않습니다. 아래의 그래프에서 우리는 벡터 v ¯ w ¯ 가 그들의 원점이 다름에도 불구하고 같은 벡터임을 알 수 있습니다.



  벡터 수학을 설명할 때 일반적으로 v ¯ 처럼 문자 위에 작은 바가 있는 표기법으로 표현합니다. 또한 수식에서 벡터를 표현할 때 다음과 같이 표시합니다.



  벡터는 방향으로서 나타내어지기 때문에 때때로 그들을 위치로서 표현하기가 힘듭니다. 우리가 기본적으로 시각화 해놓은 것은 방향의 원점을 (0,0,0)로 설정한 후 점을 지정하는 특정 방향을 가지는 위치 벡터(position vector)로 만듭니다(또한 다른 원점을 지정할 수 도 있습니다). 위치 벡터 (3,5)는 그래프에서 (3,5)를 가리키고 원점은 (0,0)입니다. 벡터를 사용하여 방향 그리고 2D와 3D 공간에서의 위치를 나타낼 수 있습니다.


  일반적인 숫자처럼 벡터도 연산을 정의할 수 있습니다(일부는 여러분이 이미 본 것들).

벡터의 스칼라 연산

  스칼라(scalar) 는 하나의 숫자입니다(또는 하나의 요소를 가진 벡터). 스칼라와 함께 벡터에 대해 더하기/빼기/곱하기/나누기 할 때 우리는 간단히 벡터의 각 요소들을 스칼라로 더하거나 빼거나 곱하거나 나누면 됩니다. 예를 들어 다음과 같습니다.



  위 수식에서 +는 +, −, ⋅ , ÷로 대체될 수 있고 ⋅ 는 곱하기 연산자입니다. − 및 ÷ 연산자의 역순서는 정의되지 않습니다.

역벡터

  역벡터는 벡터의 반대방향을 나타내는 벡터입니다. 북동쪽을 가리키고 있는 벡터의 역벡터는 남서쪽을 가리키는 벡터입니다. 역벡터를 만들기 위해서는 각 요소에 마이너스 기호를 붙이면 됩니다(또한 -1 스칼라 곱을 해도 됩니다).



덧셈과 뺄셈

  두 벡터의 덧셈은 component-wise(요소끼리)로서 정의됩니다. 이는 벡터의 각 요소들과 다른 벡터의 각 요소들을 더하는 것을 의미합니다.



v=(4,2)k=(1,2)의 덧셈을 시각화하면 다음과 같습니다.



  일반적인 덧셈, 뺄셈과 마찬가지로 벡터의 뺄셈은 두 번째 벡터의 역벡터를 더한 것과 같습니다.



  두 벡터를 서로 뺄셈을 하여 나온 결과는 두 벡터가 가리키는 지정과 다릅니다. 이는 두 지점 사이와 다른 벡터를 찾아야 할 때 유용하게 사용될 수 있습니다.



크기(길이)

  벡터의 길이/크기를 구하기 위해 Pythagoras theorem을 사용합니다. 이 피타고라스 정리는 수학 시간에 배웠었을 것입니다. 벡터의 xy 요소를 삼각형의 두 변으로 시각화하면 벡터가 삼각형을 형성합니다.



  두 변 (x, y)의 길이는 알고 있고 v ¯ 변의 길이를 알아야하기 때문에 피타고라스 정리를 사용하여 다음과 같이 계산할 수 있습니다.



  v ¯ 벡터의 길이는 ||v ¯ || 로 표기될 수 있습니다. 3D 에서는 방정식에 z 2 을 추가함으로써 쉽게 확장될 수 있습니다.


  이 경우에는 (4, 2) 벡터의 길이는 다음과 같습니다.



  결과는 4.47입니다.


  단위 벡터(unit vector)라고 불리는 벡터의 특별한 유형이 있습니다. 단위 벡터는 하나의 여분의 속성을 가지고 있으며 길이는 정확히 1입니다. 특정 벡터의 단위 벡터 n ^ 을 계산하기 위해서 벡터의 각 요소들을 벡터의 길이로 나눕니다.



  이를 벡터 정규화(normalizing)라고 부릅니다. 단위 벡터는 위에 작은 지붕이 있는 형태로 표시됩니다. 오직 그들의 방향에 대해서만 고려해야할 때 유용하게 쓰일 수 있습니다(벡터의 크기/길이를 변경하여도 방향은 변하지 않습니다).

벡터와 벡터의 곱셈

  두 벡터의 곱셈은 약간 낯설 것입니다. 일반적인 곱셈은 시각화하여도 의미가 없기 때문에 벡터에서 정의되지 않습니다. 하지만 곱셈을 할 때에 선택할 수 있는 2개의 특정한 경우가 있습니다. 하나는 v ¯ k ¯ 로 표기 되는 내적(dot product)이고 다른 하나는 v ¯ ×k ¯ 로 표기 되는 외적(cross product)입니다.

내적

  두 벡터의 내적은 그들 길이의 스칼라 곱에 두 벤터 사이의 각의 코사인을 곱한 것과 같습니다. 이해가 가지 않는다면 다음 공식을 살펴 봅시다.



  두 벡터 사이의 각은 세타(θ)로 나타내집니다. 이게 왜 흥미롭냐고요? v ¯ k ¯ 가 단위 벡터라면 그들의 길이는 1로 같을 것입니다. 이는 위의 공식을 효과적으로 간단히 만들 수 있습니다.



  이제 내적은 오직 두 벡터 사이의 각만을 정의합니다. 각도가 0도면 코사인은 1을 나타내며 각도가 90도면 코사인은 0을 나타내는 것을 기억하고 있을 것입니다. 내적을 이용하여 두 벡터가 직각(orthogonal)인지 아닌지 혹인 평행한지 아닌지 확인할 수 있습니다. sin이나 cosine에 대해서 더 알고 싶다면 Khan Academy videos를 추천합니다.

  단위 벡터가 아닌 두 벡터에 대해서도 각도를 계산할 수 있지만 결과로 나온 값을 두 벡터의 길이로 나누어줘야 cosθ만 남게 됩니다.

  그래우 어떻게 내적을 계산할까요? 내적은 요소들끼리 서로 곱하는 것입니다. 두 단위 벡터의 내적은 다음과 같습니다(두 벡터의 길의는 정확히 1이라는 것을 생각하세요).



  이 두 벡터 사이의 각도를 계산하기 위해 코사인의 역함수 cos 1 를 사용합니다. 그리고 이 결과는 143.1도 입니다. 이제 우리는 두 벡터 사이의 각도를 효과적으로 계산할 수 있습니다. 내적은 빛 계산을 할 때 아주 유용합니다.

외적

 외적은 오직 3D 공간에서만 정의되고 평행하지 않는 두 개의 벡터를 입력으로 받으며 두 벡터에 직교하는 하나의 벡터를 생성합니다. 입력된 두 벡터가 서로 직교한다면 외적의 결과는 3개의 직교 벡터가 됩니다. 이는 다음 강좌에서 유용하게 쓰일 수 있습니다. 다음 이미지는 이 것이 3D 공간에서 어떻게 보여지는지 나타냅니다.



  다른 연산들과 달리 외적은 선형 대수학을 탐구하지 않으면 직관적이지 않으므로 공식을 외워 두는 것이 가장 좋습니다. 그러면 괜찮을 것입니다(그러지 않아도 괜찮습니다). 아래에서 직교하는 벡터 A와 벡터 B를 외적하는 것을 볼 수 있습니다.



  보시다시피 정말로 이상합니다. 하지만 이 단계를 거친다면 입력된 벡터와 직교하는 새로운 벡터를 얻을 수 있습니다.

행렬

  이제 벡터에 대해서 거의 모든 것을 다루었습니다. 이제 행렬에 들어갈 차례입니다! 행렬은 기본적으로 숫자, 기호, 수식들의 사각 배열입니다. 각각의 아이템들은 행렬의 요소(element)라고 불립니다. 예를 들어 2x3 행렬은 다음과 같이 나타납니다.



  행렬은 (i,j)로 인덱싱됩니다. i는 행이고 j는 열입니다. 이 것이 위의 행렬이 2x3 행렬이라고 불리는 이유입니다(3 열과 2행, 행렬의 dimemsions라고도 부릅니다). 이는 2D 그래프를 (x,y)로 인덱싱 할 때와 정반대입니다. 값 4를 찾기 위해 (2,1)로 인덱싱하면 됩니다(두 번째 행, 첫 번째 열).


  행렬은 기본적으로 다진 수학 수식의 사각 배열일 뿐 그 이상도 아닙니다. 행렬은 아주 멋진 수학적 특성들을 가지고 있고 벡터처럼 여러 연산자들을 정의할 수 있습니다(덧셈, 뺄셈, 곱셈).

덧셈과 뺄셈

  행렬과 스칼라 사이의 덧셈과 뺄셈은 다음과 같이 정의됩니다.



  스칼라 값은 기본적으로 행렬의 각각의 요소들에 더해집니다. 행렬-스칼라 뺄셈에서도 같습니다.



  두 행렬사이의 덧셈과 뺄셈은 행렬의 요소별로 수행됩니다. 그래서 우리가 알고 있는 일반적인 수와 동일한 규칙들이 적용되지만 동일한 인덱스를 가진 두 행렬의 요소에만 적용됩니다. 이는 같은 차원의 행렬에 대해서만 덧셈과 뺄셈이 이루어진다는 뜻입니다. 3x2 행렬과 2x3 행렬(또는 3x3 행렬과 4x4 행렬)은 서로 덧셈과 뺄셈을 수행할 수 없습니다. 두 개의 2x2 행렬로 덧셈을 하는 방법을 살펴봅시다.



  행렬의 뺄셈에도 같은 규칙이 적용됩니다.



행렬-스칼라 곱셈

  덧셈, 뺄셈과 마찬가지로 행렬-스칼라 행렬의 각 요소들을 스칼라로 곱합니다. 다음은 곱셈의 예를 보여줍니다.



  이제 저 하나의 숫자를 왜 스칼라라고 불리는지 알 수 있을 것입니다. 스칼라는 기본적으로 행렬의 모든 요소들의 값을 조정(scales)하므로 모든 요소들은 2로 인해 조정됩니다.

  지금까지는 좋습니다. 지금 까지의 것들은 그렇게 복잡하지는 않았습니다. 행렬-행렬 곱셈을 시작하기 전까지는 말이죠.

행렬-행렬 곱셈

  행렬의 곱셈이 반드시 복잡하지는 않지만 쉽게 하기 힘듭니다. 행렬 곱셈은 기본적으로 곱셈을 할 때 미리 정의된 규칙을 따릅니다. 몇가지 제한사항이 있습니다.


  1. 왼쪽 행렬의 열의 갯수와 오른쪽 행렬의 행의 갯수가 같아야만 곱셈을 수행할 수 있습니다.
  2. 행렬의 곱셈은 교환법칙이 성립하지 않습니다. 즉 ABBA.

  두 개의 2x2 행렬의 곱셈 예를 보면서 시작해봅시다.



  지금 이게 도대체 무슨일인지 알고 싶을 것입니다. 행렬의 곱셈은 왼쪽 행렬의 행들과 오른쪽 행렬의 열들의 일반적인 곱셈과 덧셈이 혼합되어 있습니다. 다음 이미지를 보고 설명드리도록 하겠습니다.



  우리는 먼저 왼쪽 행렬의 위쪽 행과 오른쪽 행렬의 한 열을 가져옵니다. 우리가 선택한 행과 열은 계산하려는 2x2 행렬의 결과 값을 결정하게 됩니다. 만약 왼쪽 행렬의 첫번째 행을 가져왔다면 결과 값은 결과 행렬의 첫 번째 행렬에 나타나게 됩니다. 그런 다음 오른쪽 행렬의 첫 번째 열을 가져왔다면 결과 값은 결과 행렬의 첫 번째 열에 나타나게 됩니다. 이는 정확히 빨간색으로 표시한 경우에 해당합니다. 우측 하단의 결과를 계싼하기 위해서는 첫 번째 행렬의 아래 행과 두 번째 행렬의 오른쪽 열을 가져와야 합니다.


  결과 값을 계산하기 위해 우리는 행과 열의 첫 번째 요소를 일반적인 곱셈을 이용하여 곱합니다. 두 번째, 세 번째, 네 번째 요소들도 똑같이 수행합니다. 각각의 곱셈의 결과들이 요약되고 결과가 나타납니다. 이제 제한사항 중 하나가 왼쪽 행렬의 열 갯수와 오른쪽 행렬의 행 갯수가 동일해야 한다는 것인 이유를 이해할 수 있습니다. 그렇지 않으면 연산을 완료할 수 없기 때문이죠!


  결과는 (n,m) 차원의 행렬입니다. n은 왼쪽 행렬의 행의 갯수와 같고 m은 오른쪽 행렬의 열의 갯수와 같습니다.


  머릿속에 곱셈을 상상하기 어려워도 걱정하지 마세요. 손으로 계산하려고 노력해보시고 어려움을 느낄때마다 이 페이지를 다시 읽어보세요. 시간이 지남에 따라 익숙해질 것입니다.


  큰 예제를 보면서 행렬-행렬 곱셈을 끝내도록 합시다. 컬러를 사용하여 머릿속에 패턴을 그려보세요. 유용한 연습으로 다음 곱셈에 대한 여러분의 답을 생각한 다음 정답과 비교해보세요(손으로 행렬의 곱셈을 시도하면 빨리 이해할 수 있을 것입니다).



  보시다시피 행렬-행렬 곱셈은 꽤 번거로운 작업이고 틀리기 쉽습니다(일반적으로 이 작업을 컴퓨터를 이용하여 수행하는 이유입니다). 그리고 행렬이 커지면 문제가 빠르게 발생합니다. 여러분이 더 많은 것을 알고 싶다면 Khan Academy videos를 볼것을 강력하게 권장합니다.


  아무튼 이제 행렬을 서로 곱셈하는 방법을 알게 되었으니 좋은 것을 시작해볼 수 있습니다.

행렬-벡터 곱셈

  지금까지 우리는 이 강좌를 벡터와 공정하게 나누었습니다. 우리는 벡터를 위치, 컬러, 텍스처 좌표를 나타내는데 사용했습니다. 좀 더 깊이 생각해보면 벡터는 기본적으로 Nx1 차원의 행렬임을 알 수 있습니다. 여기서 N은 벡터의 요소의 갯수입니다(N-dimensional 벡터라고도 부릅니다). 생각해보면 이해가 될 것입니다. 벡터는 행렬과 마찬가지고 숫자들의 배열이지만 오직 하나의 열만을 가지고 있습니다. 그래서 이 사실이 우리에게 무슨 도움이 될까요? 만약 Nx1 벡터로 곱할 수 있는 MxN 행렬이 있다고 한다면 이 행렬의 열의 갯수와 벡터의 행의 갯수가 같기 때문에 행렬의 곱셈이 성립합니다.


  하지만 왜 행렬과 벡터가 곱셈이 가능한지를 왜 따지고 있을까요? 많은 흥미로운 2D/3D 변환들은 행렬 내부에 있고 벡터에 그 행렬을 곱하면 우리의 벡터가 변환(transform)되어 집니다. 이 경우에 아마 혼란스러울 것입니다. 예제를 보면서 시작해봅시다. 곧 무슨 뜻이지 이해할 수 있을 것입니다.

단위 행렬

  OpenGL에서 우리는 여러가지 이유때문에 일반적으로 4x4 변환 행렬을 가지고 작업합니다. 그 이유 중 하나는 대부분의 벡터의 크기가 4이기 때문입니다. 우리가 생각할 수 있는 가장 간단한 변환 행렬은 단위 행렬(identify matrix)입니다. 단위 행렬은 NxN 행렬이며 대각선을 제외하고는 단지 0만 갖는 행렬입니다. 곧 보시게 될 이 변환 행렬은 벡터에 아무런 영향을 끼치지 않습니다.



  이 벡터는 완전히 그대로입니다. 이 것은 곱셈의 규칙에서 분명해집니다. 첫 번째 결과 요소는 곱해진 행렬의 첫 번째 행의 각각의 요소들입니다. 이 행의 각각의 요소들은 첫 번째 요소를 제외하고 모두 0입니다. 11+02+03+04=1을 얻을 수 있습니다. 벡터의 다른 3개의 요소에서도 똑같이 적용됩니다.

  변환하지 않는 변환 행렬이 무슨 의미가 있는지 궁금할 것입니다. 단위 행렬은 일반적으로 다른 변환 행렬을 생성하는 데에 시작 지점이고 선형 대수학을 더 깊게 파고 들면, 정리를 증명하고 선형 방정식을 푸는 데에 매우 유용한 행렬입니다.

Scaling(확대, 축소)

  벡터를 스케일링할 때 우리는 화살표의 방향은 그대로 둔 채로 우리가 원하는 만큼 길이를 증가시킨다. 우리는 2,3 차원에서 작업하기 때문에 2, 3개의 스케일 변수를 가지고있는 벡터들을 사용할 수 있습니다. 이는 각 하나의 축을 스케일합니다(x, y, z).


  벡터 v ¯ =(3,2) 를 스케일해봅시다. 우리는 x의 축으로 0.5만큼 스케일할 것입니다. 따라서 그것은 2배로 줄어들 것입니다. 그리고 y축으로 2만큼 스케일할 것입니다. 따라서 그것은 2배로 길어질 것입니다. 벡터를 (0.5, 2)로 스케일하면 어떻게 보이는지 살펴보도록 하겠습니다. 결과는 s ¯ 벡터입니다.



  OpenGL은 일반적으로 3D 공간에서 동작한다는 것을 잊지마세요. 이 2D 예시에서는 z축의 스케일 값을 1로 설정할 수 있습니다. 그렇게 하면 영향을 끼치지 않습니다. 우리가 방금 한 스케일 연산은 비균일(non-uniform)스케일입니다. 스케일링 요소가 각 축마다 같지 않기 때문입니다. 스칼라가 모든 축에서 동일하다면 균일 스케일(uniform sacle)이라고 부릅니다.


  스케일을 하는 변환 행렬을 만들어보겠습니다. 단위 행렬로부터 각 대각 요소가 대응하는 벡터 요소와 곱해진 것을 보았습니다. 단위 행렬의 13으로 바꾸면 어떻게 될까요? 이 경우에는 벡터의 각 요소들에 3을 곱하게 됩니다. 따라서 벡터를 3으로 효과적으로 스케일할 수 있습니다. 만약 스케일 변수들을 (S 1 ,S 2 ,S 3 )처럼 나타낸다면 (x,y ,z )벡터에 대한 스케일 행렬을 다음과 같이 정의할 수 있습니다.



  스케일된 벡터의 4 번째 값은 여전히 1입니다. 3D 공간에서 2요소를 스케일하는 것은 정의되지 않기 때문입니다. w 요소는 나중에 볼 다른 목적으로 사용됩니다.

Translation(이동)

  Translation은 원본 벡터 위에 다른 벡터를 더하여 다른 위치의 새로운 벡터를 반환하는 작업입니다. 따라서 벡터를 moving(이동)시킵니다. 우리는 이미 벡터의 덧셈에 대해서 배웠으므로 쉬울 것입니다.


  스케일 행렬과 마찬가지로 4x4 행렬위에 특정한 연산을 하여 그들을 이동시키기 위해 우리가 사용할 여러가지 위치가 있습니다. 바로 4번째 열의 위에서부터 3번째까지의 값들입니다. 우리는 이동 행렬을 다음과 같이 정의할 수 있습니다.



  이동 값들 모두가 벡터의 w 요소와 곱해져 벡터의 원본 값들에 더해지기 때문에 동작하게 됩니다(행렬 곱셈의 규칙을 기억하세요). 3x3 행렬로는 가능하지 않습니다.

Homogeneous coordinates(동차 좌표)
  벡터의 w 요소는 homogeneous coordinate(동차 좌표)라고도 불립니다. 동차 좌표로부터 3D 벡터를 가져오기 위해 x, y, z 좌표를 w 좌표로 나눕니다. 우리는 일반적으로 w 요소가 대부분 1.0이기 때문에 알아차리지 않습니다. 동차 좌표를 사용하면 여러가지 장점이 있습니다. 3D 벡터를 이동시킬 수 있도록 하고(w 요소 없이는 벡터를 이동시키지 못합니다) 3D 비주얼을 생성하기 위해 다음 강좌에서 사용할 것입니다.

  또한 동차 좌표가 0일 경우 벡터는 특병히 방향 벡터라고 불립니다. w 좌표의 값이 0인 좌표는 이동시킬 수 없기 때문입니다.

  이동 행렬을 사용하여 우리는 3가지 방향(x, y,, z)으로 오브젝트를 이동시킬 수 있습니다. 우리의 변환 도구에 대한 아주 유용한 변환 행렬입니다.

Rotation(회전)

  마지막 변환은 비교적 이해하기 쉽고 2D, 3D 공간에서 시각화하기 쉽지만 회전은 좀 까다롭습니다. 어떻게 이 행렬들이 만들어졌는지 정확히 히해하려면 linear algebra 영상을 보는 것을 추천합니다.


  먼저 벡터의 회전이 실제로 무엇을 의미하는지 알아봅시다. 2D, 3D에서의 회전은 각(angle)으로 나타내어집니다. 각은 각도(degree)나 라디안(radian)으로 나타낼 수 있으며 원은 360도(degree)로, 2 PI 라디안(radians)로 나타낼 수 있습니다. 개인적으로 각도로 작업하는 것을 선호합니다. 저에게 있어서 좀 더 이해하기 쉽기 때문입니다.

  대부분의 회전 함수들은 라디안으로 각을 표현하기를 요구합니다. 하지만 운좋게 각도는 라디안으로 쉽게 변환될 수 있습니다.
각도에 의한 각 = 라디안 * (180.0f / PI)
라디안에 의한 각 = 각도 * (PI / 180.0f)
PI3.14159265359와 같습니다.

  반원을 돌리면 360/2 = 180도 회전하고 1/5회전하면 360/5 = 72도 회전합니다. 이 것은 벡터 v ¯ 가 벡터 k ¯ 를 오른쪽으로 72도 회전한 벡터임을 의미합니다.



  3D 공간에서의 회전은 각 그리고 회전 축을 사용합니다. 지정된 각은 주어진 축에 대해 최전시킵니다. 하나의 회전 축을 바라보며 여러분의 머리를 회전시켜보면서 생각해보세요. 3D 공간에서 2D 벡터를 회전시킬 때 회전축을 z축으로 설정합니다(상상해보세요).


  삼각법을 사용하면 주어진 각에 대해 벡터를 회전하여 새로운 벡터로 변환하는 것이 가능합니다. 이는 일반적으로 sinecosine 함수의 조합으로 수행되어집니다(흔히 sin, cos로 축약하여 표현합니다). 어떻게 변환 행렬이 생성되는지에 대한 설명은 이 강좌의 범위를 벗어납니다.


  회전 행렬은 3D 공간에서 각 축에 대해 정의됩니다. 각은 세타 기호 θ로 나타내어집니다.


  X 축에 대해서 회전:



  Y 축에 대해서 회전:



  Z 축에 대해서 최전:



  회전 행렬을 사용하면 위치 벡터를 세 가지의 축 중 하나에 대해 변환시킬 수 있습니다. 예를 들어 X 축에 대해 회전한 후 Y 축에 대해 회전하는 것처럼 이들을 조합해서 사용하는 것도 가능합니다. 하지만 이는 Gimbal lock이라고 불리는 문제를 야기시킬 수 있습니다. 자세한 사항을 서명하지 않을 것이지만 더 나은 해결방법은 회전 행렬을 조합하는 것 대신 임의의 단위 축을 중심으로 즉시 회전하는 것입니다. 예를 들어 (0.662, 0.2, 0.722)(이 것은 단위 벡터입니다). 그런 (끔찍한) 행렬이 존재하며 밑에 주어져있습니다. (R x ,R y ,R z )가 임의의 회전 축입니다.



  이러한 행렬을 생성하는 것에 관한 설명은 이 강좌의 범위에서 벗어납니다. 이러한 행렬 조차도 gimbal lack을 완벽히 예방하지는 못한다는 것을 알아두세요(심지어 이것보다 더 어려워도). 진짜로 Gimbal lock을 예방하기 위해선 사원수(quaternions)를 사용하여 회전시키는 것입니다. 사원수를 사용하여 회전하면 더 안전할 뿐만 아니라 계산하는 데에 있어 더 친숙합니다. 하지만 사원스에 대한 설명은 나중 강좌에서 다루도록 하겠습니다.

행렬 조합

  변환을 위해 행렬을 사용하는 것의 진정한 힘은 여러 변환들을 하나의 행렬에 조합할 수 있다는 것입니다. 행렬-행렬 곱셈에게 고마워해야 할 것입니다. 여러 변환들을 조합한 변환 행렬을 만들 수 있는지 알아봅시다. (x,y,z) 벡터를 가지고 있고 이 것을 2만큼 스케일한 후 (1,2,3)만큼 이동시키고자 한다고 가정해봅시다. 우리는 이를 위해 이동 행렬과 스케일 행렬이 필요합니다. 결과 변환 행렬은 다음과 같습니다.



  행렬을 곱할 때 먼저 이동을 한 후 스케일 변환을 한다는 것을 알아 두세요. 행렬 곱은 교환법칙이 성립하지 않기 때문에 순서가 중요합니다. 행렬을 곱할 때 가장 오른쪽에 있는 행렬이 벡터와 처음으로 곱해지므로 곱셈은 오른쪽에서 왼쪽으로 읽어야 합니다. 행렬을 조합할 때 먼저 스케일 연산을 한 후 회전 연산을 하고 마지막으로 이동 연산을 하는 것을 권장합니다. 그렇게 하지 않으면 (부정적인) 효과를 서로에게 줄 수 있습니다. 예를 들어 여러분이 먼저 이동을한 후 스케일링을 했다면 이동 벡터 또한 스케일되어집니다.


  마지막 변환 행렬을 우리의 벡터에 적용하면 결과는 다음 벡터와 같습니다.



  잘했습니다! 이 벡터는 먼저 2만큼 스케일된 후 (1,2,3)만큼 이동됩니다.

사용

  이제 우리는 변환의 모든 이론을 설명했으므로 실제로 이 지식의 이점을 어떻게 활용할 수 있는지 알아보도록 하겠습니다. OpenGL 행렬이나 벡터 지식에 대한 어떠한 형식도 가지고 있지 않으므로 우리만의 수학관련 클래스와 함수들을 만들어야 합니다. 강좌에서는 모든 수학적 세부사항을 추상화하고 간단히 사용할 수 있는 미리 만들어진 수하 라이브러리를 사용합니다. 운좋게도 사용하기 쉽고 OpenGL과 호환되는 수학 라이브러리 GLM이 있습니다.

GLM

  GLM은 OpenGL Mathematics의 약자이고 헤더 파일만 있는 라이브러리입니다. 이는 적절한 헤더 파일만 포함하면 된다는 것을 의미합니다. 링킹과 컴파일이 필요없습니다. GLM 그들의 웹사이트에서 다운로드 가능합니다(0.9.9). 헤더 파일들의 루트 디렉터리를 여러분의 includes 폴더에 복사하세요.

  GLM 0.9.9 버전부터는 초기화된 기본 행렬이 단위 행렬이 아닌 0으로 초기화된 행렬입니다. 이 버전에서는 glm::mat4 mat = glm::mat4(1.0f)와 같은 형식으로 행렬을 초기화해야 합니다. 이 강좌의 코드와 일관성을 유지하려면 0.9.9보다 낮은 버전을 사용하거나 언급한 모든 행렬을 초기화하는 것이 좋습니다.

  우리가 필요한 GLM의 대부분의 기능은 단 3개의 헤더 파일에서찾을 수 있습니다.


#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

  우리의 지식을 잘 사용해서 (1,0,0) 벡터를 이동하여 (1,1,0) 벡터로 변환할 수 있을지 한 번 보겠습니다(glm::vec4로 정의하면 1.0으로 설정된 정방행렬이 됩니다).


glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;

  먼저 GLM의 벡터 클래스를 사용하여 vec 벡터를 선언합니다. 그런 다음 4x4 단위 행렬인 mat4를 선언합니다. 다음 단계는 glm::translate 함수에 우리의 단위 행렬을 집어 넣어 변환 행렬을 생성하는 것입니다. 또한 변환 벡터(주어진 행렬은 변환 벡터와 곱해지고 결과로 행렬을 리턴합니다)도 집어 넣습니다.
  그런 다음 우리의 벡터와 변환 행렬을 곱한 후 결과를 출력합니다. 이동 행렬이 어떻게 동작하는지 기억하고 있다면 결과 벡터는 (1+1,0+1,0+0)연산을 하여 (2,1,0) 벡터가 됩니다. 이 짧은 코드는 210을 출력하고 변환 행렬을 자신의 일을 수행한 것입니다.


  좀더 흥미로운 것을 해봅시다. 이전의 강좌에서의 컨테이너 오브젝트를 스케일하고 회전해봅시다.


glm::mat4 trans;
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));  

  먼저 우리는 컨테이너를 각 축에 대해 0.5만큼 스케일한 후 Z 축을 중심으로 90도 회전시킵니다. GLM은 각을 라디안으로 들어오길 원하므로 glm::radians 함수를 사용하여 각도를 라디안으로 변환시킵니다. 이 텍스처가 입혀진 사각형은 XY 평면이기 때문에 Z 축을 중심으로 돌리기를 원합니다. 우리가 중심으로 돌리기 위한 축은 반드시 단위 벡터여야하므로 회전하지 않는다면 벡터가 정규화 되었는지 확인하세요. GLM 함수에 행렬을 전달하기 때문에 GLM 자동적으로 행렬을 서로 곱하고 결과로 모든 변환이 조합된 변환 행렬을 리턴합니다.


  shader에서는 어떻게 이 변환 행렬을 가져올 까요? 우리는 GLSL에 mat4 타입이 있다고 잠시 언급했었습니다. 그래서 우리는 vertex shader가 mat4 uniform 변수를 받고 위치 벡터와 이 행렬 uniform을 곱할 것입니다.


#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;
  
uniform mat4 transform;

void main()
{
    gl_Position = transform * vec4(aPos, 1.0f);
    TexCoord = vec2(aTexCoord.x, aTexCoord.y);
} 
  또한 GLSL은 mat2mat3 타입도 가지고 있습니다. 이들은 GLSL also has mat2 and mat3 벡터와 마찬가지로 혼합 비슷한 연산을 할 수 있게 해줍니다. 앞서 언급한 모든 수학 연산들(스칼라-행렬 곱, 행렬-벡터 곱, 행렬-행렬 곱과 같은)은 행렬 타입위에서 가능합니다. 특별한 행렬 연산이 사용되는 곳이라면 무슨 일인지 설명해드릴 것입니다.

  uniform을 추가하였고 gl_Position에 넘겨주기 전에 변환 행렬과 위치 벡터를 곱해주었습니다. 우리의 컨테이너는 이제 2배 작아지고 90도 회전되야 합니다(왼쪽으로 기울어집니다). 이제 변환 행렬을 shader로 넘겨주어야 합니다.


unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

  먼저 uniform 변수의 location을 확인한 후 glUniform함수를 Matrix4fv를 접미사로 함께 사용하여 행렬 데이터를 shader에 보냅니다. 첫 번째 파라미터는 지금까지와 다를게 없이 uniform의 location을 나타냅니다. 두 번째 파라미터는 OpenGL에게 몇개의 행렬을 넣을 것인지를 알려줍니다. 여기서는 1개 입니다. 세 번째 파라미터는 행과 열을 바꿀 것인지 물어보는 것입니다. OpenGL 개발자들은 column-major ordering라고 불리는 GLM의 기본 행렬 레이아웃인 내부 행렬 레이아웃을 사용하므로 행과 열을 바꿀 필요가 없습니다. 그래서 우리는 이 파라미터를 GL_FALSE로 지정합니다. 마지막 파라미터는 실제 행렬 데이터이지만 GLM의 행렬은 정확히 OpenGL이 받기 원하는 형태의 행렬이 아니므로 먼저 GLM의 value_ptr함수를 사용하여 행렬을 변환해주어야 합니다.


  변환 행렬을 생성했고 vertex shader에 uniform을 선언하였으며 우리의 vertex 좌표를 변환할 shader에 행렬을 보내주었습니다. 결과는 다음과 같이 보일 것입니다.



  완벽합니다! 우리 컨테이너가 왼쪽으로 기울어졌고 2배 작아졌으므로 변환이 성공했습니다. 재미삼아 좀더 펑키한 것을 해보겠습니다. 시간에 따라 계속 회전하고 컨테이너를 윈도우의 우측 하단으로 위치시켜보겠습니다. 시간에 따라 컨테이너를 회전시키려면 게임 루프 안에서 변환 행렬을 계속해서 수정해야 합니다. 우리는 시간에 따른 각을 얻기위해 GLFW의 시간 함수를 사용합니다.


glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));

  이전의 경우에 변환 행렬을 아무데나 선언해도 됬지만 지금은 루프가 돌때마다 선언해야 회전을 계속해서 업데이트할 수 있습니다. 이는 게임 루프가 돌때마다 변환 행렬을 재생성해야 한다는 것을 의미합니다. 일반적으로 화면을 렌더링할 때 루프가 돌때마다 새로운 값으로 재생성된 여러 변환 행렬들을 사용 합니다.


  여기에서 먼저 컨테이너를 원점(0,0,0)을 중심으로 회전시키고 회전하면 회전된 컨테이너를 화면의 우측 하단으로 이동시킵니다. 코드에서는 이동을 한 후 나중에 회전을 시켰어도 실제 변환 순서는 거꾸로 읽어야 한다는 것을 기억하세요. 실제 변환은 먼저 회전을 적용시키고 그후에 이동을 적용시킵니다. 변환의 이 모든 조합들을 이해하는 것과 이 조합들이 오브젝트에 어떻게 적용시키는가에 대한 것은 이해하기 쉽지 않습니다. 변환에 대한 시도와 경험이 이해하기 쉽도록 해줄 것입니다.


잘 했다면 다음과 같은 결과를 볼 수 있을 것입니다.



  이동된 컨테이너가 시간에 따라 회전되고 있습니다. 이 모든 것이 하나의 변환 행렬에 의해서 수행되어졌습니다! 이제 여러분은 그래픽 작업에서 행렬이 왜 이렇게 중요한지 이해할 수 있을 것입니다. 우리는 무한한 양의 변환을 정의할 수 있으며 모든 것을 하나의 행렬에 결합하여 우리가 원하는 만큼 자주 재사용할 수 있습니다. 이처럼 vertex shader에서 변환을 사용하면 데이터를 계속해서 다시 전달(이는 꽤 느립니다)할 필요가 없기 때문에 vertex 데이터를 다시 정의하는 수고를 덜어주고 일부 작업에서 시간을 아낄 수 있습니다.


  올바른 결과를 얻지 못했거나 다른 어딘가에서 문제가 생겼다면 소스 코드를 확인하고 shader 클래스를 수정해보세요.


  다음 강좌에서는 행렬을 사용하여 vertex에 대해 서로 다른 좌표 공간을 정의하는 방법에 대해 설명할 것입니다. 이 것은 실시간 3D 그래픽에 다가가는 우리의 첫 걸음이 될 것입니다!

추가 자료

  • Essence of Linear Algebra: Grant Sanderson의 변형 및 선형 대수학의 기본 수학에 대한 훌륭한 비디오 강좌 시리즈

연습

  • 컨테이너에서의 마지막 변환을 사용하여 먼저 회전한 후 이동시켜 변환 순서를 바꿔보세요. 어떤 일이 일어나는지 관찰하고 왜 이런 일이 일어나는지 생각해보세요: 해답
  • glDrawElements 함수를 사용하여 두 번째 컨테이너를 그려보고 이 컨테이너는 변환 사용하여 다른 위치에 놓아보세요. 이 두번째 컨테이너는 창의 좌측 상단에 배치하고 회전하는 대신 시간이 지남에 따라 스케일링 해보세요(여기에서 sin함수가 유용하게 쓰일 것입니다. 그리고 sin을 사용하면 음수 스케일이 적용되는 즉시 오브젝트가 반전된다는 것을 알아두세요): 해답.



출처 : https://learnopengl.com, Joey de Vries

반응형