컴퓨터 공학/OpenGL

[최신 OpenGL 시작하기 #1_7] 변환 (Transformation)

James Kim_ 2025. 3. 18. 13:44

이전 시간에 Texture에 이어서 변환(Transformation)을 배워 보겠습니다.

변환(Transformations)

이전 시간까지 객체를 생성하고, 색상을 입히거나 텍스처를 사용하여 세부적인 외관을 설정했지만, 정적 객체라는 한계가 있습니다. 동적으로 매 프레임마다 정점을 변경하고 버퍼를 재구성하여 움직이게 할 수 있지만, 코드가 너무 길어집니다. 객체를 변환하는 훨씬 더 좋은 방법이 있는데, 바로 행렬 객체(martrix objects)를 사용하는 것입니다.

 

행렬은 처음에는 무섭게 보이지만, 익숙해지면 매우 유용한 이론인데요. 행렬에 대해 논의할 때, 우리는 약간의 수학에 빠져들어야 하며, 수학적으로 더 관심 있는 독자들을 위해 추가 자료도 첨부할 것입니다.

 

그러나 변환을 완전히 이해하기 위해서는 행렬을 논의하기 전에 벡터에 대해 더 깊이 알아봐야 합니다. 이 장의 초점은 나중에 필요한 주제에 대한 기본적인 수학적 배경을 제공하는 것입니다. 주제가 어렵다면, 최대한 이해하려고 노력하고 필요할 때 이 장으로 돌아와서 읽어보세요

벡터(Vectors)

가장 기본적인 정의에서, 벡터는 방향이며 그 이상은 아닙니다. 벡터는 방향(direction) 크기(magnitude)(강도 ' strength ' 또는 길이'length' 라고도 함)를 갖습니다. 벡터를 보물 지도의 방향처럼 생각할 수 있습니다: '왼쪽으로 10보 가고, 위로 3보, 오른쪽으로 5보 가라'; 여기서 '왼쪽'은 방향이고 '10보'는 벡터의 크기입니다. 따라서 보물 지도의 방향에는 3개의 벡터가 포함됩니다. 벡터는 어떤 차원도 가질 수 있지만, 우리는 일반적으로 2~4 차원으로 작업합니다. 벡터가 2차원이면 평면(2D 그래프를 생각해보세요)에서의 방향을 나타내고, 3차원이면 3D 세계에서의 모든 방향을 나타낼 수 있습니다.

 

아래에서 각 벡터가 2D 그래프에서 화살표로 표현된 (x,y)로 표시된 3개의 벡터를 볼 수 있습니다. 2D에서 벡터를 표시하는 것이 더 직관적이기 때문에(3D보다), 2D 벡터를 z 좌표가 0인 3D 벡터로 생각할 수 있습니다. 벡터는 방향을 나타내기 때문에 벡터의 원점은 그 값을 변경하지 않습니다. 아래 그래프에서 벡터 는 원점이 다르지만 동일함을 볼 수 있습니다:

수학자들은 일반적으로 벡터를 v̄와 같이 머리 위에 작은 막대가 있는 문자 기호로 설명하는 것을 선호합니다. 또한, 공식에서 벡터를 표시할 때 일반적으로 다음과 같이 표시됩니다: 

벡터는 방향으로 지정되기 때문에 위치로 시각화하기가 때로는 어렵습니다. 벡터를 위치로 시각화하려면 방향 벡터의 원점이 (0,0,0)이라고 상상하고 특정 지점을 지정하는 특정 방향을 가리키면, 이것이 위치 벡터가 됩니다(또는 다른 원점을 지정하고 "이 원점에서 저 공간의 지점을 가리키는 벡터"라고 말할 수도 있습니다). 위치 벡터 (3,5)는 원점이 (0,0)인 그래프에서 (3,5)를 가리킵니다. 벡터를 사용하여 2D 및 3D 공간에서 방향과 위치를 설명할 수 있습니다.

 

일반 숫자와 마찬가지로 벡터에도 여러 연산을 정의할 수 있습니다

스칼라 벡터 연산(Scalar vector operations)

스칼라(Scalar)는 단일 숫자입니다. 벡터에 스칼라를 더하거나 빼거나 곱하거나 나눌 때, 우리는 단순히 벡터의 각 요소에 스칼라를 더하거나 빼거나 곱하거나 나눕니다. 덧셈의 경우 다음과 같이 보일 것입니다:

여기서 +는 +, −, ⋅ 또는 ÷ 일 수 있으며, ⋅은 곱셈 연산자입니다.

벡터 부정 (Vertor negation)

벡터를 부정하면 반대 방향의 벡터가 생성됩니다. 북동쪽을 가리키는 벡터는 부정 후 남서쪽을 가리킬 것입니다. 벡터를 부정하려면 각 구성 요소에 마이너스 부호를 추가합니다(-1의 스칼라 값을 가진 스칼라 벡터 곱셈으로 표현할 수도 있습니다):

더하기와 빼기(Addition and subtraction)

두 벡터의 덧셈은 구성 요소(component-wise)별 덧셈으로 정의됩니다. 즉, 한 벡터의 각 구성 요소가 다른 벡터의 동일한 구성 요소에 더해집니다:

시각적으로, 벡터 v=(4,2)와 k=(1,2)에서는 다음과 같이 보입니다. 두 번째 벡터가 첫 번째 벡터의 끝에 추가되어 결과 벡터의 끝점을 찾습니다(head-to-tail 방법):

일반적인 덧셈 및 뺄셈과 마찬가지로 벡터 뺄셈은 부정된 두 번째 벡터를 더하는 것과 동일합니다: 

서로에게서 두 벡터를 빼면 두 벡터가 가리키는 위치 차이인 벡터가 생성됩니다. 이는 두 점 사이의 차이인 벡터가 필요한 특정 경우에 유용합니다.

길이(Length)

벡터의 길이/크기를 구하기 위해 수학 수업에서 기억할 수 있는 피타고라스 정리를 사용합니다. 벡터의 개별 x 및 y 구성 요소를 삼각형의 두 변으로 시각화하면 삼각형이 형성됩니다:

두 변(x, y)의 길이를 알고 있고 기울어진 변 v̄의 길이를 알고 싶다면 피타고라스 정리를 사용하여 계산할 수 있습니다: 

여기서 ||v̄||는 벡터 v̄의 길이를 나타냅니다. 이것은 방정식에 z²를 추가하여 3D로 쉽게 확장됩니다.

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

즉, 4.47입니다.

 

단위 벡터(unit vector)라고 부르는 특별한 종류의 벡터도 있습니다. 단위 벡터는 길이가 정확히 1이라는 추가 속성을 가집니다. 벡터의 각 구성 요소를 길이로 나누어 임의의 벡터에서 단위 벡터 n̂을 계산할 수 있습니다:

이것을 벡터 정규화(normalizing)라고 부릅니다. 단위 벡터는 머리 위에 작은 지붕이 있는 것으로 표시되며 일반적으로 작업하기가 더 쉽습니다. 특히 방향만 중요할 때는 벡터의 길이를 변경해도 방향은 변하지 않기 때문입니다.

벡터-벡터 곱셈(Vector-vector multiplication)

두 벡터를 곱하는 것은 약간 이상한 경우입니다. 일반적인 곱셈은 벡터에서 실제로 정의되지 않았기 때문에 시각적인 의미가 없지만, 곱셈할 때 선택할 수 있는 두 가지 특정 경우가 있습니다: 하나는 v̄ ⋅ k̄로 표시되는 내적(dot product)이고 다른 하나는 v̄ × k̄로 표시되는 외적(cross product)입니다.

내적(Dot product)

두 벡터의 내적은 v̄ , k̄ 길이의 스칼라 곱에 v̄ , k̄ 사이의 각도의 cosine을 곱한 것과 같습니다. 이것이 혼란스럽게 들린다면 공식을 살펴보세요: 

여기서 그들 사이의 각도는 세타(θ)로 표현됩니다. 왜 이것이 흥미로울까요? v̄와 k̄가 단위 벡터라면 그들의 길이는 1과 같을 것입니다. 그렇다면 다음과 같이 공식을 줄여서 표현할수있습니다.: 

이제 내적은 두 벡터 사이의 각도만 정의합니다. cosine 또는 cos function가 각도가 90도일 때 0이 되고 각도가 0일 때 1이 된다는 것을 기억할 수 있습니다. 이를 통해 내적을 사용하여 두 벡터가 서로 직교(orthogonal)하거나 평행(parallel)한지 쉽게 테스트할 수 있습니다(직교는 벡터가 서로 직각에 있다는 것을 의미합니다). sin 또는 cos 함수에 대해 더 알고 싶다면 기본 삼각법에 관한 Khan Academy 동영상을 확인해 보세요.

비-단위 벡터 사이의 각도도 계산할 수 있지만, 그 경우에는 cosθ만 남도록 결과에서 두 벡터의 길이를 나눠야 합니다.

 

그렇다면 내적을 어떻게 계산할까요? 내적은 구성 요소별 곱셈으로, 결과를 모두 더합니다. 두 단위 벡터로 다음과 같이 보입니다(둘 다의 길이가 정확히 1이라는 것을 확인할 수 있습니다):

이 두 단위 벡터 사이의 각도를 계산하기 위해 우리는 코사인 함수의 역 cos⁻¹을 사용하고 이것은 143.1도가 됩니다. 이제 우리는 효과적으로 이 두 벡터 사이의 각도를 계산했습니다. 내적은 나중에 조명 계산을 할 때 매우 유용합니다.

외적(Cross product)

외적은 3D 공간에서만 정의되며 입력으로 두 개의 비평행 벡터를 취하고 입력 벡터 모두에 직교하는 세 번째 벡터를 생성합니다. 두 입력 벡터도 서로 직교한다면, 외적은 3개의 직교 벡터를 결과로 내놓을 것입니다. 이것은 앞으로의 장에서 유용할 것입니다. 다음 이미지는 3D 공간에서 이것이 어떻게 보이는지 보여줍니다:

다른 연산과 달리, 외적은 선형 대수학을 파고들지 않고는 정말로 직관적이지 않으므로 공식을 그냥 외워두는 것이 좋습니다(또는 외우지 않아도 괜찮을 것입니다). 아래에서 두 직교 벡터 A와 B 사이의 외적을 볼 수 있습니다: 

보시다시피, 그다지 이치에 맞지 않는 것처럼 보입니다. 그러나 이러한 단계를 따르기만 하면 입력 벡터에 직교하는 또 다른 벡터를 얻을 수 있습니다.

행렬(Matrices)

이제 벡터에 대해 거의 모든 것을 논의했으니 행렬 세계로 들어가 봅시다! 행렬은 숫자, 기호 및/또는 수학적 표현의 직사각형 배열입니다. 행렬의 각 개별 항목은 행렬의 요소라고 불립니다. 2x3 행렬의 예는 다음과 같습니다: 

행렬은 (i,j)로 인덱싱되는데, 여기서 i는 행이고 j는 열입니다. 그래서 위의 행렬은 2x3 행렬이라고 불립니다(2개 행, 3개 열). 이것은 (x,y)로 2D 그래프를 인덱싱할 때 익숙한 것과 반대입니다. 값 4를 검색하려면 (2,1)(두 번째 행, 첫 번째 열)로 인덱싱합니다.

 

행렬은 기본적으로 그 이상의 것이 아니라 단지 수학적 표현의 직사각형 배열입니다. 그러나 행렬은 좋은 수학적 속성들의 집합을 가지고 있으며, 벡터와 마찬가지로 행렬에 대해 덧셈, 뺄셈 및 곱셈과 같은 여러 연산을 정의할 수 있습니다.

덧셈과 뺄셈(Addition and subtraction)

두 행렬 간의 행렬 덧셈 및 뺄셈은 요소별로 수행됩니다. 따라서 우리가 일반 숫자에 익숙한 것과 동일한 일반 규칙이 적용되지만, 두 행렬의 동일한 인덱스를 가진 요소에 대해 수행됩니다. 이는 덧셈 및 뺄셈이 동일한 차원의 행렬에 대해서만 정의된다는 것을 의미합니다. 3x2 행렬과 2x3 행렬(또는 3x3 행렬과 4x4 행렬)은 서로 더하거나 뺄 수 없습니다. 행렬 덧셈이 두 개의 2x2 행렬에서 어떻게 작동하는지 봅시다: 

동일한 규칙이 행렬 뺄셈에도 적용됩니다: 

행렬-스칼라 곱(Matrix-scalar products)

행렬-스칼라 곱은 행렬의 각 요소에 스칼라를 곱합니다. 다음 예는 곱셈을 보여줍니다: 

이제 왜 그 단일 숫자들이 스칼라라고 불리는지도 이해가 됩니다. 스칼라는 기본적으로 행렬의 모든 요소를 그 값으로 스케일링합니다. 이전 예에서는 모든 요소가 2로 스케일링되었습니다.

행렬-행렬 곱셈

행렬끼리 곱하는것은 어렵지않지만, 익숙해지기 어렵습니다. 행렬 곱셈은 기본적으로 곱셈 시 미리 정의된 규칙 세트를 따르는 것을 의미합니다. 하지만 몇 가지 제한이 있습니다:

  1. 왼쪽 행렬의 열 수가 오른쪽 행렬의 행 수와 같을 때만 두 행렬을 곱할 수 있습니다.
  2. 행렬 곱셈은 교환 법칙이 성립하지 않습니다. 즉, A⋅B≠B⋅A 입니다.

2개의 2x2 행렬의 행렬 곱셈 예제부터 시작해 봅시다: 

행렬 곱셈은 왼쪽 행렬의 행과 오른쪽 행렬의 열을 사용하는 일반 곱셈과 덧셈의 조합입니다.
다음 이미지를 봅시다:

우리는 먼저 왼쪽 행렬의 상단 행을 취한 다음 오른쪽 행렬에서 열을 가져옵니다. 우리가 선택한 행과 열은 결과로 나오는 2x2 행렬의 어떤 출력 값을 계산할 것인지 결정합니다. 왼쪽 행렬의 첫 번째 행을 취하면 결과 값은 결과 행렬의 첫 번째 행에 들어가고, 열을 선택하면 첫 번째 열인 경우 결과 값은 결과 행렬의 첫 번째 열에 들어갑니다. 이것은 정확히 빨간색 경로의 경우입니다. 오른쪽 하단 결과를 계산하기 위해 첫 번째 행렬의 하단 행과 두 번째 행렬의 가장 오른쪽 열을 취합니다.

결과 값을 계산하기 위해 행과 열의 첫 번째 요소를 일반 곱셈을 사용하여 곱합니다. 두 번째 요소, 세 번째, 네 번째 등에 대해서도 동일하게 수행합니다. 개별 곱셈의 결과는 합산되고 결과가 나옵니다. 이제 왼쪽 행렬의 열과 오른쪽 행렬의 행의 크기가 같아야 한다는 요구 사항 중 하나가 이해됩니다. 그렇지 않으면 연산을 완료할 수 없습니다!

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

 

머릿속에서 곱셈을 상상하는 데 어려움이 있어도 걱정하지 마세요. 손으로 계산을 계속 시도하고 어려움이 있을 때마다 이 페이지로 돌아오세요. 시간이 지남에 따라 행렬 곱셈은 자연스러워질 것입니다.

 

색상을 사용하여 패턴을 시각화해 봅시다. 유용한 연습으로, 곱셈의 자신만의 답을 생각해 보고 결과 행렬과 비교해 보세요(손으로 행렬 곱셈을 시도하면 빠르게 이해하게 될 것입니다).

보시다시피, 행렬-행렬 곱셈은 꽤 번거로운 과정이며 오류가 발생하기 쉽습니다(그래서 우리는 일반적으로 컴퓨터에게 이 작업을 맡깁니다). 행렬이 커질수록 문제는 빠르게 커집니다. 더 많은 것에 목말라 있고 행렬의 다른 수학적 특성들에 대해 궁금하다면 행렬에 관한 Khan Academy 동영상을 강력히 추천합니다.

행렬-벡터 곱셈(Matrix-Vector multiplication)

지금까지 우리는 벡터를 충분히 다뤘습니다. 우리는 위치, 색상, 심지어 텍스처 좌표를 나타내기 위해 벡터를 사용했습니다. 토끼 굴을 조금 더 깊이 들어가서 벡터가 기본적으로 Nx1 행렬이라고 말해 봅시다. 여기서 N은 벡터의 구성 요소 수입니다(N차원 벡터(N-dimensional vector)라고도 함). 생각해 보면 매우 이치에 맞습니다. 벡터는 행렬처럼 숫자의 배열이지만, 열이 하나뿐입니다. 그렇다면 이 새로운 정보가 우리에게 어떻게 도움이 될까요? MxN 행렬이 있으면 행렬의 열 수가 벡터의 행 수와 같기 때문에 이 행렬을 Nx1 벡터와 곱할 행렬의 열 수가 벡터의 행 수와 같기 때문에 이 행렬을 Nx1 벡터와 곱할 수 있습니다. 따라서 행렬 곱셈이 정의됩니다.

 

그런데 왜 행렬을 벡터와 곱할 수 있다는 것이 중요할까요? 우연히도 행렬 안에 많은 흥미로운 2D/3D 변환을 넣을 수 있고, 그 행렬을 벡터와 곱하면 그 벡터를 변환시킵니다. 아직 혼란스럽다면, 몇 가지 예제부터 시작해 보면 우리가 무엇을 의미하는지 빨리 이해할 수 있을 것입니다.

항등 행렬(Identity matrix)

OpenGL에서는 대개 여러 이유로 4x4 변환 행렬을 사용하는데, 그 중 하나는 대부분의 벡터가 크기 4라는 것입니다. 우리가 생각할 수 있는 가장 간단한 변환 행렬은 항등 행렬입니다. 항등 행렬은 대각선을 제외하고 모두 0인 NxN 행렬입니다. 보시다시피, 이 변환 행렬은 벡터를 완전히 손상되지 않은 상태로 둡니다:

벡터는 완전히 변하지 않았습니다. 이는 곱셈 규칙에서 분명합니다: 첫 번째 결과 요소는 행렬의 첫 번째 행의 각 요소와 벡터의 각 요소를 곱한 것입니다. 행의 첫 번째 요소를 제외한 모든 요소가 0이므로, 1⋅1+0⋅2+0⋅3+0⋅4=1이 되고 벡터의 다른 3개 요소에도 동일하게 적용됩니다.

변환하지 않는 변환 행렬의 용도가 무엇인지 궁금할 수 있습니다. 항등 행렬은 일반적으로 다른 변환 행렬을 생성하는 출발점이며, 선형 대수학을 더 깊이 들어가면 정리를 증명하고 선형 방정식을 해결하는 데 매우 유용한 행렬입니다.

 

스케일링(Scaling)

벡터를 스케일링할 때, 우리는 방향을 동일하게 유지하면서 화살표의 길이를 스케일링하고자 하는 양만큼 증가시킵니다. 2D 또는 3D에서 작업하므로 각 축(x, y 또는 z)을 스케일링하는 2개 또는 3개의 스케일링 변수로 구성된 벡터로 스케일링을 정의할 수 있습니다.

 

벡터 v̄=(3,2)를 스케일링해 봅시다. x축을 따라 0.5배로 스케일링하여 두 배 좁게 만들고, y축을 따라 2배로 스케일링하여 두 배 높게 만들 것입니다. 벡터를 s̄=(0.5,2)로 스케일링하면 어떻게 보이는지 봅시다:

OpenGL은 일반적으로 3D 공간에서 작동한다는 것을 명심하세요. 따라서 이 2D 경우에서는 z축 스케일을 1로 설정하여 변경하지 않을 수 있습니다. 우리가 방금 수행한 스케일링 작업은 비균일 스케일(non-uniform scale)입니다. 각 축에 대한 스케일링 요소가 동일하지 않기 때문입니다. 스칼라가 모든 축에 동일하다면 균일 스케일(uniform scale)이라고 합니다.

 

스케일링을 수행하는 변환 행렬을 만들어 봅시다. 항등 행렬에서 대각선의 각 요소가 해당 벡터 요소와 곱해진다는 것을 보았습니다. 항등 행렬의 1을 3으로 바꾸면 어떻게 될까요? 그 경우, 벡터의 각 요소를 3의 값으로 곱하게 되므로 벡터를 균일하게 3배로 스케일링하게 됩니다. 스케일링 변수를 (S1,S2,S3)로 표현하면 임의의 벡터 (x,y,z)에 대한 스케일링 행렬을 다음과 같이 정의할 수 있습니다:

4번째 스케일링 값은 1로 유지합니다. w 구성 요소는 나중에 볼 다른 목적으로 사용됩니다.

이동(Translation)

이동(Translation)은 원래 벡터 위에 다른 벡터를 추가하여 다른 위치를 가진 새 벡터를 반환하는 과정으로, 이동 벡터를 기반으로 벡터를 이동시킵니다. 우리는 이미 벡터 덧셈에 대해 논의했으므로 이것은 그다지 새롭지 않을 것입니다.

 

스케일링 행렬과 마찬가지로 4x4 행렬에서 특정 연산을 수행하는 데 사용할 수 있는 여러 위치가 있으며, 이동의 경우 4번째 열의 상위 3개 값입니다. 이동 벡터를 (Tx,Ty,Tz)로 표현하면 이동 행렬을 다음과 같이 정의할 수 있습니다:

이것이 작동하는 이유는 모든 이동 값이 벡터의 w 열과 곱해져 벡터의 원래 값에 더해지기 때문입니다(행렬 곱셈 규칙을 기억하세요). 이것은 3x3 행렬로는 불가능했을 것입니다.

동차 좌표(Homogeneous coordinates)

벡터의 w 구성 요소는 동차 좌표라고도 알려져 있습니다. 동차 벡터에서 3D 벡터를 얻으려면 x, y, z 좌표를 w 좌표로 나눕니다. 대개 w 구성 요소가 대부분의 시간 동안 1.0이기 때문에 우리는 이것을 보통 알아채지 못합니다. 동차 좌표를 사용하면 몇 가지 장점이 있습니다: 3D 벡터에 대한 행렬 이동을 수행할 수 있게 해주고(w 구성 요소 없이는 벡터를 이동시킬 수 없음), 다음 장에서는 w 값을 사용하여 3D 원근감을 만들 것입니다.

또한, 동차 좌표가 0과 같을 때마다, 벡터는 특별히 방향 벡터(direction vector)로 알려져 있습니다. w 좌표가 0인 벡터는 이동될 수 없기 때문입니다. 

즉 ㄷ회전과 이동을 같이할수 있기에 3x3이 아닌, 4x4로 표현한 이유입니다.

이동 행렬을 사용하면 3개의 축 방향(x, y, z) 중 어느 방향으로든 객체를 이동할 수 있으므로 우리의 변환 툴킷에 매우 유용한 변환 행렬입니다.

회전(Rotation)

지금까지의 변환은 2D 또는 3D 공간에서 이해하고 시각화하기가 상대적으로 쉬웠지만, 회전은 조금 더 까다롭습니다. 이러한 행렬이 어떻게 구성되는지 정확히 알고 싶다면 Khan Academy의 선형 대수학 동영상 중 회전 항목을 보는 것이 좋습니다.

 

먼저 벡터의 회전(rotaion of a vector)이 실제로 무엇인지 정의해 봅시다. 2D 또는 3D에서의 회전은 각도로 표현됩니다. 각도는 한 원이 360도 또는 2 PI 라디안인 도(degrees) 또는 라디안(radians)일 수 있습니다. 저는 우리가 일반적으로 각도에 더 익숙하기 때문에 도를 사용하여 회전을 설명하는 것을 선호합니다. 대부분의 회전 함수는 라디안 단위의 각도를 필요로 하지만, 다행히도 도는 라디안으로 쉽게 변환됩니다:

도 단위 각도 = 라디안 단위 각도 * (180 / PI)
라디안 단위 각도 = 도 단위 각도 * (PI / 180)
여기서 PI는 (반올림하면) 3.14159265359입니다. 

 

반원을 회전하는 것은 360/2 = 180도를 회전하는 것이고, 오른쪽으로 1/5을 회전하는 것은 오른쪽으로 360/5 = 72도를 회전하는 것을 의미합니다. 이것은 기본 2D 벡터에 대해 설명되어 있으며, v̄가 k̄에서 오른쪽, 즉 시계 방향으로 72도 회전한 것입니다:

3D에서의 회전은 각도와 회전축으로 지정됩니다. 지정된 각도는 지정된 회전축을 따라 객체를 회전시킵니다. 머리를 특정 각도로 회전시키면서 단일 회전축을 계속 바라보는 것을 상상해 보세요. 예를 들어, 3D 세계에서 2D 벡터를 회전시킬 때, 우리는 회전축을 z축으로 설정합니다(이것을 시각화해 보세요).

 

삼각법을 사용하면 각도가 주어진 벡터를 새로운 회전된 벡터로 변환할 수 있습니다. 이것은 일반적으로 사인과 코사인 함수(일반적으로 sin과 cos로 축약됨)의 스마트한 조합을 통해 수행됩니다. 회전 행렬이 어떻게 생성되는지에 대한 논의는 이 장의 범위를 벗어납니다.

 

회전 행렬은 3D 공간의 각 단위 축에 대해 정의되며, 여기서 각도는 세타(θ) 기호로 표현됩니다.
다음은 회전 변환공식 또는 오일러의 공식입니다.

X축을 중심으로 한 회전:

Y축을 중심으로 한 회전:

Z축을 중심으로 한 회전:

회전 행렬을 사용하여 위치 벡터를 세 개의 단위 축 중 하나를 중심으로 변환할 수 있습니다. 임의의 3D 축을 중심으로 회전하려면 예를 들어 먼저 X축을 중심으로 회전한 다음 Y축, 그리고 Z축으로 세 개를 모두 결합할 수 있습니다. 그러나 이로 인해 짐벌 락(Gimbal lock)이라는 문제가 빠르게 발생합니다. 세부 사항은 논의하지 않겠지만, 더 나은 해결책은 회전 행렬을 결합하는 대신 처음부터 (0.662,0.2,0.722)와 같은 임의의 단위 축(이것이 단위 벡터임에 유의하세요)을 중심으로 회전하는 것입니다. 그런 (상세한) 행렬이 존재하며 임의의 회전 축이 (Rx,Ry,Rz)인 경우 아래와 같이 제공됩니다:

이러한 행렬을 생성하는 방법에 대한 수학적 논의는 이 장의 범위를 벗어납니다. 이 행렬조차도 짐벌 락을 완전히 방지하지는 않는다는 점을 명심하세요(비록 훨씬 어려워지기는 합니다). 짐벌 락을 진정으로 방지하려면 쿼터니온(quaternions)을 사용하여 회전을 표현해야 합니다. 쿼터니온은 더 안전할 뿐만 아니라 계산적으로도 더 친화적입니다. 그러나 쿼터니온에 대한 논의는 이 장의 범위를 벗어납니다.

행렬 결합하기(Combining matrics)

변환에 행렬을 사용하는 진정한 힘은 행렬-행렬 곱셈 덕분에 여러 변환을 단일 행렬로 결합할 수 있다는 것입니다. 여러 변환을 결합하는 변환 행렬을 생성할 수 있는지 살펴봅시다. 벡터 (x,y,z)가 있고 이를 2배로 스케일링한 다음 (1,2,3)만큼 이동시키고 싶다고 가정해 봅시다. 필요한 단계에 대한 이동 행렬과 스케일링 행렬이 필요합니다. 결과 변환 행렬은 다음과 같이 보일 것입니다:

행렬을 곱할 때 먼저 이동을 수행한 다음 스케일 변환을 수행한다는 점에 유의하세요. 행렬 곱셈은 교환 법칙이 성립하지 않으므로 순서가 중요합니다. 행렬을 곱할 때 가장 오른쪽 행렬이 먼저 벡터와 곱해지므로 곱셈을 오른쪽에서 왼쪽으로 읽어야 합니다. 행렬을 결합할 때는 먼저 스케일링 연산을 수행한 다음 회전을 하고 마지막으로 이동을 수행하는 것이 좋습니다. 그렇지 않으면 서로 (부정적으로) 영향을 미칠 수 있습니다. 예를 들어, 먼저 이동을 수행한 다음 스케일링을 수행하면 이동 벡터도 스케일링됩니다!

 

벡터에 최종 변환 행렬을 적용하면 다음과 같은 벡터가 나옵니다:

좋습니다! 벡터는 먼저 2배로 스케일링된 다음 (1,2,3)만큼 이동되었습니다.

실제로 적용하기(In practice)

이제 변환에 대한 모든 이론을 설명했으니, 실제로 이 지식을 어떻게 활용할 수 있는지 살펴볼 시간입니다. OpenGL에는 행렬이나 벡터 지식이 내장되어 있지 않으므로 자체 수학 클래스와 함수를 정의해야 합니다. 이 책에서는 모든 작은 수학적 세부 사항을 추상화하고 단순히 미리 만들어진 수학 라이브러리를 사용하려고 합니다. 다행히도, GLM이라는 사용하기 쉽고 OpenGL에 맞춰진 수학 라이브러리가 있습니다.

GLM

OpenGL Mathematics


GLM은 OpenGL Mathematics의 약자로, 헤더 전용 라이브러리입니다. 즉, 적절한 헤더 파일만 포함하면 됩니다. 링크 및 컴파일이 필요 없습니다. GLM은 그들의 웹사이트에서 다운로드할 수 있습니다. 헤더 파일의 루트 디렉토리를 includes 폴더에 복사하고 시작해 봅시다.

 

우리가 필요로 하는 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 = glm::mat4(1.0f);
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라는 벡터를 정의합니다. 다음으로 mat4를 정의하고 행렬의 대각선을 1.0으로 초기화하여 명시적으로 항등 행렬로 초기화합니다. 항등 행렬로 초기화하지 않으면 행렬은 널 행렬(모든 요소가 0)이 되고 이후의 모든 행렬 연산도 null 행렬이 됩니다.

 

다음 단계는 우리의 항등 행렬을 이동 벡터와 함께 glm::translate 함수에 전달하여 변환 행렬을 만드는 것입니다(주어진 행렬은 이동 행렬과 곱해지고 결과 행렬이 반환됩니다). 그런 다음 벡터에 변환 행렬을 곱하고 결과를 출력합니다. 행렬 이동이 어떻게 작동하는지 기억한다면 결과 벡터는 (1+1,0+1,0+0), 즉 (2,1,0)이어야 합니다. 이 코드 스니펫은 210을 출력하므로 이동 행렬이 제대로 작동했습니다.

 

더 흥미로운 것을 해보고 이전 장의 컨테이너 객체를 스케일링하고 회전시켜 봅시다:

glm::mat4 trans = glm::mat4(1.0f);
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축을 중심으로 회전하려고 합니다. 회전하는 축은 단위 벡터여야 한다는 점을 명심하세요. 따라서 X, Y 또는 Z축이 아닌 축을 중심으로 회전하는 경우에는 먼저 벡터를 정규화해야 합니다. 행렬을 GLM의 각 함수에 전달하기 때문에 GLM은 자동으로 행렬을 함께 곱하여 모든 변환을 결합하는 변환 행렬을 생성합니다.

 

다음 질문은: 변환 행렬을 셰이더에 어떻게 전달할까요? 이전에 GLSL에도 mat4 유형이 있다고 짧게 언급했습니다. 따라서 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에는 또한 벡터처럼 스위즐링(swizzling) 같은 연산을 허용하는 mat2 및 mat3 유형이 있습니다. 앞서 언급한 모든 수학 연산(스칼라-행렬 곱셈, 행렬-벡터 곱셈 및 행렬-행렬 곱셈과 같은)이 행렬 유형에서 허용됩니다. 특별한 행렬 연산이 사용되는 곳마다 무슨 일이 일어나고 있는지 설명할 것입니다. 


uniform을 추가하고 gl_Position에 전달하기 전에 위치 벡터에 변환 행렬을 곱했습니다. 이제 우리의 컨테이너는 두 배 작아지고 90도 회전(왼쪽으로 기울어짐)해야 합니다. 그러나 여전히 변환 행렬을 셰이더로 전달해야 합니다:

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

먼저 uniform 변수의 위치를 쿼리한 다음 glUniform과 Matrix4fv를 접미사로 사용하여 행렬 데이터를 셰이더에 보냅니다. 

첫 번째 인수는 이제 익숙해졌을 uniform의 위치입니다. 두 번째 인수는 OpenGL에게 보내려는 행렬의 수를 알려주는데, 이는 1입니다. 세 번째 인수는 행렬을 전치할 것인지 묻는 것으로, 즉 열과 행을 바꾸는 것입니다. OpenGL 개발자들은 종종 열 우선 순서(column-major ordering)라고 불리는 내부 행렬 레이아웃을 사용하는데, 이는 GLM의 기본 행렬 레이아웃이므로 행렬을 전치할 필요가 없습니다. 따라서 GL_FALSE로 유지할 수 있습니다. 마지막 매개변수는 실제 행렬 데이터이지만, GLM은 행렬 데이터를 OpenGL의 기대와 항상 일치하지 않는 방식으로 저장하므로 GLM의 내장 함수인 value_ptr을 사용하여 데이터를 먼저 변환합니다.

 

우리는 변환 행렬을 만들었고, 버텍스 셰이더에서 uniform을 선언했으며, 행렬을 셰이더로 보내서 버텍스 좌표를 변환했습니다. 결과는 다음과 같아야 합니다:

완벽합니다! 우리의 컨테이너가 실제로 왼쪽으로 기울어지고 두 배 작아져서 변환이 성공적이었습니다. 조금 더 재미있게 해보고 시간이 지남에 따라 컨테이너를 회전시킬 수 있는지 확인해 봅시다. 또한 재미삼아 컨테이너를 창의 오른쪽 하단에 재배치할 것입니다. 시간이 지남에 따라 컨테이너를 회전시키려면 매 프레임마다 업데이트해야 하므로 렌더링 루프에서 변환 행렬을 업데이트해야 합니다. 시간에 따른 각도를 얻기 위해 GLFW의 time 함수를 사용합니다:

glm::mat4 trans = glm::mat4(1.0f);
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)을 중심으로 회전시킨 다음, 회전이 완료되면 회전된 버전을 화면의 오른쪽 하단 모서리로 이동시킵니다. 실제 변환 순서는 역순으로 읽어야 한다는 것을 기억하세요. 코드에서 먼저 이동한 다음 나중에 회전하더라도 실제 변환은 먼저 회전을 적용한 다음 이동을 적용합니다. 이러한 모든 변환 조합과 그것들이 객체에 어떻게 적용되는지 이해하는 것은 어렵습니다. 이러한 변환을 시도하고 실험해 보면 빠르게 이해할 수 있을 것입니다.

 

