vbo update from heightmap

Started by
12 comments, last by sprite_hound 15 years, 11 months ago
I've been messing around with an fft water implementation, and on profiling the code, I find that updating my vbo is currently pretty slow. This isn't surprising since my vbo setup is really inefficient. I've been looking around, and noone seems to recommend any particular way of doing things. Quad strips? Triangle strips? Indexed triangle strips? Interleaved vbo or not? Use a vertex attribute for the heights in a seperate buffer (might be better for updating every frame)? Current update and rendering code is here:


// update:
void FFTWater::updateVBO(bool init) {
	std::vector<cml::vector3f> vboVertices;
	std::vector<cml::vector3f> vboNormals;
	
	numQuads = 0;
	for (unsigned int x = 0; x != xSize-1; ++x) {
		for (unsigned int z = 0; z != zSize-1; ++z) {
			vboVertices.push_back( cml::vector3f(getWorldX(x), getWorldHeight(x,z+1), getWorldZ(z+1)) );
			vboVertices.push_back( cml::vector3f(getWorldX(x+1), getWorldHeight(x+1,z+1), getWorldZ(z+1)) );
			vboVertices.push_back( cml::vector3f(getWorldX(x+1), getWorldHeight(x+1,z), getWorldZ(z)) );
			vboVertices.push_back( cml::vector3f(getWorldX(x), getWorldHeight(x,z), getWorldZ(z)) );
			
			vboNormals.push_back( normal[index(x,z+1)] );
			vboNormals.push_back( normal[index(x+1,z+1)] );
			vboNormals.push_back( normal[index(x+1,z)] );
			vboNormals.push_back( normal[index(x,z)] );
			
			++numQuads;
		}
	}
	
	vboVertexSize = vboVertices.size() * sizeof(cml::vector3f);
	vboNormalSize = vboNormals.size() * sizeof(cml::vector3f);
	
	if (init) {
		vbo.setData(vboNormalSize + vboVertexSize, 0, GL_DYNAMIC_DRAW);
	}
	vbo.replaceData(vboNormalSize, &vboNormals[0], 0);
	vbo.replaceData(vboVertexSize, &vboVertices[0], vboNormalSize);
}

// rendering:

	VertexBuffer::enableState(GL_NORMAL_ARRAY);
	VertexBuffer::enableState(GL_VERTEX_ARRAY);
	vbo.bind();
	
#define BUFFER_OFFSET(i) ((char*)NULL + (i))
	glNormalPointer(GL_FLOAT, 0, 0);
	glVertexPointer(3, GL_FLOAT, 0, BUFFER_OFFSET(vboNormalSize));
#undef BUFFER_OFFSET
	
	glDrawArrays(GL_QUADS, 0, numQuads * 4);
	
	VertexBuffer::unbind();
	VertexBuffer::disableState(GL_VERTEX_ARRAY);
	VertexBuffer::disableState(GL_NORMAL_ARRAY);


Advertisement
Since you know how big your buffer is going to be, why not just create a fixed-sized buffer to start with instead of a dynamic vector?

I've read an interleaved VBO can give better cache performance, but I really haven't noticed much difference in practice.

If only your heights are changing (not your x & z values, I assume?), yea, it might be beneficial to send those in as an attribute. That way you only have to update & send the heights and normals on the CPU-end.

Indexed triangle strips might help out. You shouldn't ever have to update the index buffer, which is good. But I can't say exactly how much it will help.

How are you sending your data across? I've noticed a significant difference between glBufferSubData and glMap/UnmapBuffer (where glBufferSubData performed much better).

You'll really have to test various things to find out what works best.
For simplicity of data Indexed triangle lists in strip order are the way to go; all the benifits of an index'd triangle list with only index data over head.

Update wise, if you are replacing the whole buffer the trick is to do a discard and update.

This is done by binding the VBO, called glBufferData() with a NULL data pointer and then calling again with the data. This effectively says to the driver 'I'm done with the content of the VBO, please give me a new buffer to write to if the old one isn't free' and can improve your speeds as you won't be syncing on buffer read/writes.

