출처: http://soen.kr/lecture/library/opengl/opengl-7.htm
19. 버텍스 배열
OpenGL은 배열로 정점의 집합을 정의하는 방법을 공식적으로 지원한다. 먼저 다음 함수를 호출하여 배열을 사용하도록 설정한다.
void glEnableClientState(GLenum cap);
배열을 사용하는 것은 OpenGL 서버인 그래픽 카드의 설정과는 상관이 없고 그래픽을 그리는 클라이언트인 CPU와 상관이 있으므로 glEnable
함수를 사용하지 않는다. 어떻게 그릴 것인가의 문제가 아니고 어떻게 정보를 전달할 것인가의 문제이므로 배열 사용 여부는 클라이언트의 설정일 뿐이다. 그래서 glEnable 함수 대신 glEnableClientState
함수를 사용한다. 인수로 어떤 배열을 사용할 것인가를 전달한다.
GL_COLOR_ARRAY GL_EDGE_FLAG_ARRAY GL_FOG_COORD_ARRAY GL_INDEX_ARRAY GL_NORMAL_ARRAY GL_SECONDARY_COLOR_ARRAY GL_TEXTURE_COORD_ARRAY GL_VERTEX_ARRAY
(법선: 평면에서 곡선 위의 한 점을 지나고, 이 점에서의 곡선에 대한 접선에 수직인 직선. 또는 곡면 위의 한 점을 지나고, 이 점에서의 곡면에 대한 접평면에 수직인 직선.)
정점뿐만 아니라 각 정점의 색상이나 법선 정보, 텍스처 좌표 등도 배열로 정의 가능하다. 입체 도형을 구성하기 위한 모든 정보들을 하나의 배열에 집약할 수 있다. 배열은 동일 타입 변수의 집합일 뿐이므로 일반적인 C 구문으로 작성한다.
GLfloat vert[] = { x1, y1, z1, x2, y2, z2, .... };
다음 함수는 이 배열의 위치와 구조를 알려 준다. 대표적으로 정점 배열의 경우만 보자.
void glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid * pointer);
size는 한 좌표를 구성하는 요소의 개수이다. x, y 두개로 구성된 좌표이면 2를 지정하고 x, y, z 세 개로 구성된 좌표이면 3을 지정한다. 배열 자체가 일차원이므로 한 정점을 이루는 좌표가 몇 개씩 한 쌍인지를 밝히는 것이다. type은 배열 요소의 타입이다. 정수 좌표이면 GL_INT
, 실수 좌표이면 GL_FLOAT
등과 같이 타입을 밝힌다. 이 두 인수는 정점을 정의할 때 glVertex2i
를 호출할 것인지 glVertext3f
를 호출할 것인지를 결정한다.
stride
는 배열 요소간의 간격을 지정하는데 모든 요소를 연이어 배치하는 것이 보통이므로 이 값은 대개의 경우 0이다. 마지막 인수 pointer
는 배열이 정의된 실제 주소이다. 이 함수 호출에 의해 OpenGL은 정점 데이터가 어느 배열에 어떤 구조로 저장되어 있는지를 알게 된다.
색상 배열이나 법선 배열도 구조는 거의 동일하다.
void glColorPointer(GLint size, GLenum type, GLsizei stride, const GLvoid * pointer);
void glNormalPointer(GLenum type, GLsizei stride, const GLvoid * pointer);
색상은 rgb, rgba 등으로 구성될 수 있으므로 size
인수가 있다. 단, size의 가능한 값은 3 또는 4뿐이라는 점에서 정점 배열과는 다르다. 법선은 무조건 xyz 세 개씩 한 쌍이므로 size 정보는 불필요하다.
배열을 정의한 후 다음 함수로 배열 요소를 하나씩 꺼낸다.
void glArrayElement(GLint i);
이 함수는 배열에서 i 번째 정점 좌표를 꺼내 해당 정보를 서버로 전달한다. size
와 type
정보를 바탕으로 한번에 몇 개씩 꺼낼 것인가를 결정할 수 있다.
다음 예제는 피라미드를 구성하는 정점들을 배열로 정의해 두고 루프를 돌며 정점을 정의함으로써 입체 도형을 한번에 그린다.
#include <windows.h> #include <gl/glut.h> #include <stdio.h> void DoDisplay(); void DoKeyboard(unsigned char key, int x, int y); GLfloat xAngle, yAngle, zAngle; int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance , LPSTR lpszCmdParam, int nCmdShow) { glutCreateWindow("OpenGL"); glutDisplayFunc(DoDisplay); glutKeyboardFunc(DoKeyboard); glutMainLoop(); return 0; } void DoKeyboard(unsigned char key, int x, int y) { switch(key) { case 'a': yAngle += 2; break; case 'd': yAngle -= 2; break; case 'w': xAngle += 2; break; case 's': xAngle -= 2; break; case 'q': zAngle += 2; break; case 'e': zAngle -= 2; break; case 'z': xAngle = yAngle = zAngle = 0.0; break; } char info[128]; sprintf(info, "x=%.1f, y=%.1f, z=%.1f", xAngle, yAngle, zAngle); glutSetWindowTitle(info); glutPostRedisplay(); } void DoDisplay() { static GLfloat vert[] = { 0, 0, -0.8, // 12시 0.5, 0.5, 0, -0.5, 0.5, 0, 0, 0, -0.8, // 9시 -0.5, 0.5, 0, -0.5, -0.5, 0, 0, 0, -0.8, // 6시 -0.5, -0.5, 0, 0.5, -0.5, 0, 0, 0, -0.8, // 3시 0.5, -0.5, 0, 0.5, 0.5, 0, }; glEnableClientState(GL_VERTEX_ARRAY); // OpenGL 배열 사용 glVertexPointer(3, GL_FLOAT, 0, vert); // 정점 배열의 위치와 구조를 알려 준다 glClear(GL_COLOR_BUFFER_BIT); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glRotatef(xAngle, 1.0f, 0.0f, 0.0f); glRotatef(yAngle, 0.0f, 1.0f, 0.0f); glRotatef(zAngle, 0.0f, 0.0f, 1.0f); glRectf(-0.5, 0.5, 0.5, -0.5); // 밑바닥 glBegin(GL_TRIANGLES); printf("vert: %d, vert[0]: %d", sizeof(vert), sizeof(vert[0])); for (int i = 0; i < sizeof(vert) / sizeof(vert[0]); i += 3) { glVertex3f(vert[i], vert[i+1], vert[i+2]); } glEnd(); glPopMatrix(); glFlush(); }
vert 배열에는 피라미드를 구성하는 각 삼각형의 정점 좌표들을 저장했다. 삼각형 하나당 3개씩의 정점이 필요하고 각 정점당 x, y, z 세 쌍씩 좌표가 필요하므로 총 정점의 개수는 12개이며 배열의 크기는 36이다. 매 함수 호출시마다 배열을 초기화할 필요는 없으므로 static
으로 선언했는데 아예 전역으로 선언해도 상관없다.
배열을 선언한 후 정점 배열 기능을 사용하겠다고 선언한다. 그리고 glVertexPointer
함수로 vert 배열의 위치를 알려 주는데 GL_FLOAT
타입의 변수 3개씩 한쌍을 이루어 정점 하나에 대한 정보임을 명시한다. 아직 색상 정보가 없으므로 폴리곤 모드를 GL_LINE
으로 설정하여 선만 그리도록 했다.
일단 glRectf
함수로 밑면 사각형을 먼저 그린다. 나머지 삼각형은 루프를 돌며 그린다. 예제의 코드는 아주 원론적이다. vert 배열의 선두에서 시작하여 3칸씩 건너뛰면서 i, i+1, i+2 번째 요소를 꺼내면 이 값들이 곧 x, y, z 좌표값이다. 좌표를 꺼내 glVertex
함수로 차례대로 전달한 것이다.
정점들이 배열에 정의되어 있으므로 배열을 인수로 취하는 glVertex
함수를 호출할 수도 있다. 3칸씩 건너뛰면서 x 좌표가 있는 선두 번지를 glVertex3fv
함수로 전달하면 나머지 y, z 값은 알아서 꺼내 쓸 것이다. 다음 코드도 결과는 동일하다.
glBegin(GL_TRIANGLES); for (int i=0;i<sizeof(vert)/sizeof(vert[0]);i+=3) { glVertex3fv(&vert[i]); } glEnd();
이상의 두 코드는 어디까지나 배열에 좌표를 저장해 놓고 쓸 수 있다는 것을 보여주는 것 뿐이다. 좌표값을 인수로 직접 전달하지 않을 뿐 glVertex
함수를 매번 호출하는 것과 원론적으로 같은 코드이다. 함수 호출 코드가 반복되지 않으므로 메모리상의 이점은 있지만 루프 내부에서 함수를 여러 번 호출하기는 매 한가지이므로 속도상의 이점은 없다. 하지만 정점이 배열에 모여 있어 수정하기 편하다는 이점은 있다.
이제 OpenGL의 정점 배열 기능을 활용해 보자. glBegin ~ glEnd 부분을 다음 코드로 대체한다.
glBegin(GL_TRIANGLES); for (int i=0;i<sizeof(vert)/sizeof(vert[0])/3;i++) { glArrayElement(i); } glEnd();
배열에서 좌표를 직접 꺼낼 필요없이 glArrayElement
함수로 정점의 순서값만 전달해 주면 된다. 이때 glArrayElement
함수의 인수는 배열의 첨자가 아니라 정점의 순서값임을 유의하자. 각 정점이 3개씩의 값으로 구성되어 있으므로 배열 크기의 1/3만큼만 루프를 돌면 된다.
배열을 활용하는 최종적인 코드는 다음 함수를 호출하는 것이다.
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
이 함수는 배열의 크기만 알려 주면 알아서 루프를 돌고 glBegin, glEnd 블록까지도 처리해 준다. mode
인수는 glBegin으로 전달할 그리기 모드이며 first
는 배열의 첫번째 요소를 지정하되 통상 0이다. count
는 배열에 저장된 정보의 개수이다. 이 경우 정점이 12개이므로 12로 전달한다.
glDrawArrays(GL_TRIANGLES, 0, sizeof(vert)/sizeof(vert[0])/3);
glBegin ~ glEnd 부분을 한 줄로 대체할 수 있다.
20. 배열 인덱스
피라미드는 다음 다섯 개의 정점으로 구성된다.
각 정점들을 배열에 한번씩만 나열해 놓고 도형은 어느 정정들로 구성되는지 인덱스만 밝힘으로써 정의할 수 있다. 정점의 인덱스 배열을 정의한 후 다음 함수로 도형을 그린다.
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid * indices);
그리기 모드와 배열의 크기, 인덱스 배열의 요소 타입 그리고 인덱스 배열을 전달한다.
void DoDisplay() { static GLfloat vert[] = { 0, 0, -0.8, // 중앙 0.5, 0.5, 0, // 우상 -0.5, 0.5, 0, // 좌상 -0.5, -0.5, 0, // 좌하 0.5, -0.5, 0, // 우하 }; // 3개씩 끊어서 0, 1, 2, 3, 4 static GLubyte index[] = { 0, 1, 2, //12시 0, 2, 3, // 9시 0, 3, 4, // 6시 0, 4, 1, // 3시 }; glClear(GL_COLOR_BUFFER_BIT); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glRotatef(xAngle, 1.0f, 0.0f, 0.0f); glRotatef(yAngle, 0.0f, 1.0f, 0.0f); glRotatef(zAngle, 0.0f, 0.0f, 1.0f); glRectf(-0.5, 0.5, 0.5, -0.5); glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, vert); glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_BYTE, index); //인덱스 배열 정보 glPopMatrix(); glFlush(); }
실행 결과는 앞 예제와 완전히 동일하다. 그러나 중복되는 정점을 한번만 기록함으로써 개수가 5개로 줄어 들었다. 대신 이 정점들이 어떻게 조합되어 삼각형을 구성하는지 인덱스 배열이 추가되었다.
21. 색상 배열
다음은 색상 배열도 적용하여 각 면에 색상을 입혀 보자. 정점 배열과 사용하는 방법이나 구성 원리는 거의 비슷하다. 각 정점에 해당하는 색상을 배열로 정의해 두고 위치를 가르쳐 준다. 면을 채울 것이므로 폴리곤 모드는 지정할 필요가 없되 대신 면끼리 전후 관계를 표현하기 위해 깊이 테스트는 해야 한다.
void DoDisplay() { static GLfloat vert[] = { 0, 0, -0.8, // 중앙 0.5, 0.5, 0, // 우상 -0.5, 0.5, 0, // 좌상 -0.5, -0.5, 0, // 좌하 0.5, -0.5, 0, // 우하 }; static GLubyte index[] = { 0, 1, 2, //12시 0, 2, 3, // 9시 0, 3, 4, // 6시 0, 4, 1, // 3시 }; // 색상 배열 정보 static GLfloat color[] = { 1,1,1, // 중앙(흰색) 0,0,1, // 우상(파란색) 1,0,0, // 좌상(빨간색) 1,1,0, // 좌하(노란색) 0,1,0, // 우하(초록색) }; // 색상 정보 준비 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glRotatef(xAngle, 1.0f, 0.0f, 0.0f); glRotatef(yAngle, 0.0f, 1.0f, 0.0f); glRotatef(zAngle, 0.0f, 0.0f, 1.0f); glColor3f(1,1,1); glRectf(-0.5, 0.5, 0.5, -0.5); glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, vert); // 사용법은 glVertexPointer와 같음 glEnableClientState(GL_COLOR_ARRAY); glColorPointer(3, GL_FLOAT, 0, color); glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_BYTE, index); glPopMatrix(); glFlush(); }
color 배열에 각 정점에 대응되는 색상을 지정하되 정점 배열과 일대일로 대응되므로 순서를 동일하게 유지해야 한다.
22. 인터리브 배열
정점 배열과 색상 배열은 하나의 정점에 대한 정보이되 하나는 좌표이고 하나는 색상이어서 두 개의 배열로 따로 나누어 저장했다. 요소의 타입만 일치한다면 이 두 배열을 하나로 합칠 수도 있다. 이때 사용되는 인수가 stride
이다. 같은 배열에 순서대로 정의해 놓아도 stride
인수로 건너뛸 바이트 수를 지정할 수 있기 때문이다.
void DoDisplay() { static GLfloat vertcolor[] = { 1,1,1, 0, 0, -0.8, // 중앙 0,0,1, 0.5, 0.5, 0, // 우상 1,0,0, -0.5, 0.5, 0, // 좌상 1,1,0, -0.5, -0.5, 0, // 좌하 0,1,0, 0.5, -0.5, 0, // 우하 }; // 각 행에서 왼쪽 3개는 색상 정보, 오른쪽 3개는 정점 정보 (... 위 예제 참조 ...) glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, sizeof(GLfloat) * 6, &vertcolor[3]); // &vertcolor[3]: vertcolor 배열의 3번 주소를 찾아라 glEnableClientState(GL_COLOR_ARRAY); glColorPointer(3, GL_FLOAT, sizeof(GLfloat) * 6, vertcolor); (... 위 예제 참조 ...) }
vertcolor
배열은 정점의 좌표와 색상을 같이 저장한다. 배열의 0,1,2 번째 요소는 색상값이고 3,4,5번째 요소는 정점의 좌표값이며 두 값이 번갈아 나타난다. 정점 배열의 시작 위치를 3번째 요소의 번지에 맞추어 두고 6칸씩 건너뛰도록 지정하면 순서대로 꺼내서 사용할 것이다.
좌표와 색상 뿐만 아니라 법선이나 텍스처 좌표 등도 동일한 방법으로 한 배열에 저장할 수 있다. 여러 배열로 나누어 저장하는 것보다 한 배열에 섞어서 저장하되 주기적인 거리만큼만 잘 배치하면 논리적으로 아무 문제가 없다. 이 방법을 좀 더 공식화한 것을 인터리브 배열이라고 한다.
<span>void glInterleavedArrays(GLenum format, GLsizei stride, const GLvoid * pointer);</span>
format은 배열에 어떤 정보가 같이 들어 있는지를 지정한다. 레퍼런스를 보면 여러 가지 가능한 조합들에 대해 상수가 정의되어 있다. 대표적인 몇 가지만 소개하자면 다음과 같다.
GL_C3F_V3F
: 색상값 3개, 좌표값 3개가 교대로 들어 있다.GL_C4F_N3F_V3F
: 색상값 4개, 법선 3개, 좌표값 3개가 교대로 들어 있다.GL_T2F_C4F_N3F_V3F
: 텍스처, 색상, 법선, 좌표값이 들어 있다.
해당 정보 배열은 자동으로 활성화되므로 glEnableClientState
함수는 호출하지 않아도 상관없다. stride
는 인터리브 배열의 건너뛸 거리인데 이 인수를 활용하면 그 외의 추가 정보도 더 넣을 수 있다. pointer
는 물론 인터리브 배열의 선두 위치이다.
위 예제에서는 색상 배열과 좌표 배열을 활용하기 위해 다음 4줄의 함수를 호출했다.
glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, sizeof(GLfloat)*6, &vertcolor[3]); glEnableClientState(GL_COLOR_ARRAY); glColorPointer(3, GL_FLOAT, sizeof(GLfloat)*6, vertcolor);
각 배열을 사용하겠다는 의사 표시를 하고 또 각 배열에 대한 모양과 위치를 가르쳐 주어야 한다. 인터리브 배열을 사용하면 다음 한줄로 간단하게 이 호출을 대신할 수 있다.
glInterleavedArrays(GL_C3F_V3F, 0, vertcolor);
23. 출력 목록
glBegin과 glEnd 블록에서 그리기 명령을 직접 실행하는 것을 즉시 모드(immediate mode)라고 한다. 이 블록에 포함된 명령은 서버로 즉시 전송되어 바로 실행된다. 이에 비해 출력 목록(display list)
은 그리기 명령의 집합을 일단 정의한 후 한꺼번에 실행하는 방법이다. 미리 컴파일해 놓음으로써 출력 속도가 향상되고 동일한 명령을 여러 번 반복할 때 유리하다.
반복을 최소화하고 재사용성을 높인다는 면에서 프로그래밍에서 함수를 정의하는 것과 유사하다. 출력 목록은 이름 대신 정수 ID로 구분한다. 아무 정수나 쓸 수 없고 출력 목록끼리 구분되어야 한다.
다음 함수로 출력 목록의 ID를 생성한다.
GLuint glGenLists(GLsizei range);
필요한 개수를 전달하면 이 개수만큼 빈 영역을 찾아 시작 ID를 리턴한다. 이 ID 이후 range -1번까지의 ID를 사용할 수 있다. 하나만 필요하다면 glGenLists(1)
을 호출하여 리턴된 값을 바로 사용하면 된다. 두 개가 필요하다면 glGenLists(2)
을 호출하고 리턴값을 dl 변수로 받은 후 dl
과 dl + 1
을 사용하면 된다.
출력 목록을 시작할 때는 다음 함수를 호출한다.
void glNewList(GLuint list, GLenum mode);
list는 출력목록의 이름이다. mode는 출력 목록을 작성만 할 것인지(GL_COMPILE
) 아니면 작성과 동시에 실행할 것인지(GL_COMPILE_AND_EXECUTE
)를 지정한다.
다음 함수는 출력 목록 작성을 종료한다.
void glEndList(void);
glNewList와 glEndList 사이의 명령들이 출력 목록에 등록된다. 실행할 때는 다음 함수를 호출한다.실행할 출력목록의 ID를 전달한다.
void glCallList(GLuint list);
이때 목록에 저장된 그리기 명령이 실행된다.
다음 예제는 삼각형을 그리는 명령을 출력 목록에 저장해 두고 3번 호출한다.
#include <windows.h> #include <gl/glut.h> void DoDisplay(); int dl; int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance ,LPSTR lpszCmdParam,int nCmdShow){ glutCreateWindow("OpenGL"); /* * 메인 함수에서 출력 목록을 미리 작성해 놓는다. * 컴파일만 해 놓는 것이므로 출력 함수에서 작성하지 않아도 상관없다. */ dl = glGenLists(1); // 1개의 출력 목록 ID를 dl이라는 변수에 저장하고 // 이 목록에 삼각형을 그리는 명령들을 저장해 두었다. glNewList(dl, GL_COMPILE); // 출력 리스트 시작 glBegin(GL_TRIANGLES); glVertex2f(0.0, 0.2); glVertex2f(-0.2, -0.2); glVertex2f(0.2, -0.2); glEnd(); glEndList(); // 출력 리스트 끝 glutDisplayFunc(DoDisplay); glutMainLoop(); return 0; } void DoDisplay() { glClear(GL_COLOR_BUFFER_BIT); glColor3f(1,0,0); glCallList(dl); glTranslatef(0.5, 0.0, 0.0); // 도형 이동(상대적 위치) glColor3f(0,1,0); glCallList(dl); glTranslatef(0.2, 0.0, 0.0); glColor3f(0,0,1); glCallList(dl); glFlush(); }
0개의 댓글