제대로 했다면 다음과 같은 결과를 얻을 수 있습니다:

그리고 이것이 바로 그것입니다. 시간이 지남에 따라 회전하는 이동된 컨테이너로, 모두 단일 변환 행렬로 수행됩니다! 이제 행렬이 그래픽 영역에서 왜 그렇게 강력한 구성인지 알 수 있습니다. 무한한 양의 변환을 정의하고 그것들을 모두 원하는 만큼 재사용할 수 있는 단일 행렬로 결합할 수 있습니다. 이런 식으로 버텍스 셰이더에서 변환을 사용하면 버텍스 데이터를 다시 정의하는 수고를 덜 수 있으며, 데이터를 계속 다시 보낼 필요가 없으므로(이는 상당히 느림) 처리 시간도 절약할 수 있습니다. 우리가 해야 할 일은 변환 uniform을 업데이트하는 것뿐입니다.

 

올바른 결과를 얻지 못했거나 다른 곳에서 막혔다면, 소스 코드와 업데이트된 셰이더 클래스를 확인해 보세요.

 

다음 장에서는 행렬을 사용하여 버텍스에 대한 다른 좌표 공간을 정의하는 방법에 대해 논의할 것입니다.해당 내용은 3D 그래픽으로 가는 첫 번째 단계가 될 것입니다!