Other than that Nairb had some good tips about cutting down the amount of data you need to upload. A static VBO with X,Z values in and a dynamic one with Y and normal data would certainly help as you've just cut out 1/3 of the data to upload.
@Nairb: Thanks for the tips :)

Quote:Original post by phantom
For simplicity of data Indexed triangle lists in strip order are the way to go; all the benifits of an index'd triangle list with only index data over head.


@Phantom: Please could you clarify what you mean by an indexed triangle list in strip order? Use GL_TRIANGLES for the primitive type and send in strip style indices?
Quote:Original post by sprite_hound
@Nairb: Thanks for the tips :)

Quote:Original post by phantom
For simplicity of data Indexed triangle lists in strip order are the way to go; all the benifits of an index'd triangle list with only index data over head.


@Phantom: Please could you clarify what you mean by an indexed triangle list in strip order? Use GL_TRIANGLES for the primitive type and send in strip style indices?


Yes, use an IBO to order the triangles in strip pattern.
Quote:Original post by phantom
For simplicity of data Indexed triangle lists in strip order are the way to go; all the benifits of an index'd triangle list with only index data over head.


Well, I've fiddled some, and I'm still not entirely sure what this means.

Index generation is like so:
    std::vector<unsigned int> indices;    numTriangles = xSize*2 * (zSize-1) + zSize-2;    bool even = true;	for (unsigned int z = 0; z != zSize-1; ++z) {		if (even) {			int x;			for (x = 0; x != static_cast<int>(xSize); ++x) {				indices.push_back(x + z*xSize);				indices.push_back(x + z*xSize + xSize);			}			if (z != zSize-2) {				indices.push_back(--x + z*xSize);			}		}		else {			int x;			for (x = xSize-1; x >= 0; --x) {				indices.push_back(x + z*xSize);				indices.push_back(x + z*xSize + xSize);			}			if (z != zSize-2) {				indices.push_back(++x + z*xSize);			}		}		even = !even;	}    vboIndices.setData(indices.size()*sizeof(unsigned int), &indices[0]);


And the primitives are drawn thusly:
	VertexBuffer::enableState(GL_VERTEX_ARRAY);	vboVertices.bind();	glVertexPointer(3, GL_FLOAT, 0, NULL);		VertexBuffer::enableState(GL_INDEX_ARRAY);	vboIndices.bind();	glDrawElements(GL_TRIANGLE_STRIP, numTriangles, GL_UNSIGNED_INT, NULL);	vboIndices.unbind();	VertexBuffer::disableState(GL_INDEX_ARRAY);		VertexBuffer::unbind();	VertexBuffer::disableState(GL_VERTEX_ARRAY);


It works fine, but is this what you meant? (i.e. should I be using GL_TRIANGLE_STRIP?).

[Edited by - sprite_hound on April 24, 2008 6:25:26 PM]
No you use GL_TRIANGLES but you order the IBO to draw them like strips. You may have to use degenerate triangles depending how you go about this.
r u sure its the updating of the VBOs that is causing the slowdown?
cause the cpu code u have there is hugely inefficent

defining a vector array + then using many push_back() each frame is not the way to go, also the construction of the cml::vector3f() as well as getWorldX() etc aint the fastest methods

i wouldnt be surprised if u could speed this up by 10x at least
Just one possible idea but how about just defining the heightmap using the fft and then modifying the vertex position within a vertex shader? Then you can just use a flat plane defined as a vbo and it never needs to be updated.

The heightmap and normals can be passed in as textures samplers. I've done it to displace the surface of a sphere and was quite impressed with the results for a relatively simple shader.

I'm at work now but PM me and I'll post the code I wrote as I'm sure you could adapt it for your needs. Even if you don't it gives you one more option ;)

Another thing is that, as someone has pointed out, you're doing a push_back on your vectors to build all of the data each frame. You can speed things up quite simply here by simply calling "reserve" on the vector within the number of elements that you'll be populating it with and/or by storing the vector itself to avoid rebuilding it every frame. I bet that's not really adversely affecting your performace at this point anyway but it makes no sense to waste those cycles when you don't have too.

