Experience does not err, it is only your judgment that errs in promising itself results which are not caused by your experiments. --Leonardo Da Vinci
* 주의! : 이 확장은 ARB_vertex_object 확장이 나옴으로서 이제는 구식방법으로 간주됩니다. 참고하시길...
1 알리는 글 #
http://www.levp.de/3d/ 에서 원문을 찾을 수 있습니다. 이 곳의 글은 나름대로 의역한 글로써 읽어주시면 좋겠습니다. 특히, extension 초기화 부분은 과감히 삭제했습니다. 몇몇부분은 제맘대로(?) 써놓은 부분이 있습니다. 영어가 되시는 분은 원문을 읽어보시기 바랍니다.
2 VAR가 뭡니까? #
표준 OpenGL에서는 그래픽 파이프라인에 정점 데이타를 전송하는데 3가지 방법이 존재한다 :
- Immediate 모드 - glBegin, glEnd, glVertex, ...
- 디스플레이 리스트 - 랜더링 정보들을 컴파일한 후 1개의 함수호출로 그것을 실행한다.
- 정점배열(Vertex array) - 랜더링 정보들을 배열에 담은 후, 그것을 몇개의 함수 호출에 배열 포인터를 넘겨줌으로써 처리한다.
- Immediate 모드방식은 사용하기 매우 쉽고, 간단한 Primitive들을 그릴 필요가 있을때 매우 유용하다. 그러나 함수호출의 수가 많은 경우는 오버해드가 심하고 일반적으로 가장 느리다.
- 디스플레이 리스트방식은 매우 빠르다. (서버쪽에 저장될 경우) 그러나 한번 생성된 디스플레이 리스트는 변경할 수 없다. 또한 몇몇 측면에서 엄청난 크기의 메모리를 소모할 수 있다.
- 정점배열방식은 함수호출에 따른 오버해드가 없지만 그것들은 클라이언트쪽에 저장된다. 따라서 표준 OpenGL에서 그것들은 매 프레임때마다 서버쪽 그래픽 파이프라인으로 복사된다. 또다른 제약조건은 다른 텍스펴 좌표를 가진 동일한 정점을 사용할 수 없다는 것이다. 정점배열은 생성 이후에 변경될 수 있고 따라서 동적인 표현이 허락된다. (역주 : GL_EXT_vertex_array 를 사용하여 지정할 수 있다.)
3 그래서 어떻게 동작하는데? #
VAR의 개념은 정말 간단하다:
정점배열을 생성할 때 여러분은 그것들을 일반 시스템 메모리에 위치시키지 않고, 그래픽카드에서 직접적으로 억세스할 수 있는 메모리(비디오 메모리나 AGP 메모리)에 위치시킨다. 그런 다음 VAR를 사용한다는 것을 OpenGL에게 알려주고 모든 것이 정확히 설정되었다면, OpenGL카드는 정점배열을 직접 억세스할 것이다 - (특별히 초당 프레임 한정이나 CPU에 따른 제한이 없을 경우라면) 표면적으로는 속도가 빨라지는 결과를 얻을 수 있다. (역주 : 요즘 카드는 비디오메모리가 상당히 넓다.
)
)
기본적으로 VAR를 사용할 때, glDrawElements()함수는 OpenGL 파이프라인에 데이타들을 모두 넣기 전에 바로 리턴된다. (이것은 병렬성을 증가시킨다) : 그래픽카드는 아직도 데이타를 잡고 있고 정점배열들을 그리기 위한 몇가지 작업을 가지고 있을 것이지만 CPU는 어떤 다른 연산에도 자유로울 수 있다. (즉, 랜더링 작업 중에 CPU는 다른 일을 할 수 있다.)
정말 좋지 않은가? 자, 그렇지만 몇가지 약점이 있다 :
- 만약 정점배열들을 비디오 메모리에 넣는 경우에는 데이타는 (카드와 마더보드사이의) 버스를 통해서 전송되어야하는 이유때문에 읽기/쓰기에 관련된 억세스는 매우 느려진다.
- 만약 AGP 메모리를 사용하는 중이라면 AGP 메모리는 캐쉬되지 않기때문에 읽기 억세스가 엄청나게 느려지게 된다. 그러나, 몇가지 규칙만 따라주면 AGP 메모리에 쓰기 작업을 하는 것은 매우 빠르다. (구형 비디오 카드가 아니라면 왠만한 카드는 AGP를 지원한다.)
4 세부사항들 #
먼저 우리는 Extension을 사용하기 위해 몇가지 함수를 정의할 필요가 있다. 여기서는 크로스플랫폼을 지원하기 위해 wgl과 glX 양쪽을 정의하고 있다. (역주 : 사실 쓸데없는 짓이다.
여기를 참조해서 가져다 쓰면 함수정의는 필요없다.
) 아래의 세가지 코드조각들은 필요한 함수의 포인터를 얻기 위한 것들이다.
여기를 참조해서 가져다 쓰면 함수정의는 필요없다.
) 아래의 세가지 코드조각들은 필요한 함수의 포인터를 얻기 위한 것들이다.
void *lglGetProcAddress(char *name)
{
#ifdef _WIN32
void *t = wglGetProcAddress(name);
if (t == 0)
ErrorMsg("lglGetProcAddress: wglGetProcAddredd returned 0\n");
return t;
#else
void *t = glXGetProcAddressARB((byte*)name);
if (t == 0)
ErrorMsg("lglGetProcAddress: glXGetProcAddressARB returned 0\n");
return t;
#endif
}
#ifndef GL_NV_vertex_array_range #error Header files must define GL_NV_vertex_array_range #endif extern PFNGLFLUSHVERTEXARRAYRANGENVPROC glFlushVertexArrayRangeNV; extern PFNGLVERTEXARRAYRANGENVPROC glVertexArrayRangeNV;
glFlushVertexArrayRangeNV = (PFNGLFLUSHVERTEXARRAYRANGENVPROC)
lglGetProcAddress("glFlushVertexArrayRangeNV");
glVertexArrayRangeNV = (PFNGLVERTEXARRAYRANGENVPROC)
lglGetProcAddress("glVertexArrayRangeNV");
#ifdef _WIN32
typedef void* (APIENTRY * PFNWGLALLOCATEMEMORYNVPROC) (GLsizei size,
GLfloat readFrequency, GLfloat writeFrequency, GLfloat priority);
typedef void (APIENTRY * PFNWGLFREEMEMORYNVPROC) (void *pointer);
#else
typedef void* ( * PFNGLXALLOCATEMEMORYNVPROC) (GLsizei size,
GLfloat readFrequency, GLfloat writeFrequency, GLfloat priority);
typedef void ( * PFNGLXFREEMEMORYNVPROC) (void *pointer);
#endif _WIN32
#ifdef _WIN32
wglAllocateMemoryNV = (PFNWGLALLOCATEMEMORYNVPROC)
wglGetProcAddress("wglAllocateMemoryNV");
wglFreeMemoryNV = (PFNWGLFREEMEMORYNVPROC)
wglGetProcAddress("wglFreeMemoryNV");
#else
glXAllocateMemoryNV = (PFNGLXALLOCATEMEMORYNVPROC)
glXGetProcAddressARB((byte*)"glXAllocateMemoryNV");
glXFreeMemoryNV = (PFNGLXFREEMEMORYNVPROC)
glXGetProcAddressARB((byte*)"glXFreeMemoryNV");
#endif
단지 1개의 큰 덩어리 메모리 만을 선언한 후 자신만의 메모리 관리를 통해서 정점배열들을 선언된 메모리 안에 넣어야 한다. 결코 1개 이상의 메모리를 선언할 수 없다!
이제 코드를 보자.
#ifdef _WIN32
if ((wglAllocateMemoryNV) && (wglFreeMemoryNV))
m_memory = (byte*)wglAllocateMemoryNV(size, 0.2f, 0.2f, 0.5f);
#else
if ((glXAllocateMemoryNV != 0) && (glXFreeMemoryNV != 0))
m_memory = (byte*)glXAllocateMemoryNV(size, 0.2f, 0.2f, 0.5f);
#endif
if (!m_memory)
{
// do error handling here
}
glVertexArrayRangeNV(m_memorySize, m_memory); glEnableClientState(GL_VERTEX_ARRAY_RANGE_NV);
전체에 걸쳐서 한번만 glVertexArrayRangeNV()을 호출해야한다.
지금 우리는 메모리를 얻었고 VAR는 사용가능한 상태가 되었다. 이제 정점들/노멀벡터/텍스쳐좌표들을 이 새로 할당된 메모리에 위치시킬 필요가 있다. 만약 1개 이상의 배열을 그릴려고 한다면 이것은 간단한 문제가 아니다. 왜냐하면 앞에서 보았듯이 단지 1개의 큰 덩어리 메모리만을 할당할 수 있기 때문이다. 해결책은 간단한 메모리 관리자를 작성하는 것이지만, 그것은 말한 것보다는 더 복잡하게 들릴 수도 있겠다.
다운로드 란에서
lge.zip을 다운로드 받을 수 있다. 압축화일내에서 lmemorymanager.cpp 화일과 lmemorymanager.h 화일을 살펴보기 바란다. (간단한 메모리 관리자 소스임)
lge.zip을 다운로드 받을 수 있다. 압축화일내에서 lmemorymanager.cpp 화일과 lmemorymanager.h 화일을 살펴보기 바란다. (간단한 메모리 관리자 소스임)
자, 이제 어떻게 VAR를 사용하는지 간단한 예제를 보여주려고 한다. 우리가 일단은 m_memory가 가리키고 있는 메모리가 충분히 할당되어있다고 가정할 것이다. 예제는 지정한 원점으로 부터 난수로 지정된 점들을 그린다.
float m[100][4]; // 이것은 정점들의 배열이다.
for (int i=0; i<100; i++)
{
// 각 축을 기준으로 -25 ~ +25사이에 난수로 지정된 좌표를 생성한다.
m[i][0] = -25 + (float)rand()/(float)RAND_MAX*50;
m[i][1] = -25 + (float)rand()/(float)RAND_MAX*50;
m[i][2] = -25 + (float)rand()/(float)RAND_MAX*50;
m[i][0] = 1;
}
// 이제 우리는 VAR 메모리로 이 정점 리스트를 복사할 필요가 있다,
// (이때, 또한 VAR 메모리에 직접 리스트를 생성할 수도 있다. 그렇다면 메모리를 복사할 필요가 없다)
memcpy(m_memory, (void*)m, 100*4*sizeof(float));
// 지금 정점배열을 활성화 시키고 OpenGL에게 그것이 어디에 있는지 알려준다.
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(4, GL_FLOAT, 0, m_memory);
// 광원을 끄고 현재 색을 백색으로 놓는다.
glDisable(GL_LIGHTING);
glColor3f(1.0f, 1.0f, 1.0f);
// 정점의 크기를 3 픽셀로 지정한다.
glPointSize(3);
glDrawArrays(GL_POINTS, 0, 100);
이 예제에서 나는 primitive들을 위한 인덱스가 필요없는 glDrawArrays를 사용했다. glDrawElements는 인덱스의 배열이 필요하며, 이 배열의 규칙은 다음과 같다:
Primitive들을 위한 인덱스를 AGP/비디오 메모리안에 위치시키지 말아라. 그 인덱스 배열은 일반 시스템 메모리에 그대로 위치시켜라.
5 마지막 단계들 #
기본적인 내용은 위에 있는 것으로 끝이다. 여기애서는 기대한 것처럼 VAR가 동작하지 않게 될만한 요인이 되는 몇가지 요소들을 소개한다.
VAR를 활성화/비활성화(Enabling/disabling)하는 것은 느리다. 가급적 정규 정점배열(Vertex Array) 기능과 VAR를 섞어쓰려고 하지말아라. 아니면 VAR2 extension을 사용하라
VAR를 활성화/비활성화하는 것은 파이프라인을 비우는 작업을 적용한다. 이것은 glDisableClientState(GL_VERTEX_ARRAY_RANGE_NV)이 실행될 때, 모든 OpenGL 명령들이 실행될 때까지 기다리고 그 다음에야 리턴된다. VAR2를 사용하면 파이프라인을 비우는(flush) 작업없이 VAR를 활성화/비활성화 설정이 가능하다.
비디오 메모리를 읽기/쓰기 억세스를 하는 것과 AGP 메모리를 읽는 것은 매우 느리다. AGP메모리에 쓰기를 실행할때 만족할만한 속도를 얻기 위해서는 (메모리 번지 순서에 따른) 순차적으로 쓰기를 실행하라.
따라서 만약 몇가지 이유(예를 들자면 충돌체크)때문에 데이타를 읽어야할 필요가 있을때에는 시스템 메모리에 데이타의 복사본을 두는 편이 더 낫다. 이것은 이런 경우와 같을 때를 대비해서 두배만큼의 메모리를 필요로하게 될 것을 의미한다.
만일 충분한 AGP/비디오 메모리를 확보하지 못했을 경우에는 시스템메모리상에서 VAR를 사용하기 보다는 VAR를 비활성화 시킨 후 기존 정점배열을 사용하라.
할당가능한 AGP 메모리의 크기는 PC 시스템에 따라 다양하다. 그러므로 어느만큼의 메모리가 할당된다는 것을 코드작성시에 정할 수 없으므로 실행중에 검사하여야만 한다.
만약 여러분이 정점배열내에 몇가지 유별난 포맷을 사용하고 있다면, VAR 스팩을 확인한 후 그 포맷이 지원되는지 확인해보아라. 모든 포맷이 지원되는 것이 아니기 때문이다. (일반적으로는 거의 모든 포맷이 지원된다고도 할 수 있다.
)
)
몇몇개의 중요한 규칙들을 지나치지 않기를 바란다.
어플리케이션이 종료할때 할당한 메모리를 해제하는 것은 중요한 것이다. 잊지말도록.
자, 이제 시작하는데 있어 충분한 정보였으리라 생각된다. 건투를 빈다.
#ifdef _WIN32
wglFreeMemoryNV(m_memory);
#else
glXFreeMemoryNV(m_memory);
#endif