추가 자료

  • 선형 대수학의 본질: Grant Sanderson의 훌륭한 비디오 튜토리얼 시리즈로, 변환과 선형 대수학의 기본 수학에 대해 다룹니다.
  • 행렬 곱셈 XYZ: 행렬 곱셈을 보여주는 인터랙티브 시각 도구입니다.행렬을 연습할 수 있습니다.

연습 문제

  1. 컨테이너에 대한 마지막 변환을 사용하여 순서를 바꿔서 먼저 회전한 다음 이동해 보세요. 어떤 일이 일어나는지 보고 왜 이런 일이 발생하는지 추론해 보세요: 해답.
  2. glDrawElements에 대한 다른 호출로 두 번째 컨테이너를 그려보되, 변환만 사용하여 다른 위치에 배치해 보세요. 이 두 번째 컨테이너가 창의 왼쪽 상단에 배치되었는지 확인하고, 회전하는 대신 시간이 지남에 따라 스케일링하세요(sin 함수는 여기서 유용합니다. 음수 스케일이 적용되는 즉시 객체가 반전된다는 점에 유의하세요): 해답.

출처: https://learnopengl.com/Getting-started/Transformations

생각하기

오늘은 물체를 변환(Transformation)하는 방법에 대해 알아봤는데요.

변환하기위해 필요한 이론을 몇가지 배웠습니다.

크기, 방향을 나타내는 벡터(Vector)를 배웠고 벡터는 단일 열을 갖고,

2D, 3D 공간을 나타내기위해 행렬을 사용합니다.

 

행렬(Matrix)은 행과 열을 갖는 집합체입니다.

예를들어, 3D 공간벡터는 (x, y, z)성분을 가진 3x1행렬로 표현할 수 있겠죠.


또한 자연과학분야에서는 도(degree)보다는 라디안(radian)을 많이 쓴다고 합니다.
도 단위 각도 = 라디안 단위 각도 * (180 / PI)
라디안 단위 각도 = 도 단위 각도 * (PI / 180)

360도는 2π 라디안 이여서 위와같이 표현됩니다.

3월인데 눈이 많이 오네요.

개인적으로 어제 새차했는데 눈이와서 슬픕니다..
이런일이 생기다니 얼마나 큰 기쁨이 올지 기대되네요.

 

피드백 언제나 환영하며, 좋은하루 보내세요.

감사합니다 :)