눈팅하는 게임개발자 블로그

OpenGL Getting Started 2-7 Translation 본문

공부한거/OpenGL

OpenGL Getting Started 2-7 Translation

Palamore 2020. 9. 27. 20:13

원본 사이트

learnopengl.com/Translations

 

LearnOpenGL - Translations

 

learnopengl.com

번역 사이트

heinleinsgame.tistory.com/10?category=757483

 

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

변환(Transformations) 시작하기/변환(Transformations) 우리는 이제 오브젝트를 생성하고 컬러를 입히고 텍스처를 이용하여 세밀하게 표현하는 방법을 알고 있지만 정적인 오브젝트이기 때문에 여전히 �

heinleinsgame.tistory.com

 

Transformations

오브젝트를 생성, 색과 텍스처를 입힌 후에는 이제 오브젝트를 움직여야 할 차례다.

하지만 transformation을 다루기에 앞서 기본 수학적 지식인 행렬(matrix), 벡터와 같은 개념을 우선 이해해야 한다.

 

벡터

벡터는 기본적으로 방향과 크기를 가진다.

'벡터'라는 객체를 정의하는 클래스를 만들어보자면 다음과 같다.

class Vector{
	direction mDir; // 방향
	magnitude mMag; // 크기
};

direction은 왼쪽, 오른쪽, 아래, 위, 왼쪽 위 등의 방향을 의미.

magnitude는 실수로 나타낼 수 있는 크기를 의미한다.

벡터가 존재하는 위치는 다를 수 있겠지만, 방향과 크기가 같다면 위치가 다르더라도 기본적으로 같은 벡터가 된다.

 

벡터 연산

스칼라 연산

스칼라는 하나의 숫자로서 벡터에 4칙연산을 적용할 수 있다.

x(스칼라)를 벡터에 더한다면

이와 같이 표현할 수 있다. 위의 수식에서 '+'는 '-', '*', '%', '/'로 대체될 수 있다.

 

역벡터

역벡터는 반대 방향을 나타내는 벡터이다.

단순히 -1 스칼라 곱셈을 하는 것과도 같다.

 

덧셈과 뺄셈

두 벡터끼리의 덧셈과 뺄셈은 같은 축끼리의 연산으로 정의된다.

덧셈의 경우 단순히 두 벡터의 요소들을 더해준다.

뺄셈의 경우는 빼주는 벡터의 역벡터를 더한 것과도 같다.

 

벡터의 크기

벡터의 길이/크기를 구하기 위해서는 각 요소들을 제곱하여 모두 더해준 뒤 제곱근을 구한다.

2D 벡터의 요소가 각각 x, y이고 벡터의 길이가 v라고 하면 다음과 같은 계산이 가능하다.

연산해서 나온 해당 벡터의 길이로 단위 벡터라는 특별한 벡터를 구할 수도 있다.

벡터의 각 요소들을 벡터의 길이로 나눈다면 길이가 1인 벡터를 얻을 수 있는데.

해당 벡터를 단위 벡터(unit vector)라고 하며 위의 과정을 정규화(normalization)라고 부른다.

 

벡터의 곱셈

두 벡터의 곱셈은 다소 낯선 개념일 수 있다.

두 벡터의 곱셈을 할 때 선택할 수 있는 2개의 경우가 있는데. 하나는 내적, 다른 하나는 외적이다.

 

내적

두 벡터(v, k)의 내적은 아래와 같이 계산된다.

두 벡터의 길이에 두 벡터 사이의 각의 코사인 값을 곱한 것과 같다.

여기서 v와 k가 모두 단위벡터라면 코사인 값만 남게 된다.

이와 같은 식으로 벡터 v와 k가 서로 직각인지 아닌지 또는 평행인지 확인할 수 있다.

평행일 경우 코사인 값은 1, 직각일 경우 코사인의 값은 0이 된다.

단위 벡터의 내적의 값을 계산해서 나온 값이 1이라면 두 벡터는 평행.

값이 0이라면 두 벡터는 직각임을 알 수 있다.

내적의 값은 단순히 요소들끼리 서로 곱한 값을 더한 것으로도 구할 수 있다.

해당 식은 단위요소들 끼리의 내적 연산이다.

내적의 계산 결과인 -0.8을 코사인의 역함수를 이용해 계산하면 143.1도라는 계산이 나온다.

이를 이용하여 두 벡터 사이의 각도를 효과적으로 계산할 수 있다.

 

외적

외적은 오직 3D 공간에서만 정의되고 평행하지 않은 두 개의 벡터를 입력으로 받는다.

또한 두 벡터에 직교하는 하나의 벡터를 생성한다.

입력된 두 벡터가 서로 직교한다면 외적의 결과는 3개의 직교 벡터를 생성한다.

