E D R S I H C RSS
ID
Password
Join
공식적으로 거절당하기 전까지는 어떤 것도 믿지 말아라. - 클라우드 콕번


모든 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 [http]Visual C++ Code For This Lesson.
  • DOWNLOAD [http]Code Warrior 5.3 Code For This Lesson. ( Conversion by Scott Lupton )
  • DOWNLOAD [http]Dev C++ Code For This Lesson. ( Conversion by Gerald Buchgraber )

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2010-10-28 12:42:52
Processing time 0.7172 sec