모든 3D 어플리케이션의 가장 대표적인 목표중 하나는 속도입니다. 여러분은 언제나 정렬, 컬링(culling), LOD 알고리즘 때문에 실제로 랜더링되는 폴리곤의 수를 제한적으로 사용할 수 밖에 없습니다. 하지만, 다른 모든 것이 실패하더라도 단지 raw 폴리곤 푸싱 기능만을 필요로하는 경우라면 GL에 의해 제공되는 최적화를 다룰 필요가 있습니다. 정점 배열은 이런 상황에 사용하기에 좋은 방법중 하나이며, 모든 사람이 꿈꾸는 FPS 향상을 위해서 '정점버퍼객체(VBO:vertex buffer object)'라 불리는 최근의 그래픽카드 확장을 부가적으로 사용할 수 있습니다. ARB_vertex_buffer_object 확장은 마치 정점 배열과 비슷하게 동작하지만, 랜더링 소요시간을 엄청나게 줄이기위해 그래픽카드의 고성능 메모리 영역에 데이타를 적재한다는 점이 다릅니다. 이 확장은 비교적 최신에 등장하였기 때문에(2003년 2월) 모든 카드가 이것을 지원하지는 않습니다(지포스256이상급필요). 그러므로 기술 확장이라는 맥락에서 사용할 필요가 있습니다.
이 튜토리얼에서 우리는 다음과 같은 것을 해볼겁니다.
- 높이필드맵으로부터 데이타 로딩
- 매쉬 데이타를 GL에 보다 효율적으로 전송하기위해 정점버퍼를 사용하기
- VBO 확장을 통하여 데이타를 고성능 메모리 영역에 적재하기
#define MESH_RESOLUTION 4.0f // 정점당 픽셀수 #define MESH_HEIGHTSCALE 1.0f // 메쉬 높이 배율 //#define NO_VBOS // 이부분이 정의되면, VBO는 강제로 사용하지 않도록 설정하게 됩니다.
처음 두 상수 선언은 표준 높이필드맵에 필요한 요소입니다 - 첫번째 값은 높이필드맵이 팩셀당 생성하게될 해상도를 설정하고 있으며, 두번째 값은 높이필드맵으로부터 읽어들인 데이타의 세로 확대비율을 설정합니다. 세번째 값은 (만일 정의되어있다면) VBO를 강제로 사용하지 않도록 합니다 - 이것은 한물간 카드를 가지고 있다면 쉽게 차이를 볼 수 있도록 하기위한 설정입니다.
다음에는 VBO 확장 관련 상수와 함수 포인터를 정의하도록 합시다.
// VBO 확장 정의(glext.h에서 가져옴) #define GL_ARRAY_BUFFER_ARB 0x8892 #define GL_STATIC_DRAW_ARB 0x88E4 typedef void (APIENTRY * PFNGLBINDBUFFERARBPROC) (GLenum target, GLuint buffer); typedef void (APIENTRY * PFNGLDELETEBUFFERSARBPROC) (GLsizei n, const GLuint *buffers); typedef void (APIENTRY * PFNGLGENBUFFERSARBPROC) (GLsizei n, GLuint *buffers); typedef void (APIENTRY * PFNGLBUFFERDATAARBPROC) (GLenum target, int size, const GLvoid *data, GLenum usage); // VBO 확장 함수 포인터들 PFNGLGENBUFFERSARBPROC glGenBuffersARB = NULL; // VBO Name 생성 함수 PFNGLBINDBUFFERARBPROC glBindBufferARB = NULL; // VBO Bind 함수 PFNGLBUFFERDATAARBPROC glBufferDataARB = NULL; // VBO 데이타 로딩 함수 PFNGLDELETEBUFFERSARBPROC glDeleteBuffersARB = NULL; // VBO 제거 함수
여기에서는 단지 데모에 필요한 함수들만 정의했습니다. 더많은 함수와 상수 선언들이 (VBO확장내에) 존재하므로 가장 최신의 glext.h를 http://www.opengl.org에서 다운로드하여 여기에 정의된 요소들을 사용할 것을 권장합니다. (어떻게 만들든 간에, 이 헤더화일이 여러분이 직접 적는 것보다는 더 깔끔할 겁니다) 이 함수들의 사용예제는 아래에서 곧 볼 수 있을겁니다.
이제 표준 수학관련 정의와 매쉬 클래스를 정의하겠습니다. 이들 모두는 정말로 기본적인 것만 만든 것이며, 데모 전용으로 만들어진 것이라는 점에 주의하세요. 실전에서는 여러분만의 수학관련 라이브러리를 개발할 것을 권장합니다.
class CVert // 정점 클래스
{
public:
float x; // X 요소
float y; // Y 요소
float z; // Z 요소
};
typedef CVert CVec; // 두 타입 각각의 정의가 비슷하므로 이렇게 정의합니다.
class CTexCoord // 텍스쳐 좌표 클래스
{
public:
float u; // U 요소
float v; // V 요소
};
class CMesh
{
public:
// 메쉬 데이타
int m_nVertexCount; // 정점 개수
CVert* m_pVertices; // 정점 데이타
CTexCoord* m_pTexCoords; // 텍스쳐 좌표들
unsigned int m_nTextureId; // 텍스쳐 ID
// Vertex Buffer Object Names
unsigned int m_nVBOVertices; // 정점 VBO Name
unsigned int m_nVBOTexCoords; // 텍스쳐 좌표 VBO Name
// 임시 데이타
AUX_RGBImageRec* m_pTextureImage; // 높이필드 데이타
public:
CMesh(); // 매쉬 클래스 생성자
~CMesh(); // 매쉬 클래스 소멸자
// 높이필드맵 로더
bool LoadHeightmap( char* szPath, float flHeightScale, float flResolution );
// 특정 위치에서의 높이값을 반환합니다
float PtHeight( int nX, int nY );
// VBO 구축 함수
void BuildVBOs();
};
이 코드의 대부분은 설명을 위해 자작된 것입니다. 여기에서 정점과 텍스쳐 좌표 데이타를 별도로 놓고 있다는 점에 주목하시기 바랍니다. 나중에 지적하게 되겠지만 이것은 전적으로 불필요한 방식입니다.
이제 전역변수 선언을 보도록 합시다. 먼저 VBO 유효여부 플래그 변수를 볼 수 있는데, 이것은 초기화 코드에서 설정하게 될 것습니다. 그다음에는 매쉬 인스턴스 변수가 있고, Y축 회전값이 있습니다. 그다음에는 FPS 모니터링을 위한 변수들이 선언되어있습니다. 이것은 이 코드에서 제공하는 최적화를 보여주기위해서 FPS 게이지 출력을 위한 것입니다.
bool g_fVBOSupported = false; // ARB_vertex_buffer_object가 지원되는가? CMesh* g_pMesh = NULL; // 매쉬 데이타 float g_flYRot = 0.0f; // 회전각 int g_nFPS = 0, g_nFrames = 0; // FPS 및 FPS 카운터 DWORD g_dwLastFPS = 0; // 최근의 FPS 검사 시간
일단 CMesh 함수 정의는 건너뛰고 LoadHeightmap에서 시작하도록 하겠습니다. For those of you who live under a rock, a heightmap is a two-dimensional dataset, commonly an image, which specifies the terrain mesh's vertical data. There are many ways to implement a heightmap, and certainly no one right way. My implementation reads a three channel bitmap and uses the luminosity algorithm to determine the height from the data. The resulting data would be exactly the same if the image was in color or in grayscale, which allows the heightmap to be in color. 개인적으로 targa와 같은 4 채널 이미지를 선호하는데 이는 높이값으로 알파채널을 사용할 수 있기 때문입니다. 어쨌든, 이 튜토리얼에서는 간단한 비트맵을 사용하기로 합시다.
먼저, 높이필드맵화일이 존재하는지 확인한다음, 있다면 GLaux의 비트맵 로딩 함수를 사용하여 그것을 로딩합니다. 네, 네, 물론 여러분이 즐겨 사용하는 이미지 로딩 루틴을 작성하는 것이 더 좋겠지만 그건 이 튜토리얼의 범위밖의 얘깁니다.
bool CMesh :: LoadHeightmap( char* szPath, float flHeightScale, float flResolution )
{
// 오류검사
FILE* fTest = fopen( szPath, "r" ); // 이미지를 엽니다
if( !fTest ) // 반드시 열려야합니다.
return false; // 열리지 않았다면, 화일은 없는거겠죠.
fclose( fTest ); // 핸들을 닫습니다.
// 텍스쳐 데이타를 로딩합니다
m_pTextureImage = auxDIBImageLoad( szPath ); // GLaux의 비트맵 로딩 루틴을 사용합니다.
Now things start getting a little more interesting. First of all, I would like to point out that my heightmap generates three vertices for every triangle - vertices are not shared. I will explain why I chose to do that later, but I figured you should know before looking at this code.
I start by calculating the amount of vertices in the mesh. The algorithm is essentially ( ( Terrain Width / Resolution ) * ( Terrain Length / Resolution ) * 3 Vertices in a Triangle * 2 Triangles in a Square ). Then I allocate my data, and start working my way through the vertex field, setting data.
// Generate Vertex Field
m_nVertexCount = (int) ( m_pTextureImage->sizeX * m_pTextureImage->sizeY * 6 / ( flResolution * flResolution ) );
m_pVertices = new CVec[m_nVertexCount]; // Allocate Vertex Data
m_pTexCoords = new CTexCoord[m_nVertexCount]; // Allocate Tex Coord Data
int nX, nZ, nTri, nIndex=0; // Create Variables
float flX, flZ;
for( nZ = 0; nZ < m_pTextureImage->sizeY; nZ += (int) flResolution )
{
for( nX = 0; nX < m_pTextureImage->sizeX; nX += (int) flResolution )
{
for( nTri = 0; nTri < 6; nTri++ )
{
// Using This Quick Hack, Figure The X,Z Position Of The Point
flX = (float) nX + ( ( nTri == 1 || nTri == 2 || nTri == 5 ) ? flResolution : 0.0f );
flZ = (float) nZ + ( ( nTri == 2 || nTri == 4 || nTri == 5 ) ? flResolution : 0.0f );
// Set The Data, Using PtHeight To Obtain The Y Value
m_pVertices[nIndex].x = flX - ( m_pTextureImage->sizeX / 2 );
m_pVertices[nIndex].y = PtHeight( (int) flX, (int) flZ ) * flHeightScale;
m_pVertices[nIndex].z = flZ - ( m_pTextureImage->sizeY / 2 );
// Stretch The Texture Across The Entire Mesh
m_pTexCoords[nIndex].u = flX / m_pTextureImage->sizeX;
m_pTexCoords[nIndex].v = flZ / m_pTextureImage->sizeY;
// Increment Our Index
nIndex++;
}
}
}
I finish off the function by loading the heightmap texture into OpenGL, and freeing our copy of the data. This should be fairly familiar from past tutorials.
// Load The Texture Into OpenGL
glGenTextures( 1, &m_nTextureId ); // Get An Open ID
glBindTexture( GL_TEXTURE_2D, m_nTextureId ); // Bind The Texture
glTexImage2D( GL_TEXTURE_2D, 0, 3, m_pTextureImage->sizeX, m_pTextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, m_pTextureImage->data );
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
// Free The Texture Data
if( m_pTextureImage )
{
if( m_pTextureImage->data )
free( m_pTextureImage->data );
free( m_pTextureImage );
}
return true;
}
PtHeight is relatively simple. It calculates the index of the data in question, wrapping any overflows to avoid error, and calculates the height. The luminance formula is very simple, as you can see, so don't sweat it too much.
float CMesh :: PtHeight( int nX, int nY )
{
// Calculate The Position In The Texture, Careful Not To Overflow
int nPos = ( ( nX % m_pTextureImage->sizeX ) + ( ( nY % m_pTextureImage->sizeY ) * m_pTextureImage->sizeX ) ) * 3;
float flR = (float) m_pTextureImage->data[ nPos ]; // Get The Red Component
float flG = (float) m_pTextureImage->data[ nPos + 1 ]; // Get The Green Component
float flB = (float) m_pTextureImage->data[ nPos + 2 ]; // Get The Blue Component
return ( 0.299f * flR + 0.587f * flG + 0.114f * flB ); // Calculate The Height Using The Luminance Algorithm
}
Hurray, time to get dirty with Vertex Arrays and VBOs. So what are Vertex Arrays? Essentially, it is a system by which you can point OpenGL to your geometric data, and then subsequently render data in relatively few calls. The resulting cut down on function calls (glVertex, etc) adds a significant boost in speed. What are VBOs? Well, Vertex Buffer Objects use high-performance graphics card memory instead of your standard, ram-allocated memory. Not only does that lower the memory operations every frame, but it shortens the bus distance for your data to travel. On my specs, VBOs actually triple my framerate, which is something not to be taken lightly.
So now we are going to build the Vertex Buffer Objects. There are really a couple of ways to go about this, one of which is called "mapping" the memory. I think the simplist way is best here. The process is as follows: first, use glGenBuffersARB to get a valid VBO "name". Essentially, a name is an ID number which OpenGL will associate with your data. We want to generate a name because the same ones won't always be available. Next, we make that VBO the active one by binding it with glBindBufferARB. Finally, we load the data into our gfx card's data with a call to glBufferDataARB, passing the size and the pointer to the data. glBufferDataARB will copy that data into your gfx card memory, which means that we will not have any reason to maintain it anymore, so we can delete it.
void CMesh :: BuildVBOs()
{
// Generate And Bind The Vertex Buffer
glGenBuffersARB( 1, &m_nVBOVertices ); // Get A Valid Name
glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOVertices ); // Bind The Buffer
// Load The Data
glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*3*sizeof(float), m_pVertices, GL_STATIC_DRAW_ARB );
// Generate And Bind The Texture Coordinate Buffer
glGenBuffersARB( 1, &m_nVBOTexCoords ); // Get A Valid Name
glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOTexCoords ); // Bind The Buffer
// Load The Data
glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*2*sizeof(float), m_pTexCoords, GL_STATIC_DRAW_ARB );
// Our Copy Of The Data Is No Longer Necessary, It Is Safe In The Graphics Card
delete [] m_pVertices; m_pVertices = NULL;
delete [] m_pTexCoords; m_pTexCoords = NULL;
}
Ok, time to initialize. First we will allocate and load our mesh data. Then we will check for GL_ARB_vertex_buffer_object support. If we have it, we will grab the function pointers with wglGetProcAddress, and build our VBOs. Note that if VBOs aren't supported, we will retain the data as usual. Also note the provision for forced no VBOs.
// Load The Mesh Data
g_pMesh = new CMesh(); // Instantiate Our Mesh
if( !g_pMesh->LoadHeightmap( "terrain.bmp", // Load Our Heightmap
MESH_HEIGHTSCALE, MESH_RESOLUTION ) )
{
MessageBox( NULL, "Error Loading Heightmap", "Error", MB_OK );
return false;
}
// Check For VBOs Supported
#ifndef NO_VBOS
g_fVBOSupported = IsExtensionSupported( "GL_ARB_vertex_buffer_object" );
if( g_fVBOSupported )
{
// Get Pointers To The GL Functions
glGenBuffersARB = (PFNGLGENBUFFERSARBPROC) wglGetProcAddress("glGenBuffersARB");
glBindBufferARB = (PFNGLBINDBUFFERARBPROC) wglGetProcAddress("glBindBufferARB");
glBufferDataARB = (PFNGLBUFFERDATAARBPROC) wglGetProcAddress("glBufferDataARB");
glDeleteBuffersARB = (PFNGLDELETEBUFFERSARBPROC) wglGetProcAddress("glDeleteBuffersARB");
// Load Vertex Data Into The Graphics Card Memory
g_pMesh->BuildVBOs(); // Build The VBOs
}
#else /* NO_VBOS */
g_fVBOSupported = false;
#endif
IsExtensionSupported is a function you can get from OpenGL.org. My variation is, in my humble opinion, a little cleaner.
bool IsExtensionSupported( char* szTargetExtension )
{
const unsigned char *pszExtensions = NULL;
const unsigned char *pszStart;
unsigned char *pszWhere, *pszTerminator;
// Extension names should not have spaces
pszWhere = (unsigned char *) strchr( szTargetExtension, ' ' );
if( pszWhere || *szTargetExtension == '\0' )
return false;
// Get Extensions String
pszExtensions = glGetString( GL_EXTENSIONS );
// Search The Extensions String For An Exact Copy
pszStart = pszExtensions;
for(;;)
{
pszWhere = (unsigned char *) strstr( (const char *) pszStart, szTargetExtension );
if( !pszWhere )
break;
pszTerminator = pszWhere + strlen( szTargetExtension );
if( pszWhere == pszStart || *( pszWhere - 1 ) == ' ' )
if( *pszTerminator == ' ' || *pszTerminator == '\0' )
return true;
pszStart = pszTerminator;
}
return false;
}
It is relatively simple. Some people simply use a sub-string search with strstr, but apparently OpenGL.org doesn't trust the consistancy of the extension string enough to accept that as proof. And hey, I am not about to argue with those guys.
Almost finished now! All we gotta do is render the data.
void Draw (void)
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
glLoadIdentity (); // Reset The Modelview Matrix
// Get FPS
if( GetTickCount() - g_dwLastFPS >= 1000 ) // When A Second Has Passed...
{
g_dwLastFPS = GetTickCount(); // Update Our Time Variable
g_nFPS = g_nFrames; // Save The FPS
g_nFrames = 0; // Reset The FPS Counter
char szTitle[256]={0}; // Build The Title String
sprintf( szTitle, "Lesson 45: NeHe & Paul Frazee's VBO Tut - %d Triangles, %d FPS", g_pMesh->m_nVertexCount / 3, g_nFPS );
if( g_fVBOSupported ) // Include A Notice About VBOs
strcat( szTitle, ", Using VBOs" );
else
strcat( szTitle, ", Not Using VBOs" );
SetWindowText( g_window->hWnd, szTitle ); // Set The Title
}
g_nFrames++; // Increment Our FPS Counter
// Move The Camera
glTranslatef( 0.0f, -220.0f, 0.0f ); // Move Above The Terrain
glRotatef( 10.0f, 1.0f, 0.0f, 0.0f ); // Look Down Slightly
glRotatef( g_flYRot, 0.0f, 1.0f, 0.0f ); // Rotate The Camera
Pretty simple - every second, save the frame counter as the FPS and reset the frame counter. I decided to throw in poly count for impact. Then we move the camera above the terrain (you may need to adjust that if you change the heightmap), and do a few rotations. g_flYRot is incremented in the Update function.
To use Vertex Arrays (and VBOs), you need to tell OpenGL what data you are going to be specifying with your memory. So the first step is to enable the client states GL_VERTEX_ARRAY and GL_TEXTURE_COORD_ARRAY. Then we are going to want to set our pointers. I doubt you have to do this every frame unless you have multiple meshes, but it doesn't hurt us cycle-wise, so I don't see a problem.
To set a pointer for a certain data type, you have to use the appropriate function - glVertexPointer and glTexCoordPointer, in our case. The usage is pretty easy - pass the amount of variables in a point (three for a vertex, two for a texcoord), the data cast (float), the stride between the desired data (in the event that the vertices are not stored alone in their structure), and the pointer to the data. You can actually use glInterleavedArrays and store all of your data in one big memory buffer, but I chose to keep it seperate to show you how to use multiple VBOs.
Speaking of VBOs, implementing them isn't much different. The only real change is that instead of providing a pointer to the data, we bind the VBO we want and set the pointer to zero. Take a look.
// Set Pointers To Our Data
if( g_fVBOSupported )
{
glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOVertices );
glVertexPointer( 3, GL_FLOAT, 0, (char *) NULL ); // Set The Vertex Pointer To The Vertex Buffer
glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOTexCoords );
glTexCoordPointer( 2, GL_FLOAT, 0, (char *) NULL ); // Set The TexCoord Pointer To The TexCoord Buffer
} else
{
glVertexPointer( 3, GL_FLOAT, 0, g_pMesh->m_pVertices ); // Set The Vertex Pointer To Our Vertex Data
glTexCoordPointer( 2, GL_FLOAT, 0, g_pMesh->m_pTexCoords ); // Set The Vertex Pointer To Our TexCoord Data
}
Guess what? Rendering is even easier.
// Render glDrawArrays( GL_TRIANGLES, 0, g_pMesh->m_nVertexCount ); // Draw All Of The Triangles At Once
Here we use glDrawArrays to send our data to OpenGL. glDrawArrays checks which client states are enabled, and then uses their pointers to render. We tell it the geometric type, the index we want to start from, and how many vertices to render. There are many other ways we can send the data for rendering, such as glArrayElement, but this is the fastest way to do it. You will notice that glDrawArrays is not within glBegin / glEnd statements. That isn't necessary here.
glDrawArrays is why I chose not to share my vertex data between triangles - it isn't possible. As far as I know, the best way to optimize memory usage is to use triangle strips, which is, again, out of this tutorial's scope. Also you should be aware that normals operate "one for one" with vertices, meaning that if you are using normals, each vertex should have an accompanying normal. Consider that an opportunity to calculate your normals per-vertex, which will greatly increase visual accuracy.
Now all we have left is to disable vertex arrays, and we are finished.
// Disable Pointers glDisableClientState( GL_VERTEX_ARRAY ); // Disable Vertex Arrays glDisableClientState( GL_TEXTURE_COORD_ARRAY ); // Disable Texture Coord Arrays }
If you want more information on Vertex Buffer Objects, I recommend reading the documentation in SGI's extension registry - http://oss.sgi.com/projects/ogl-sample/registry. It is a little more tedious to read through than a tutorial, but it will give you much more detailed information.
Well that does it for the tutorial. If you find any mistakes or misinformation, or simply have questions, you can contact me at paulfrazee(at)cox.net.
- DOWNLOAD
Visual C++ Code For This Lesson.
- DOWNLOAD
Code Warrior 5.3 Code For This Lesson. ( Conversion by Scott Lupton )
- DOWNLOAD
Dev C++ Code For This Lesson. ( Conversion by Gerald Buchgraber )








