5장 OpenGL - ① 삼각형 폴리곤
OpenGL은 25년 넘게 이어져온 2D/3D 크로스 플랫폼 그래픽을 위한 산업 표준 라이브러리이다. 이번 장에서는 OpenGL의 초기화, 삼각형의 사용, 셰이더 프로그램 작성, 변환을 위한 행렬 사용, 텍스처 지원 추가 등 컴퓨터 그래픽스의 여러 주제를 다룬다.
💡 5장의 목차
- OpenGL 초기화
- 삼각형 폴리곤
- 셰이더
- 변환과 행렬
- 텍스처 매핑
OpenGL 초기화
초기화와 관련된 함수 설명은 교재와 코드내 주석을 참고. 아래에서는 주요 용어만 정리.
초기화 설정한 전체 코드 보기
OpenGL 콘텍스트란
콘텍스트(context)는 OpenGL이 인식하는 모든 상태나 오브젝트를 포함하는 OpenGL의 세계이다. OpenGL의 콘텍스트는 색상 깊이, 로드된 이미지나 모델, 그리고 여러 다양한 OpenGL 오브젝트를 포함한다. 하나의 OpenGL프로그램에 여러 개의 콘텍스트를 생성하는 것도 가능하다. OpenGL에서 오브젝트를 시뮬레이션하기 위해서는 하나 이상의 OpenGL의 세계인 콘텍스트를 생성해야 한다.
GLEW란
OpenGL은 확장 시스템과의 하위 호환성을 지원한다. 이 확장 기능을 사용하려면 원하는 확장 기능을 수동으로 요청해야 한다. 이 과정을 간소화하기 위해서 GLEW(OpenGL Extension Wrangler Library)라 불리는 오픈 소스 라이브러리를 사용한다. GLEW를 사용하면 간단한 하나의 함수 호출로 현재 OpenGL 콘텍스트 버전에서 지원하는 모든 확장 함수를 초기화 할 수 있다.
프레임 렌더링
OpenGL 콘텍스트 생성, OpenGL 속성 설정, GLEW 호출을 마치면 이제 프레임을 렌더링할 차례이다. OpenGL 함수를 사용하려면 Game::GenerateOutput
에서 화면을 클리어하고 장면을 그린 뒤 SDL과 마찬가지로 아래와 같이 버퍼를 스왑하는 과정이 필요하다.
void Game::GenerateOutput()
{
// 색상을 회색으로 설정
glClearColor(0.86f, 0.86f, 0.86f, 1.0f);
// 색상 버퍼 초기화
glClear(GL_COLOR_BUFFER_BIT);
// 장면을 그린다.
// 버퍼를 스왑해서 장면을 출력한다.
SDL_GL_SwapWindow(mWindow);
} // 아무런 장면을 그리지 않았기 때문에 회색 화면이 출력된다.
삼각형 폴리곤
컴퓨터가 3D 환경을 시뮬레이션하는 데는 여러 가지 방법이 있지만, 폴리곤(다각형)이 아래와 같은 이유로 게임에서 널리 사용된다.
- 런타임 시 많은 계산이 필요하지 않다.
- 폴리곤의 크기를 가변적으로 조절할 수 있다.
- 하드웨어 성능이 떨어지는 곳에서 실행되는 게임은 폴리곤의 수를 떨어뜨린 3D 모델을 사용할 수 있다.
- 대부분의 3D 오브젝트를 폴리곤으로 표현할 수 있다.
폴리곤(다각형) 중에서 게임에서는 대부분 삼각형 폴리곤을 선택한다. 그 이유는 다음과 같다.
- 삼각형은 가장 간단한 폴리곤이며 삼각형을 형성하려면 오직 3개의 정점(vertex)만 필요하다.
- 삼각형의 세 점은 항상 동일 평면상에 존재한다.
- 삼각형은 쉽게 테셀레이션(tessellation) 할 수 있다. 이는 복잡한 3D 물체를 여러 개의 삼각형으로 쉽게 나눌 수 있다는 뜻이다.
삼각형 폴리곤 렌더링은 3D게임 뿐만 아니라 2D게임에서도 적용된다. 초기 2D 게임은 스프라이트 이미지를 색상 버퍼의 원하는 위치에 간단히 복사할 수 있었는 블리팅(blitting) 이라는 기술을 사용하였다. 블리팅은 스프라이트 기반 콘솔 게임기에서는 효율적이었다. 하지만 현대의 그래픽 하드웨어에서는 3D 렌더링을 가장 효율적으로 처리하도록 제작된다. 그래서 블리팅이 비효율적인 반면 폴리곤 렌더링은 매우 효율적이다. 이 때문에 최근의 2D 게임은 삼각형을 사용해서 사각형을 그리며, 사각형에 이미지 파일의 색상을 채워서 스프라이트로 표현한다.
정규화된 장치 좌표 NDC
정규화된 장치 좌표(NDC, Normalized device coordinates)는 OpenGL에서 사용하는 기본 좌표계이다. OpenGL 위도우에서 윈도우의 중심은 정규화된 장치 좌표의 중심이다. 그리고 아래 왼쪽은 (-1,-1)이며 상단 오른쪽은 (1,1)로 정규화된 값을 사용한다. 정규화된 좌표이기 때문에 이 좌표 체계는 윈도우의 너비, 높이와 관계없다. 내부적으로 그래픽 하드웨어는 이 NDC를 해당 윈도우와 일치하는 픽셀로 변환한다.
3D에서 정규화된 장치 좌표의 z 요소 또한 [-1, 1]의 범위를 가지며 양의 z값은 화면 안으로 들어가는 방향이다.
버텍스 버퍼와 인덱스 버퍼
버텍스 버퍼
여러개의 삼각형으로 구성된 3D 모델은 많은 버텍스(정점) 정보를 가지고 있다. 만약 2개의 삼각형만으로 이루어진 모델이라면 2*3 = 6개의 버텍스를 가진다. 이들을 메모리상에 저장하는 간단한 방법은 아래와 같이 인접한 배열이나 버퍼 형태로 각 삼각형의 좌푯값을 직접 저장하는 것이다.
float vertices[] = {
-0.5f, 0.5f, 0.0f // 정점 A 의 x,y,z 좌표
0.5f, 0.5f, 0.0f // 정점 B
0.5f, -0.5f, 0.0f // 정점 C
0.5f, -0.5f, 0.0f // 정점 D
-0.5f, -0.5f, 0.0f // 정점 E
-0.5f, 0.5f, 0.0f // 정점 F
};
이 배열은 간단한 방법이지만 중복데이터를 포함한다. A와 F 정점이 중복이고 C와 D 정점이 중복이다. 이 중복을 제거 할 수 있다면 버퍼에 저장된 값의 수를 33% 줄일 수 있다. 삼각형 2개를 그리기 위해서 총 4바이트 float
18개를 사용했지만, 중복을 제거하게 된다면 12개의 float
만으로도 삼각형을 표현할 수 있다. 즉, 24바이트를 절약할 수 있다. 만약 폴리곤이 지금처럼 2개가 아니라 2만 개인 아주 큰 모델이라면 이는 굉장한 절약이 아닐 수 없다. 이 문제를 해결하기 위해 인덱스 버퍼를 사용한다.
인덱스 버퍼
인덱스 버퍼를 사용하기 위해서는 먼저 버텍스 버퍼를 생성한다. 다만 이번에는 중복된 버텍스는 제외한다. 그리고 각각의 버텍스 버퍼에 인덱스를 부여한다. 그리고 인덱스 버퍼에서는 3개의 인덱스로 구성된 개별 삼각형 정보를 배열에 저장한다.
float vertexBuffer[] = {
-0.5f, 0.5f, 0.0f // 버텍스 0
0.5f, 0.5f, 0.0f // 버텍스 1
0.5f, -0.5f, 0.0f // 버텍스 2
-0.5f, -0.5f, 0.0f // 버텍스 3
};
unsigned short indexBuffer[] = {
0, 1, 2, // 첫 번째 삼각형은 버텍스 0, 1, 2을 사용해서 생성
2, 3, 0 // 두 번째 삼각형은 버텍스 2, 3, 0을 사용해서 생성
};
인덱스 버퍼는 2바이트 크기의 unsigned short
를 사용해서 크기를 메모리 사용량을 더 줄였다. 버텍스 버퍼만 사용했을 때 72바이트를 사용한것에 비해서 인덱스 버퍼를 사용했을 때는 12 x 4 + 6 x 2 = 60 바이트를 사용함으로써 메모리의 사용량 20%를 절감했다. 더 복잡한 모델에서 버텍스/인덱스 버퍼 조합을 사용하면 훨씬 더 많은 메모리를 절약할 수 있다.
버텍스 버퍼와 인덱스 버퍼 사용하기
OpenGL에서 버텍스 버퍼와 인덱스 버퍼를 사용하기란 복잡하고 번거로운 일이다. 우리가 삼각형이 필요할 때마다 이 번거로운 과정을 진행하기 보다는 복잡한 과정은 한번 캡슐화해 두고 간단하게 사용하는 것이 바람직 할 것이다. 그러기 위해서 VertexArray
라는 class에 그 과정을 캡슐화 하였다. 버텍스 버퍼와 인덱스 버퍼를 사용하는 과정은 교재와 VertexArray
코드의 주석을 참고하자.
본 게시글은 에이콘 출판사의 Game Programming in C++ 도서를 학습하며 요약 정리한 내용입니다.