다른 연산들과 달리 외적은 선형대수를 탐구하지 않는다면 직관적이지 않으므로 공식을 외워 두는 것이 좋다.

각 요소들의 결과 값은 x축이 아닌 요소들끼리 x자의 모양으로 곱셈 후 뺄셈한 값이 된다.

 

행렬

행렬은 단순히 숫자들을 사각적으로 배열한 것이다.

단순히 n * m의 2차원 배열과 크게 다르지 않다.

행렬의 각 숫자들은 요소라고 불리며

위의 행렬은 2 * 3행렬으로 2차원 배열과 같이 (i, j)로 인덱싱 될 수 있다.

또한 벡터처럼 여러 연산자들을 정의할 수 있다(덧셈, 뺄셈, 곱셈)

 

행렬 연산

행렬 덧셈과 뺄셈

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

기본적으로 행렬의 각 요소들에 더해진다. 뺄셈도 이와 같다.

 

행렬과 행렬 사이의 덧셈과 뺄셈도 단순하다.

각 요소들끼리 더하거나 빼면 된다.

 

행렬 곱셈

행렬과 스칼라 곱셈은 단순히 각 요소들을 스칼라로 곱한다.

행렬과 행렬의 곱셈에서는 곱셈의 왼쪽 행렬의 행,

오른쪽 행렬의 열들을 곱셈하여 더해주는 것으로 수행된다.

다소 복잡하지만 크게 어렵지는 않다.

결과로 나오는 행렬은

(왼쪽 행렬의 행의 갯수, 오른쪽 행렬의 열의 갯수)와 같다.

크게 어렵지는 않아도 해당 작업이 번거로운 작업이며 실수하기 쉬운 부분이므로 보통 컴퓨터를 이용해 수행한다.

 

행렬과 벡터 곱셈

기본적으로 벡터는 N * 1의 행렬이다.

따라서 행렬과 벡터의 곱셈 또한 가능하다. 이는 이용해 먹을 수 있다.

대부분의 3D / 2D 변환들은 행렬 값으로 표현되고 해당 행렬 값들을 벡터에 곱하면

벡터의 transform을 수행할 수 있다.

 

단위 행렬

OpenGL에서는 대부분의 벡터의 크기가 4이기 때문에 일반적으로 4x4변환 행렬을 가지고 작업한다.

가장 간단한 변환 행렬은 단위 행렬이다. 이는

N x N행렬으로 왼쪽 위에서 오른쪽 아래로의 대각선을 제외하고는 모든 요소가 0인 행렬을 이야기한다.

위의 단위 행렬은 벡터에 아무런 영향을 끼치지 않는다.

아무런 의미를 가지지 못하는 단위 행렬 같지만 단위 행렬은 일반적으로 다른 변환 행렬을 생성하는 데에 있어서

시작점이 될 수 있으며 선형 대수학에 있어서 매우 유용한 행렬이다.

 

스케일링(확대, 축소)

벡터를 스케일링(확대 또는 축소)할 경우 화살표의 방향은 그대로 둔 채 원하는 만큼 길이를 증가시키거나

감소시킬 수 있다.

아래의 예는 (3, 2)의 벡터를 x축으로 0.5만큼 ,y축으로 2.0만큼 스케일한다.

이 스케일링은 행렬로서 계산될 수 있는데.

위의 벡터의 경우 (3, 2)벡터에 첫 번째 행에 0.5를 두 번째 행에 2.0을 요소로 가지는

단위 행렬을 곱한 것과도 같은 결과이다.

 

이와 같이 스케일링을 행렬로서 계산이 가능하다.

x, y, z, 1의 값을 가진 벡터를 x축을 S1, y축을 S2, z축을 S3만큼 스케일링 한다면

위와 같은 행렬로서 계산이 가능하다.

 

Translation(이동)

Translation은 원본 벡터 위에 다른 벡터를 더하여 다른 위치의 새로운 벡터를 얻을 수 있다.

벡터의 덧셈으로 이를 수행할 수 있지만

스케일 행렬과 마찬가지로 n * n행렬위에 특정한 연산으로 Translation을 수행할 수도 있다.

이동 값들 모두 단위행렬의 마지막 요소 값인 1에 곱해져 벡터의 원본 값들에 더해지기 때문에

Translation이 동작할 수 있게 된다. (n차원에서 이를 이용하기 위해서는 n + 1개의 축을 이용해야 한다.)

 

Rotation(회전)

다른 변환들에 비해 회전은 비교적 시각화하기 까다롭다.

먼저 벡터의 회전은 각(Angle)로 나타낼 수 있다. 각은 degree 또는 radian으로 나타낼 수 있으며

원은 360 degree로, 2 * PI * radian으로 표현할 수 있다.

또한 서로 변환될 수도 있다.

