게임공장
[Learn OpenGL 번역] 2-7. 시작하기 - 변환(Transformations) 본문
변환(Transformations)
시작하기/변환(Transformations)
우리는 이제 오브젝트를 생성하고 컬러를 입히고 텍스처를 이용하여 세밀하게 표현하는 방법을 알고 있지만 정적인 오브젝트이기 때문에 여전히 흥미롭지 못합니다. 각 프레임에서 그들의 vertex를 수정하고 버퍼를 재구성하여 움직이게 해볼 것입니다. 하지만 이는 번거롭고 프로세싱 파워를 꽤 많이 요구합니다. 오브젝트를
행렬은 아주 강력한 수학적 개념입니다. 처음에 보기에는 어려워보이지만 일단 익숙해지면 아주 유용하게 사용할 수 있습니다. 행렬을 다룰 때 우리는 약간의 수학적 개념들을 알아야하고 수학적 지식을 가지고 있는 독자들을 위해 추가 자료들을 제공할 것 입니다.
하지만 transformation을 완벽하게 이해하기 위해서는 먼저 행렬을 다루기에 앞서서 벡터에 대해 알아야 합니다. 이번 장의 중점은 나중에 필요한 기본 수학적 지식을 제공하는 것입니다. 이 부분이 어렵다면 능력이 되는대로 이해하려고 노력하시고 필요할 때마다 이 장을 다시 읽어보시기 바랍니다.
벡터
가장 기본적인 정의로서 벡터는 방향 그 이상도 아닙니다. 벡터는
아래에서 벡터는 2D 그래프의 (x,y)
를 화살표로 나타나는 것을 볼 수 있습니다. 벡터를 2D에서 보여주는 것이 직관적(3D에 비교해서)이기 때문에 2D 벡터를 z
좌표가 0
인 3D 벡터로 생각할 수 있습니다. 벡터는 방향을 나타내기 때문에 벡터의 원점의 값은 변경하지 않습니다. 아래의 그래프에서 우리는 벡터 v ¯ 와 w ¯ 가 그들의 원점이 다름에도 불구하고 같은 벡터임을 알 수 있습니다.
벡터 수학을 설명할 때 일반적으로 v ¯ 처럼 문자 위에 작은 바가 있는 표기법으로 표현합니다. 또한 수식에서 벡터를 표현할 때 다음과 같이 표시합니다.
벡터는 방향으로서 나타내어지기 때문에 때때로 그들을 위치로서 표현하기가 힘듭니다. 우리가 기본적으로 시각화 해놓은 것은 방향의 원점을 (0,0,0)
로 설정한 후 점을 지정하는 특정 방향을 가지는 (3,5)
는 그래프에서 (3,5)
를 가리키고 원점은 (0,0)
입니다. 벡터를 사용하여 방향 그리고 2D와 3D 공간에서의 위치를 나타낼 수 있습니다.
일반적인 숫자처럼 벡터도 연산을 정의할 수 있습니다(일부는 여러분이 이미 본 것들).
벡터의 스칼라 연산
위 수식에서 +는 +, −, ⋅ , ÷로 대체될 수 있고 ⋅ 는 곱하기 연산자입니다. − 및 ÷ 연산자의 역순서는 정의되지 않습니다.
역벡터
역벡터는 벡터의 반대방향을 나타내는 벡터입니다. 북동쪽을 가리키고 있는 벡터의 역벡터는 남서쪽을 가리키는 벡터입니다. 역벡터를 만들기 위해서는 각 요소에 마이너스 기호를 붙이면 됩니다(또한 -1
스칼라 곱을 해도 됩니다).
덧셈과 뺄셈
두 벡터의 덧셈은
v=(4,2)
와 k=(1,2)
의 덧셈을 시각화하면 다음과 같습니다.
일반적인 덧셈, 뺄셈과 마찬가지로 벡터의 뺄셈은 두 번째 벡터의 역벡터를 더한 것과 같습니다.
두 벡터를 서로 뺄셈을 하여 나온 결과는 두 벡터가 가리키는 지정과 다릅니다. 이는 두 지점 사이와 다른 벡터를 찾아야 할 때 유용하게 사용될 수 있습니다.
크기(길이)
벡터의 길이/크기를 구하기 위해 x
와 y
요소를 삼각형의 두 변으로 시각화하면 벡터가 삼각형을 형성합니다.
두 변 (x, y)
의 길이는 알고 있고 v ¯ 변의 길이를 알아야하기 때문에 피타고라스 정리를 사용하여 다음과 같이 계산할 수 있습니다.
v ¯ 벡터의 길이는 ||v ¯ || 로 표기될 수 있습니다. 3D 에서는 방정식에 z 2 을 추가함으로써 쉽게 확장될 수 있습니다.
이 경우에는 (4, 2)
벡터의 길이는 다음과 같습니다.
결과는 4.47
입니다.
이를 벡터
벡터와 벡터의 곱셈
두 벡터의 곱셈은 약간 낯설 것입니다. 일반적인 곱셈은 시각화하여도 의미가 없기 때문에 벡터에서 정의되지 않습니다. 하지만 곱셈을 할 때에 선택할 수 있는 2개의 특정한 경우가 있습니다. 하나는 v ¯ ⋅k ¯ 로 표기 되는
내적
두 벡터의 내적은 그들 길이의 스칼라 곱에 두 벤터 사이의 각의 코사인을 곱한 것과 같습니다. 이해가 가지 않는다면 다음 공식을 살펴 봅시다.
두 벡터 사이의 각은 세타(θ)로 나타내집니다. 이게 왜 흥미롭냐고요? v ¯ 와 k ¯ 가 단위 벡터라면 그들의 길이는 1로 같을 것입니다. 이는 위의 공식을 효과적으로 간단히 만들 수 있습니다.
이제 내적은 오직 두 벡터 사이의 각만을 정의합니다. 각도가 0도면 코사인은 1
을 나타내며 각도가 90도면 코사인은 0
을 나타내는 것을 기억하고 있을 것입니다. 내적을 이용하여 두 벡터가 sin
이나 cosine
에 대해서 더 알고 싶다면 Khan Academy videos를 추천합니다.
그래우 어떻게 내적을 계산할까요? 내적은 요소들끼리 서로 곱하는 것입니다. 두 단위 벡터의 내적은 다음과 같습니다(두 벡터의 길의는 정확히 1
이라는 것을 생각하세요).
이 두 벡터 사이의 각도를 계산하기 위해 코사인의 역함수 cos −1 를 사용합니다. 그리고 이 결과는 143.1
도 입니다. 이제 우리는 두 벡터 사이의 각도를 효과적으로 계산할 수 있습니다. 내적은 빛 계산을 할 때 아주 유용합니다.
외적
외적은 오직 3D 공간에서만 정의되고 평행하지 않는 두 개의 벡터를 입력으로 받으며 두 벡터에 직교하는 하나의 벡터를 생성합니다. 입력된 두 벡터가 서로 직교한다면 외적의 결과는 3개의 직교 벡터가 됩니다. 이는 다음 강좌에서 유용하게 쓰일 수 있습니다. 다음 이미지는 이 것이 3D 공간에서 어떻게 보여지는지 나타냅니다.
다른 연산들과 달리 외적은 선형 대수학을 탐구하지 않으면 직관적이지 않으므로 공식을 외워 두는 것이 가장 좋습니다. 그러면 괜찮을 것입니다(그러지 않아도 괜찮습니다). 아래에서 직교하는 벡터 A와 벡터 B를 외적하는 것을 볼 수 있습니다.
보시다시피 정말로 이상합니다. 하지만 이 단계를 거친다면 입력된 벡터와 직교하는 새로운 벡터를 얻을 수 있습니다.
행렬
이제 벡터에 대해서 거의 모든 것을 다루었습니다. 이제 행렬에 들어갈 차례입니다! 행렬은 기본적으로 숫자, 기호, 수식들의 사각 배열입니다. 각각의 아이템들은 행렬의
행렬은 (i,j)
로 인덱싱됩니다. i
는 행이고 j
는 열입니다. 이 것이 위의 행렬이 2x3 행렬이라고 불리는 이유입니다(3 열과 2행, 행렬의 (x,y)
로 인덱싱 할 때와 정반대입니다. 값 4를 찾기 위해 (2,1)
로 인덱싱하면 됩니다(두 번째 행, 첫 번째 열).
행렬은 기본적으로 다진 수학 수식의 사각 배열일 뿐 그 이상도 아닙니다. 행렬은 아주 멋진 수학적 특성들을 가지고 있고 벡터처럼 여러 연산자들을 정의할 수 있습니다(덧셈, 뺄셈, 곱셈).
덧셈과 뺄셈
행렬과 스칼라 사이의 덧셈과 뺄셈은 다음과 같이 정의됩니다.
스칼라 값은 기본적으로 행렬의 각각의 요소들에 더해집니다. 행렬-스칼라 뺄셈에서도 같습니다.
두 행렬사이의 덧셈과 뺄셈은 행렬의 요소별로 수행됩니다. 그래서 우리가 알고 있는 일반적인 수와 동일한 규칙들이 적용되지만 동일한 인덱스를 가진 두 행렬의 요소에만 적용됩니다. 이는 같은 차원의 행렬에 대해서만 덧셈과 뺄셈이 이루어진다는 뜻입니다. 3x2 행렬과 2x3 행렬(또는 3x3 행렬과 4x4 행렬)은 서로 덧셈과 뺄셈을 수행할 수 없습니다. 두 개의 2x2 행렬로 덧셈을 하는 방법을 살펴봅시다.
행렬의 뺄셈에도 같은 규칙이 적용됩니다.
행렬-스칼라 곱셈
덧셈, 뺄셈과 마찬가지로 행렬-스칼라 행렬의 각 요소들을 스칼라로 곱합니다. 다음은 곱셈의 예를 보여줍니다.
이제 저 하나의 숫자를 왜 스칼라라고 불리는지 알 수 있을 것입니다. 스칼라는 기본적으로 행렬의 모든 요소들의 값을 조정(scales)하므로 모든 요소들은 2
로 인해 조정됩니다.
지금까지는 좋습니다. 지금 까지의 것들은 그렇게 복잡하지는 않았습니다. 행렬-행렬 곱셈을 시작하기 전까지는 말이죠.
행렬-행렬 곱셈
행렬의 곱셈이 반드시 복잡하지는 않지만 쉽게 하기 힘듭니다. 행렬 곱셈은 기본적으로 곱셈을 할 때 미리 정의된 규칙을 따릅니다. 몇가지 제한사항이 있습니다.
- 왼쪽 행렬의 열의 갯수와 오른쪽 행렬의 행의 갯수가 같아야만 곱셈을 수행할 수 있습니다.
- 행렬의 곱셈은
교환법칙 이 성립하지 않습니다. 즉 A⋅B≠B⋅A.
두 개의 2x2
행렬의 곱셈 예를 보면서 시작해봅시다.
지금 이게 도대체 무슨일인지 알고 싶을 것입니다. 행렬의 곱셈은 왼쪽 행렬의 행들과 오른쪽 행렬의 열들의 일반적인 곱셈과 덧셈이 혼합되어 있습니다. 다음 이미지를 보고 설명드리도록 하겠습니다.
우리는 먼저 왼쪽 행렬의 위쪽 행과 오른쪽 행렬의 한 열을 가져옵니다. 우리가 선택한 행과 열은 계산하려는 2x2
행렬의 결과 값을 결정하게 됩니다. 만약 왼쪽 행렬의 첫번째 행을 가져왔다면 결과 값은 결과 행렬의 첫 번째 행렬에 나타나게 됩니다. 그런 다음 오른쪽 행렬의 첫 번째 열을 가져왔다면 결과 값은 결과 행렬의 첫 번째 열에 나타나게 됩니다. 이는 정확히 빨간색으로 표시한 경우에 해당합니다. 우측 하단의 결과를 계싼하기 위해서는 첫 번째 행렬의 아래 행과 두 번째 행렬의 오른쪽 열을 가져와야 합니다.
결과 값을 계산하기 위해 우리는 행과 열의 첫 번째 요소를 일반적인 곱셈을 이용하여 곱합니다. 두 번째, 세 번째, 네 번째 요소들도 똑같이 수행합니다. 각각의 곱셈의 결과들이 요약되고 결과가 나타납니다. 이제 제한사항 중 하나가 왼쪽 행렬의 열 갯수와 오른쪽 행렬의 행 갯수가 동일해야 한다는 것인 이유를 이해할 수 있습니다. 그렇지 않으면 연산을 완료할 수 없기 때문이죠!
결과는 (n,m
) 차원의 행렬입니다. n
은 왼쪽 행렬의 행의 갯수와 같고 m
은 오른쪽 행렬의 열의 갯수와 같습니다.
머릿속에 곱셈을 상상하기 어려워도 걱정하지 마세요. 손으로 계산하려고 노력해보시고 어려움을 느낄때마다 이 페이지를 다시 읽어보세요. 시간이 지남에 따라 익숙해질 것입니다.
큰 예제를 보면서 행렬-행렬 곱셈을 끝내도록 합시다. 컬러를 사용하여 머릿속에 패턴을 그려보세요. 유용한 연습으로 다음 곱셈에 대한 여러분의 답을 생각한 다음 정답과 비교해보세요(손으로 행렬의 곱셈을 시도하면 빨리 이해할 수 있을 것입니다).
보시다시피 행렬-행렬 곱셈은 꽤 번거로운 작업이고 틀리기 쉽습니다(일반적으로 이 작업을 컴퓨터를 이용하여 수행하는 이유입니다). 그리고 행렬이 커지면 문제가 빠르게 발생합니다. 여러분이 더 많은 것을 알고 싶다면 Khan Academy videos를 볼것을 강력하게 권장합니다.
아무튼 이제 행렬을 서로 곱셈하는 방법을 알게 되었으니 좋은 것을 시작해볼 수 있습니다.
행렬-벡터 곱셈
지금까지 우리는 이 강좌를 벡터와 공정하게 나누었습니다. 우리는 벡터를 위치, 컬러, 텍스처 좌표를 나타내는데 사용했습니다. 좀 더 깊이 생각해보면 벡터는 기본적으로 Nx1
차원의 행렬임을 알 수 있습니다. 여기서 N
은 벡터의 요소의 갯수입니다(Nx1
벡터로 곱할 수 있는 MxN
행렬이 있다고 한다면 이 행렬의 열의 갯수와 벡터의 행의 갯수가 같기 때문에 행렬의 곱셈이 성립합니다.
하지만 왜 행렬과 벡터가 곱셈이 가능한지를 왜 따지고 있을까요? 많은 흥미로운 2D/3D 변환들은 행렬 내부에 있고 벡터에 그 행렬을 곱하면 우리의 벡터가 변환(transform)되어 집니다. 이 경우에 아마 혼란스러울 것입니다. 예제를 보면서 시작해봅시다. 곧 무슨 뜻이지 이해할 수 있을 것입니다.
단위 행렬
OpenGL에서 우리는 여러가지 이유때문에 일반적으로 4x4
변환 행렬을 가지고 작업합니다. 그 이유 중 하나는 대부분의 벡터의 크기가 4이기 때문입니다. 우리가 생각할 수 있는 가장 간단한 변환 행렬은 NxN
행렬이며 대각선을 제외하고는 단지 0만 갖는 행렬입니다. 곧 보시게 될 이 변환 행렬은 벡터에 아무런 영향을 끼치지 않습니다.
이 벡터는 완전히 그대로입니다. 이 것은 곱셈의 규칙에서 분명해집니다. 첫 번째 결과 요소는 곱해진 행렬의 첫 번째 행의 각각의 요소들입니다. 이 행의 각각의 요소들은 첫 번째 요소를 제외하고 모두 0입니다. 1⋅1+0⋅2+0⋅3+0⋅4=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
로 설정할 수 있습니다. 그렇게 하면 영향을 끼치지 않습니다. 우리가 방금 한 스케일 연산은
스케일을 하는 변환 행렬을 만들어보겠습니다. 단위 행렬로부터 각 대각 요소가 대응하는 벡터 요소와 곱해진 것을 보았습니다. 단위 행렬의 1
을 3
으로 바꾸면 어떻게 될까요? 이 경우에는 벡터의 각 요소들에 3
을 곱하게 됩니다. 따라서 벡터를 3으로 효과적으로 스케일할 수 있습니다. 만약 스케일 변수들을 (S 1 ,S 2 ,S 3 )처럼 나타낸다면 (x,y,z)벡터에 대한 스케일 행렬을 다음과 같이 정의할 수 있습니다.
스케일된 벡터의 4 번째 값은 여전히 1
입니다. 3D 공간에서 2
요소를 스케일하는 것은 정의되지 않기 때문입니다. w
요소는 나중에 볼 다른 목적으로 사용됩니다.
Translation(이동)
스케일 행렬과 마찬가지로 4x4 행렬위에 특정한 연산을 하여 그들을 이동시키기 위해 우리가 사용할 여러가지 위치가 있습니다. 바로 4번째 열의 위에서부터 3번째까지의 값들입니다. 우리는 이동 행렬을 다음과 같이 정의할 수 있습니다.
이동 값들 모두가 벡터의 w
요소와 곱해져 벡터의 원본 값들에 더해지기 때문에 동작하게 됩니다(행렬 곱셈의 규칙을 기억하세요). 3x3 행렬로는 가능하지 않습니다.
벡터의
w
요소는 x
, y
, z
좌표를 w
좌표로 나눕니다. 우리는 일반적으로 w
요소가 대부분 1.0
이기 때문에 알아차리지 않습니다. 동차 좌표를 사용하면 여러가지 장점이 있습니다. 3D 벡터를 이동시킬 수 있도록 하고(w
요소 없이는 벡터를 이동시키지 못합니다) 3D 비주얼을 생성하기 위해 다음 강좌에서 사용할 것입니다.또한 동차 좌표가
0
일 경우 벡터는 특병히 w
좌표의 값이 0
인 좌표는 이동시킬 수 없기 때문입니다.
이동 행렬을 사용하여 우리는 3가지 방향(x
, y,
, z
)으로 오브젝트를 이동시킬 수 있습니다. 우리의 변환 도구에 대한 아주 유용한 변환 행렬입니다.
Rotation(회전)
마지막 변환은 비교적 이해하기 쉽고 2D, 3D 공간에서 시각화하기 쉽지만 회전은 좀 까다롭습니다. 어떻게 이 행렬들이 만들어졌는지 정확히 히해하려면 linear algebra 영상을 보는 것을 추천합니다.
먼저 벡터의 회전이 실제로 무엇을 의미하는지 알아봅시다. 2D, 3D에서의 회전은
각도에 의한 각 = 라디안 * (180.0f / PI)
라디안에 의한 각 = 각도 * (PI / 180.0f)
PI
는 3.14159265359
와 같습니다.
반원을 돌리면 360/2 = 180도 회전하고 1/5회전하면 360/5 = 72도 회전합니다. 이 것은 벡터 v ¯ 가 벡터 k ¯ 를 오른쪽으로 72도 회전한 벡터임을 의미합니다.
3D 공간에서의 회전은 각 그리고
삼각법을 사용하면 주어진 각에 대해 벡터를 회전하여 새로운 벡터로 변환하는 것이 가능합니다. 이는 일반적으로 sine
과 cosine
함수의 조합으로 수행되어집니다(흔히 sin
, cos
로 축약하여 표현합니다). 어떻게 변환 행렬이 생성되는지에 대한 설명은 이 강좌의 범위를 벗어납니다.
회전 행렬은 3D 공간에서 각 축에 대해 정의됩니다. 각은 세타 기호 θ로 나타내어집니다.
X 축에 대해서 회전:
Y 축에 대해서 회전:
Z 축에 대해서 최전:
회전 행렬을 사용하면 위치 벡터를 세 가지의 축 중 하나에 대해 변환시킬 수 있습니다. 예를 들어 X 축에 대해 회전한 후 Y 축에 대해 회전하는 것처럼 이들을 조합해서 사용하는 것도 가능합니다. 하지만 이는 (0.662, 0.2, 0.722)
(이 것은 단위 벡터입니다). 그런 (끔찍한) 행렬이 존재하며 밑에 주어져있습니다. (R x ,R y ,R z )가 임의의 회전 축입니다.
이러한 행렬을 생성하는 것에 관한 설명은 이 강좌의 범위에서 벗어납니다. 이러한 행렬 조차도 gimbal lack을 완벽히 예방하지는 못한다는 것을 알아두세요(심지어 이것보다 더 어려워도). 진짜로 Gimbal lock을 예방하기 위해선
행렬 조합
변환을 위해 행렬을 사용하는 것의 진정한 힘은 여러 변환들을 하나의 행렬에 조합할 수 있다는 것입니다. 행렬-행렬 곱셈에게 고마워해야 할 것입니다. 여러 변환들을 조합한 변환 행렬을 만들 수 있는지 알아봅시다. (x,y,z)
벡터를 가지고 있고 이 것을 2만큼 스케일한 후 (1,2,3)
만큼 이동시키고자 한다고 가정해봅시다. 우리는 이를 위해 이동 행렬과 스케일 행렬이 필요합니다. 결과 변환 행렬은 다음과 같습니다.
행렬을 곱할 때 먼저 이동을 한 후 스케일 변환을 한다는 것을 알아 두세요. 행렬 곱은 교환법칙이 성립하지 않기 때문에 순서가 중요합니다. 행렬을 곱할 때 가장 오른쪽에 있는 행렬이 벡터와 처음으로 곱해지므로 곱셈은 오른쪽에서 왼쪽으로 읽어야 합니다. 행렬을 조합할 때 먼저 스케일 연산을 한 후 회전 연산을 하고 마지막으로 이동 연산을 하는 것을 권장합니다. 그렇게 하지 않으면 (부정적인) 효과를 서로에게 줄 수 있습니다. 예를 들어 여러분이 먼저 이동을한 후 스케일링을 했다면 이동 벡터 또한 스케일되어집니다.
마지막 변환 행렬을 우리의 벡터에 적용하면 결과는 다음 벡터와 같습니다.
잘했습니다! 이 벡터는 먼저 2만큼 스케일된 후 (1,2,3)
만큼 이동됩니다.
사용
이제 우리는 변환의 모든 이론을 설명했으므로 실제로 이 지식의 이점을 어떻게 활용할 수 있는지 알아보도록 하겠습니다. OpenGL 행렬이나 벡터 지식에 대한 어떠한 형식도 가지고 있지 않으므로 우리만의 수학관련 클래스와 함수들을 만들어야 합니다. 강좌에서는 모든 수학적 세부사항을 추상화하고 간단히 사용할 수 있는 미리 만들어진 수하 라이브러리를 사용합니다. 운좋게도 사용하기 쉽고 OpenGL과 호환되는 수학 라이브러리 GLM이 있습니다.
GLM
GLM은 OpenGL Mathematics의 약자이고 헤더 파일만 있는 라이브러리입니다. 이는 적절한 헤더 파일만 포함하면 된다는 것을 의미합니다. 링킹과 컴파일이 필요없습니다. GLM 그들의 웹사이트에서 다운로드 가능합니다(0.9.9
). 헤더 파일들의 루트 디렉터리를 여러분의 includes 폴더에 복사하세요.
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
를 선언합니다. 다음 단계는
함수에 우리의 단위 행렬을 집어 넣어 변환 행렬을 생성하는 것입니다. 또한 변환 벡터(주어진 행렬은 변환 벡터와 곱해지고 결과로 행렬을 리턴합니다)도 집어 넣습니다.
그런 다음 우리의 벡터와 변환 행렬을 곱한 후 결과를 출력합니다. 이동 행렬이 어떻게 동작하는지 기억하고 있다면 결과 벡터는 (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은 각을 라디안으로 들어오길 원하므로
함수를 사용하여 각도를 라디안으로 변환시킵니다. 이 텍스처가 입혀진 사각형은 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);
}
mat2
와 mat3
타입도 가지고 있습니다. 이들은 GLSL also has mat2
and mat3
벡터와 마찬가지로 혼합 비슷한 연산을 할 수 있게 해줍니다. 앞서 언급한 모든 수학 연산들(스칼라-행렬 곱, 행렬-벡터 곱, 행렬-행렬 곱과 같은)은 행렬 타입위에서 가능합니다. 특별한 행렬 연산이 사용되는 곳이라면 무슨 일인지 설명해드릴 것입니다.
uniform을 추가하였고 gl_Position에 넘겨주기 전에 변환 행렬과 위치 벡터를 곱해주었습니다. 우리의 컨테이너는 이제 2배 작아지고 90
도 회전되야 합니다(왼쪽으로 기울어집니다). 이제 변환 행렬을 shader로 넘겨주어야 합니다.
unsigned int transformLoc = glGetUniformLocation (ourShader.ID, "transform");
glUniform Matrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
먼저 uniform 변수의 location을 확인한 후 Matrix4fv
를 접미사로 함께 사용하여 행렬 데이터를 shader에 보냅니다. 첫 번째 파라미터는 지금까지와 다를게 없이 uniform의 location을 나타냅니다. 두 번째 파라미터는 OpenGL에게 몇개의 행렬을 넣을 것인지를 알려줍니다. 여기서는 1
개 입니다. 세 번째 파라미터는 행과 열을 바꿀 것인지 물어보는 것입니다. OpenGL 개발자들은 GL_FALSE
로 지정합니다. 마지막 파라미터는 실제 행렬 데이터이지만 GLM의 행렬은 정확히 OpenGL이 받기 원하는 형태의 행렬이 아니므로 먼저 GLM의
변환 행렬을 생성했고 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
을 사용하면 음수 스케일이 적용되는 즉시 오브젝트가 반전된다는 것을 알아두세요): 해답.
'OpenGL' 카테고리의 다른 글
[Learn OpenGL 번역] 2-9. 시작하기 - 카메라 (0) | 2018.07.15 |
---|---|
[Learn OpenGL 번역] 2-8. 시작하기 - 좌표 시스템 (2) | 2018.07.15 |
[Learn OpenGL 번역] 2-6. 시작하기 - Textures (4) | 2018.07.15 |
[Learn OpenGL 번역] 2-5. 시작하기 - Shaders (6) | 2018.07.15 |
[Learn OpenGL 번역] 2-4. 시작하기 - Hello Triangle (2) | 2018.07.15 |