Andy

"Ars longa, vita brevis, occasio praeceps, experimentum periculosum, iudicium difficile"

"Life is short, [the] craft long, opportunity fleeting, experiment treacherous, judgement difficult."

@MARS_999: Enh... I suspect I'm just being dense. If I use GL_TRIANGLES with the indices set up like a strip then I get a nice pattern of disconnected triangles facing forwards and backwards.

I'm now just setting up the vbo once at the start, and updating the heights and attributes as normals every frame (setdata uses glBufferData, and replaceData uses glBufferSubData):

// Init code, called once... adding 1 to the sizes is to handle tiling.void FFTWater::initVBO() {	bool even = true;	int xSize = this->xSize+1;	int zSize = this->zSize+1;	for (int z = 0; z != zSize-1; ++z) {		if (even) {			int x;			for (x = 0; x != xSize; ++x) {				indices.push_back(x + z*xSize);				indices.push_back(x + z*xSize + xSize);			}			if (z != zSize-2) {				indices.push_back(--x + z*xSize);			}		}		else {			int x;			for (x = xSize-1; x >= 0; --x) {				indices.push_back(x + z*xSize);				indices.push_back(x + z*xSize + xSize);			}			if (z != zSize-2) {				indices.push_back(++x + z*xSize);			}		}		even = !even;	}	vboIndices.setData(indices.size()*sizeof(unsigned int), &indices[0]);	std::vector<cml::vector3f> vertices;	for (int z = 0; z != zSize; ++z) {		for (int x = 0; x != xSize; ++x) {			vertices.push_back( cml::vector3f(getWorldX(x), 0.f, getWorldZ(z)) );		}	}	vboVertices.setData(vertices.size()*sizeof(cml::vector3f), &vertices[0]);	vboNormalSize = vertices.size()*sizeof(cml::vector3f);	vboHeightSize = vertices.size()*sizeof(float);	vboAttributes.setData(vboNormalSize + vboHeightSize, 0, GL_STREAM_DRAW);	vboAttributes.replaceData(vboNormalSize, &normal[0], 0);	vboAttributes.replaceData(vboHeightSize, &floatHeight[0], vboNormalSize);}// update called every frame...void FFTWater::update(float dTime) {	// ... (update heights, normals)        vboAttributes.setData(vboNormalSize + vboHeightSize, NULL, GL_STREAM_DRAW);        vboAttributes.replaceData(vboNormalSize, &normal[0], 0);        vboAttributes.replaceData(vboHeightSize, &floatHeight[0], vboNormalSize);}// rendering...void FFTWater::render(Renderer* rd) {	// ...		VertexBuffer::enableState(GL_VERTEX_ARRAY);	vboVertices.bind();	glVertexPointer(3, GL_FLOAT, 0, NULL);		VertexBuffer::enableState(GL_NORMAL_ARRAY);	vboAttributes.bind();	glNormalPointer(GL_FLOAT, 0, NULL);		// Todo: add this to shaderProg class stuff.	glEnableVertexAttribArray(1);	glBindAttribLocation(shaderProgUnlit->getID(), 1, "Height");	vboAttributes.bind();#define BUFFER_OFFSET(i) ((char*)NULL + (i))	glVertexAttribPointer(1, 1, GL_FLOAT, 0, 0, BUFFER_OFFSET(vboNormalSize));#undef BUFFER_OFFSET		VertexBuffer::enableState(GL_INDEX_ARRAY);	vboIndices.bind();	glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, NULL);	vboIndices.unbind();	VertexBuffer::disableState(GL_INDEX_ARRAY);		glDisableVertexAttribArray(1);		VertexBuffer::unbind();	VertexBuffer::disableState(GL_NORMAL_ARRAY);	VertexBuffer::disableState(GL_VERTEX_ARRAY);		// ...}


@zedz: Yep, sorry, should have posted my altered code sooner.

@NineYearCycle: That sounds an interesting idea. PM'ing you. :)

This topic is closed to new replies.

Advertisement