각도에 의한 각 = 라디안 * (180.0f / PI)

라디안에 의한 각 = 각도 * (PI / 180.0f)

 

3D 공간에서의 회전은 회전 축을 사용하는데, 지정된 각은 주어진 축에 대해 회전이 이루어진다.

예를 들면 3D 공간에서 2D 벡터를 회전시킬 때 Z축을 회전축으로 설정하는 것이 있다.

 

삼각법을 사용하면 주어진 각에 대해 벡터를 회전하여 새로운 벡터로 변환하는 것이 가능하다.

일반적으로 sine과 cosine 함수의 조합으로 수행되는데,

각 축에 대해서의 회전은 다음과 같다.

X축

Y축

Z축

 

위의 회전 행렬을 사용하면 위치 벡터를 세 가지의 축 중 하나에 대해 변환시킬 수 있다.

또한 X축에 대한 회전 후 Y축에 대한 회전으로 위의 회전행렬들을 조합해서 사용하는 것도 가능하지만

이는 Gimbal lock이라는 문제가 생길 수 있다. (사원수, Quaternion을 사용하면 방지할 수 있다.)

따라서 회전 행렬을 조합하는 대신 임의의 단위축을 중심으로 즉시 회전하는 것이 권장된다.

 

행렬 조합

위의 여러 변환들을 위해 행렬을 사용하는 것의 진정한 힘은

여러 변환들을 하나의 행렬에 조합할 수 있다는 것이다.

예를 들어 모든 축을 2만큼 스케일 한 후 (1, 2, 3)만큼 Translation한다고 가정하면.

이를 수행하기 위한 두 개의 행렬을 곱함으로써 위의 결과를 얻을 수 있다.

결과 변환 행렬은 다음과 같다.

행렬을 곱할 때는 이동 행렬과 스케일의 순서가 바뀌어야 함을 유의해야 한다.

행렬 곱은 교환법칙이 성립하지 않기 때문에 순서가 중요하다.

행렬을 곱할 때 가장 오른쪽에 있는 행렬이 벡터와 처음으로 곱해지기 때문에

행렬 곱셈은 오른쪽에서 왼쪽으로 읽어나가는 것이 맞다는 것을 기억해야 한다.

 

이제 이 지식들을 실제로 어떻게 활용할 수 있는 지에 대해 알아보자.

OpenGL과 호환되는 수학 라이브러리 GLM을 사용한다.

 

GLM

OpenGL Mathematics. 헤더 파일만 있는 라이브러리.

현재 필요한 대부분의 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 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의 벡터 클래스를 사용하여 vec4 벡터를 선언하고 4x4단위 행렬인 mat4를 선언한다.

다음 glm::translate 함수에 단위 행렬(선언 해놓은 trans)을 집어 넣어 변환 행렬을 생성한다.

또한 변환 벡터(1, 1, 0)도 집어 넣는다.

이후 처음 선언한 벡터와 곱한 후 결과를 출력하면 210이 출력된다.

(1 + 1, 0 + 1, 0 + 0)의 이동이 이루어진 것이다.

 

이제는 이전에 했던 오브젝트를 다뤄보자.

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축을 중심으로 돌려준다.

중심을 돌리기 위한 축은 반드시 단위벡터 여야한다.

 

shader에 해당 변환 행렬을 전해주기 위해 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);
} 

해당 변환행렬을 gl_Position에 곱해줌으로써 적용해주고.

 

이제 변환 행렬을 shader에 넘겨줘야 한다.

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

먼저 uniform 변수의 location을 확인한 후 glUniform 함수를 Matrix4fv 접미사를 사용하여 행렬 데이터를 shader에 보낸다.

첫 번째 파라미터는 uniform의 로케이션을.

두 번째 파라미터는 OpenGL에게 몇 개의 행렬을 넣을 것인지를.

세 번째 파라미터는 행과 열을 바꿀 것인지의 여부. (OpenGL 개발자들은 GLM의 기본 행렬 레이아웃인 내부 행렬 레이아웃을 사용하므로 행과 열을 바꿀 필요가 없다.)

마지막 파라미터는 실제 행렬의 데이터이지만 GLM의 행렬은 정확히 OpenGL이 받기 원하는 형태의 행렬이 아니므로

GLM의 value_ptr 함수를 사용하여 행렬을 반환해주어야 한다.

 

결과는 다음과 같다.

 

이제 이걸 시간에 따라 계속 회전시켜보자.

시간에 따라 회전시키기 위해서는 게임 루프 안에서 변환 행렬을 계속해서 수정해주어야 한다.

그리고 시간에 따른 각을 얻기 위해 GLFW의 시간 함수를 사용한다.

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));

게임 루프 안에 해당 코드를 넣어준다.

 

결과 화면은 다음과 같다.