프로그래밍/컴퓨터그래픽스

OpenGL에서 기하 변환의 처리

studylida 2025. 4. 5. 04:58

🧭 OpenGL에서는 기하 변환이 어떻게 처리될까?

앞에서 우리는 점과 벡터, 어파인 결합, 이동·회전·스케일·밀림 변환, 그리고 합성 변환까지 다양한 기하 변환의 이론을 수학적으로 배웠습니다.
그렇다면 이제 이 개념들이 실제 OpenGL과 같은 그래픽스 시스템에서는 어떻게 구현될까요?


💡 동차 좌표와 행렬 곱으로 처리되는 변환

OpenGL에서는 모든 정점의 좌표를 내부적으로 동차 좌표(homogeneous coordinate) 로 표현합니다.
예를 들어, 2D 점 (x, y)은 (x, y, 1),
3D 점 (x, y, z)은 (x, y, z, 1)로 표현됩니다.

이러한 동차 좌표 덕분에 이동(translation) 과 같은 변환도 행렬 곱 하나로 처리할 수 있습니다.

즉, OpenGL에서는 다음과 같이 각 정점에 변환 행렬을 곱하는 방식으로 기하 변환을 처리합니다:

[p_i' = M_{\text{modelview}} \cdot p_i]

modelview 행렬은 도형의 위치, 방향, 크기 등을 나타내는 어파인 변환 행렬입니다.


🧱 OpenGL의 모델뷰 행렬 구조

OpenGL에서는 모든 변환 정보를 4×4 행렬로 통합해서 표현합니다.
이 4×4 행렬은 우리가 배운 이동, 회전, 스케일, 밀림 등의 모든 변환을 담을 수 있습니다.

예시:

[ M_{\text{modelview}} = \begin{bmatrix} m_0 & m_4 & m_8 & m_{12} \ m_1 & m_5 & m_9 & m_{13} \ m_2 & m_6 & m_{10} & m_{14} \ m_3 & m_7 & m_{11} & m_{15} \ \end{bmatrix} ]

이 행렬은 열 우선(column-major) 방식으로 저장되며, OpenGL 내부적으로는 다음과 같은 배열 형태로 관리됩니다:

float modelview[16];  // column-major로 저장

🔧 변환 행렬 값 얻기 (GL 상태 조회)

OpenGL에서 현재 설정된 모델뷰 행렬 값을 직접 확인하려면 다음처럼 상태를 조회할 수 있습니다:

float m[16];
glGetFloatv(GL_MODELVIEW_MATRIX, m);

이렇게 하면 현재 OpenGL이 내부적으로 사용하고 있는 모델뷰 행렬 값을 m[16] 배열에 담아줍니다.
이 배열은 위의 행렬처럼 column-major 방식으로 구성되어 있어요.


🧱 모델뷰(ModelView) 행렬 직접 조작하기

OpenGL에서는 모든 기하 변환(이동, 회전, 스케일 등)을 내부적으로 모델뷰 행렬에 누적해서 적용합니다.
즉, 우리가 glTranslate, glRotate, glScale 등을 호출하면, OpenGL은 해당 변환 행렬을 만들어 현재 모델뷰 행렬에 곱해주는 것입니다.


① 어떤 행렬을 조작할지 선택하기

glMatrixMode(GL_MODELVIEW);

OpenGL은 여러 행렬 스택을 사용하므로, 먼저 어떤 행렬을 조작할 것인지 지정해줘야 합니다.
우리는 모델뷰 행렬을 조작할 거니까 위와 같이 지정합니다.


② 단위 행렬로 초기화하기

glLoadIdentity();

기존에 쌓인 변환이 있을 수 있으므로, 초기에는 항상 단위 행렬로 초기화해 주는 것이 좋습니다.
단위 행렬은 아무런 변환도 적용되지 않은 상태를 의미합니다:

[ M_{\text{modelview}} = I = \begin{bmatrix} 1 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \end{bmatrix} ]


✨ 모델뷰 행렬에 변환 적용하기

🔹 이동 변환 – glTranslate

glTranslatef(tx, ty, tz);

정점들을 ( (tx, ty, tz) )만큼 이동시키는 변환을 모델뷰 행렬에 오른쪽에서 곱합니다.

즉, 현재 모델뷰 행렬 ( M )에 이동 행렬 ( T )을 곱하면:

[ M \leftarrow M \cdot T ]

이동 행렬은 다음과 같습니다:

[ T = \begin{bmatrix} 1 & 0 & 0 & tx \ 0 & 1 & 0 & ty \ 0 & 0 & 1 & tz \ 0 & 0 & 0 & 1 \end{bmatrix} ]

이 연산은 좌표계가 이동한 것처럼 해석할 수도 있습니다.


🔹 회전 변환 – glRotate

glRotatef(angle, vx, vy, vz);
  • angle: 회전 각도 (도 단위)
  • (vx, vy, vz): 회전 축 (벡터)

회전 행렬도 마찬가지로 현재 모델뷰 행렬의 우측에 곱해집니다.

예를 들어, z축 기준으로 회전할 경우:

[ R = \begin{bmatrix} \cosθ & -\sinθ & 0 & 0 \ \sinθ & \cosθ & 0 & 0 \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \end{bmatrix} ]

즉,

[ M \leftarrow M \cdot R ]


🔹 스케일 변환 – glScale

glScalef(sx, sy, sz);

각 축에 대해 크기를 ( s_x, s_y, s_z )배 만큼 조절합니다.
스케일 행렬:

[ S = \begin{bmatrix} s_x & 0 & 0 & 0 \ 0 & s_y & 0 & 0 \ 0 & 0 & s_z & 0 \ 0 & 0 & 0 & 1 \end{bmatrix} ]

결과적으로,

[ M \leftarrow M \cdot S ]


🎯 누적 곱의 중요성: 적용 순서에 따라 결과가 달라짐!

OpenGL은 모든 변환을 우측에서 곱해 나가기 때문에,
변환을 적용하는 순서가 매우 중요합니다.

예를 들어 다음 두 코드는 완전히 다른 결과를 만듭니다:

glTranslatef(2, 0, 0);  // 먼저 이동
glRotatef(90, 0, 0, 1); // 나중에 회전

vs

glRotatef(90, 0, 0, 1); // 먼저 회전
glTranslatef(2, 0, 0);  // 나중에 이동

→ 행렬 곱의 순서가 반대가 되므로, 최종 결과가 다르게 나타납니다.


✅ 정리

기능 명령어 효과
행렬 설정 glMatrixMode(GL_MODELVIEW) 모델뷰 행렬 선택
초기화 glLoadIdentity() 단위 행렬로 초기화
이동 glTranslatef(tx, ty, tz) 좌표계 또는 객체 이동
회전 glRotatef(angle, vx, vy, vz) 축 기준 회전
스케일 glScalef(sx, sy, sz) 크기 조절 (확대/축소)
주의 변환 순서에 따라 결과가 다름 행렬은 누적 곱 구조

 

🎯 OpenGL에서의 행렬 연산과 예제 코드 완전 정복

OpenGL에서는 3D 그래픽에서 객체를 이동, 회전, 크기 조절하는 데 필요한 모든 변환을 행렬로 처리합니다. 이 행렬을 다루기 위해 다양한 함수가 제공되고, 그중에서도 좌표계 관리를 위한 행렬 스택 개념이 매우 중요합니다.

🧮 행렬 연산 함수 정리

OpenGL에서 변환을 하기 위해 자주 사용하는 네 가지 함수가 있습니다:

함수 역할
glLoadMatrixf(float M[16]) 현재 사용 중인 행렬을 M으로 교체
glMultMatrixf(float M[16]) 현재 행렬에 M오른쪽에서 곱함 (누적 변환)
glPushMatrix() 현재 행렬을 스택에 저장
glPopMatrix() 스택에서 행렬을 꺼내 현재 행렬로 복원

이러한 연산은 지역 좌표계를 독립적으로 관리하거나 계층 구조를 표현할 때 반드시 필요합니다.


🧪 예제 코드 분석 – 기본 객체 그리기

먼저, 우리가 화면에 그릴 도형을 정의하는 DrawObject() 함수를 봅시다.

✏️ DrawObject() 함수 코드

void DrawObject(float r, float g, float b)
{
    glLineWidth(3.0);              // 선 두께 설정 (3픽셀)
    glColor3f(r, g, b);            // 선 색상 설정 (RGB 값으로 지정)

    glBegin(GL_LINES);            // 선(LINE)들을 그리기 시작

    // 선 1: 위에서 아래로 떨어지는 빨간색 선
    glVertex3f(0.0, 1.0, 0.0);    // 시작점 (0, 1)
    glVertex3f(-1.0, 0.0, 0.0);   // 끝점 (-1, 0)

    // 선 2: 가로로 이어지는 선
    glVertex3f(-1.0, 0.0, 0.0);   // 시작점 (-1, 0)
    glVertex3f(0.5, 0.0, 0.0);    // 끝점 (0.5, 0)

    // 선 3: 위에서 아래 대각선으로 연결
    glVertex3f(0.0, 1.0, 0.0);    // 시작점 (0, 1)
    glVertex3f(0.0, -0.7, 0.0);   // 끝점 (0, -0.7)

    glEnd();                      // 선 그리기 종료
}

이 함수는 총 세 개의 선분으로 이루어진 도형을 그립니다.
보면 마치 '삼각형 + 수직선' 같은 도형이 하나 그려진다고 생각할 수 있어요.


🧪 예제 코드 분석 – 지역 좌표계 활용

이제 본격적으로 행렬 스택과 지역 좌표계를 활용한 예제를 살펴보죠.

✏️ 예제 코드

RenderFloor(); // 바닥 좌표계를 그려줌

DrawObject(1.0, 1.0, 0.0); // {0} 원래 좌표계에서 객체를 그림 (노란색)

여기까지는 기본 좌표계에서 하나의 객체를 그리는 코드입니다.

glPushMatrix();                  // {0} 좌표계를 저장

glTranslatef(2.0f, 3.0f, 0.0f);  // 현재 좌표계를 오른쪽 2, 위로 3만큼 이동시킴
DrawObject(1.0, 0.0, 0.0);       // {1} 새로운 위치에서 객체 그림 (빨간색)

여기서는 하나의 객체를 다른 위치에서 다시 그리기 위해 좌표계를 이동시켰습니다.
그런데 이 상태에서 다른 객체를 또 변환하면, 이전 좌표계가 사라지죠. 그래서:

glPushMatrix();                 // {1} 좌표계 상태 저장

glTranslatef(-4.0, -2.0, 0.0);  // 좌표계를 다시 왼쪽 4, 아래 2 이동
glRotatef(45.0, 0.0, 0.0, 1.0); // z축 기준으로 45도 회전
DrawObject(0.0, 1.0, 0.0);      // {2} 회전된 좌표계에서 그리기 (초록색)

glPopMatrix();                 // {1}로 좌표계 복귀

여기서는 이동 + 회전 조합을 통해 새로운 위치와 방향에서 객체를 하나 그립니다.

glTranslatef(-4.0, -4.0, 0.0); // {1} 좌표계 기준으로 또 이동
glRotatef(-90.0, 0.0, 0.0, 1.0); // z축 기준 -90도 회전
DrawObject(0.0, 1.0, 1.0);       // {3} 좌표계에서 그림 (청록색)

glPopMatrix();                  // {0}으로 완전히 복귀

이렇게 하면 하나의 도형을 여러 번 다양한 위치에 그릴 수 있습니다.
모두 원래 도형을 재사용하면서도, 각 도형의 위치나 회전이 서로 독립적이게 되죠.


🔁 예제 코드 분석 – 회전 애니메이션

이번에는 시간에 따라 회전하는 객체를 그려볼게요.

✏️ 애니메이션 예제 코드

static float angle1 = 0.0, timer1 = 0.0;  // 첫 번째 객체의 회전 각도와 시간 누적 변수
static float angle2 = 0.0, timer2 = 0.0;  // 두 번째 객체의 회전 각도와 시간 누적 변수

두 개의 독립적인 객체를 각각 회전시키기 위해 별도의 각도와 시간 변수를 사용합니다.

glPushMatrix();                           // 원래 좌표계 저장
glTranslatef(2.0, 0.0, 0.0);              // 오른쪽으로 이동
glRotatef(angle1, 0.0, 0.0, 1.0);         // angle1만큼 회전
DrawObject(1.0, 0.0, 0.0);                // 첫 번째 객체 그리기 (빨강)
glPopMatrix();                            // 원래 좌표계로 복귀

glPushMatrix();                           // 좌표계 다시 저장
glTranslatef(2.0, 0.0, 0.0);              // 같은 위치
glRotatef(angle2, 0.0, 0.0, 1.0);         // angle2만큼 회전 (반대 방향?)
DrawObject(0.0, 1.0, 1.0);                // 두 번째 객체 그리기 (청록)
glPopMatrix();                            // 좌표계 복귀

여기서 glPushMatrix() → 변환 → glPopMatrix() 흐름이
객체마다 고유한 회전 상태를 유지할 수 있게 해주는 핵심입니다.

// 시간이 1초 이상 누적되면 회전 각도를 증가시킴
if (timer1 > 1.0) {
    angle1 += 5.0; // 5도씩 증가
    timer1 = 0.0;  // 시간 초기화
}

if (timer2 > 1.0) {
    angle2 -= 5.0; // 반대 방향 회전
    timer2 = 0.0;
}

// 매 프레임마다 시간 누적
timer1 += ImGui::GetIO().DeltaTime;
timer2 += ImGui::GetIO().DeltaTime;

이 방식은 프레임 수가 아니라 실제 시간 기반으로 회전을 제어해서
다양한 PC 환경에서도 같은 속도로 동작하게 합니다.


✅ 마무리 요약

OpenGL에서 기하 변환을 제대로 활용하려면 다음을 이해하고 자유롭게 써야 합니다:

  • glPushMatrix() / glPopMatrix()로 좌표계 저장과 복원
  • glTranslatef(), glRotatef(), glScalef()로 실제 변환 적용
  • DrawObject()는 항상 현재 좌표계 기준으로 그려짐
  • 객체 단위로 독립된 변환 적용 가능