🧭 OpenGL에서는 기하 변환이 어떻게 처리될까?
앞에서 우리는 점과 벡터, 어파인 결합, 이동·회전·스케일·밀림 변환, 그리고 합성 변환까지 다양한 기하 변환의 이론을 수학적으로 배웠습니다.
그렇다면 이제 이 개념들이 실제 OpenGL과 같은 그래픽스 시스템에서는 어떻게 구현될까요?
💡 동차 좌표와 행렬 곱으로 처리되는 변환
OpenGL에서는 모든 정점의 좌표를 내부적으로 동차 좌표(homogeneous coordinate) 로 표현합니다.
예를 들어, 2D 점 (x, y)은 (x, y, 1),
3D 점 (x, y, z)은 (x, y, z, 1)로 표현됩니다.
이러한 동차 좌표 덕분에 이동(translation) 과 같은 변환도 행렬 곱 하나로 처리할 수 있습니다.
즉, OpenGL에서는 다음과 같이 각 정점에 변환 행렬을 곱하는 방식으로 기하 변환을 처리합니다:
modelview
행렬은 도형의 위치, 방향, 크기 등을 나타내는 어파인 변환 행렬입니다.
🧱 OpenGL의 모델뷰 행렬 구조
OpenGL에서는 모든 변환 정보를 4×4 행렬로 통합해서 표현합니다.
이 4×4 행렬은 우리가 배운 이동, 회전, 스케일, 밀림 등의 모든 변환을 담을 수 있습니다.
예시:
이 행렬은 열 우선(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();
기존에 쌓인 변환이 있을 수 있으므로, 초기에는 항상 단위 행렬로 초기화해 주는 것이 좋습니다.
단위 행렬은 아무런 변환도 적용되지 않은 상태를 의미합니다:
✨ 모델뷰 행렬에 변환 적용하기
🔹 이동 변환 – glTranslate
glTranslatef(tx, ty, tz);
정점들을 ( (tx, ty, tz) )만큼 이동시키는 변환을 모델뷰 행렬에 오른쪽에서 곱합니다.
즉, 현재 모델뷰 행렬 ( M )에 이동 행렬 ( T )을 곱하면:
이동 행렬은 다음과 같습니다:
이 연산은 좌표계가 이동한 것처럼 해석할 수도 있습니다.
🔹 회전 변환 – glRotate
glRotatef(angle, vx, vy, vz);
angle
: 회전 각도 (도 단위)(vx, vy, vz)
: 회전 축 (벡터)
회전 행렬도 마찬가지로 현재 모델뷰 행렬의 우측에 곱해집니다.
예를 들어, z축 기준으로 회전할 경우:
즉,
🔹 스케일 변환 – glScale
glScalef(sx, sy, sz);
각 축에 대해 크기를 ( s_x, s_y, s_z )배 만큼 조절합니다.
스케일 행렬:
결과적으로,
🎯 누적 곱의 중요성: 적용 순서에 따라 결과가 달라짐!
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()
는 항상 현재 좌표계 기준으로 그려짐- 객체 단위로 독립된 변환 적용 가능
'프로그래밍 > 컴퓨터그래픽스' 카테고리의 다른 글
GLUT 콜백 함수 정리 (0) | 2025.04.05 |
---|---|
합성 변환과 해석 (0) | 2025.04.05 |
어파인 변환의 해석 (0) | 2025.04.05 |
동차 좌표, 어파인 공간, 어파인 변환: 이동, 회전, 스케일, 밀림 변환 (0) | 2025.04.04 |
OpenGL 기본 출력 객체 속성 정리 (0) | 2025.04.04 |