Transform feedback
Up until now we've always sent vertex data to the graphics processor and onlyproduced drawn pixels in framebuffers in return. What if we want to retrieve thevertices after they've passed through the vertex or geometry shaders? In thischapter we'll look at a way to do this, known astransform feedback.
So far, we've used VBOs (Vertex Buffer Objects) to store vertices to be used fordrawing operations. The transform feedback extension allows shaders to writevertices back to these as well. You could for example build a vertex shader thatsimulates gravity and writes updated vertex positions back to the buffer. Thisway you don't have to transfer this data back and forth from graphics memory tomain memory. On top of that, you get to benefit from the vast parallelprocessing power of today's GPUs.
Basic feedback
We'll start from scratch so that the final program will clearly demonstratehow simple transform feedback is. Unfortunately there's no preview this time,because we're not going to draw anything in this chapter! Although this featurecan be used to simplify effects like particle simulation, explaining these is abit beyond the scope of these articles. After you've understood the basics oftransform feedback, you'll be able to find and understand plenty of articlesaround the web on these topics.
Let's start with a simple vertex shader.
const GLchar* vertexShaderSrc = R"glsl( in float inValue; out float outValue; void main() { outValue = sqrt(inValue); })glsl";
This vertex shader does not appear to make much sense. It doesn't set agl_Position
and it only takes a single arbitrary float as input. Luckily, wecan use transform feedback to capture the result, as we'll see momentarily.
GLuint shader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(shader, 1, &vertexShaderSrc, nullptr);glCompileShader(shader);GLuint program = glCreateProgram();glAttachShader(program, shader);
Compile the shader, create a program and attach the shader, but don't callglLinkProgram
yet! Before linking the program, we have to tell OpenGL whichoutput attributes we want to capture into a buffer.
const GLchar* feedbackVaryings[] = { "outValue" };glTransformFeedbackVaryings(program, 1, feedbackVaryings, GL_INTERLEAVED_ATTRIBS);
The first parameter is self-explanatory, the second and third parameter specifythe length of the output names array and the array itself, and the finalparameter specifies how the data should be written.
The following two formats are available:
- GL_INTERLEAVED_ATTRIBS: Write all attributes to a single buffer object.
- GL_SEPARATE_ATTRIBS: Writes attributes to multiple buffer objects or atdifferent offsets into a buffer.
Sometimes it is useful to have separate buffers for each attribute, but letskeep it simple for this demo. Now that you've specified the output variables,you can link and activate the program. That is because the linking processdepends on knowledge about the outputs.
glLinkProgram(program);glUseProgram(program);
After that, create and bind the VAO:
GLuint vao;glGenVertexArrays(1, &vao);glBindVertexArray(vao);
Now, create a buffer with some input data for the vertex shader:
GLfloat data[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };GLuint vbo;glGenBuffers(1, &vbo);glBindBuffer(GL_ARRAY_BUFFER, vbo);glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);
The numbers indata
are the numbers we want the shader to calculate the squareroot of and transform feedback will help us get the results back.
With regards to vertex pointers, you know the drill by now:
GLint inputAttrib = glGetAttribLocation(program, "inValue");glEnableVertexAttribArray(inputAttrib);glVertexAttribPointer(inputAttrib, 1, GL_FLOAT, GL_FALSE, 0, 0);
Transform feedback will return the values ofoutValue
, but first we'll need tocreate a VBO to hold these, just like the input vertices:
GLuint tbo;glGenBuffers(1, &tbo);glBindBuffer(GL_ARRAY_BUFFER, tbo);glBufferData(GL_ARRAY_BUFFER, sizeof(data), nullptr, GL_STATIC_READ);
Notice that we now pass anullptr
to create a buffer big enough to hold all ofthe resulting floats, but without specifying any initial data. The appropriateusage type is nowGL_STATIC_READ
, which indicates that we intend OpenGL towrite to this buffer and our application to read from it. (Seereference for usage types)
We've now made all preparations for therendering computationprocess. As we don't intend to draw anything, the rasterizer should be disabled:
glEnable(GL_RASTERIZER_DISCARD);
To actually bind the buffer we've created above as transform feedback buffer,we have to use a new function calledglBindBufferBase
.
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tbo);
The first parameter is currently required to beGL_TRANSFORM_FEEDBACK_BUFFER
to allow for future extensions. The second parameter is the index of the outputvariable, which is simply0
because we only have one. The final parameterspecifies the buffer object to bind.
Before doing the draw call, you have to enter transform feedback mode:
glBeginTransformFeedback(GL_POINTS);
It certainly brings back memories of the oldglBegin
days! Just like thegeometry shader in the last chapter, the possible values for the primitive modeare a bit more limited.
GL_POINTS
—GL_POINTS
GL_LINES
—GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP, GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY
GL_TRIANGLES
—GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES_ADJACENCY, GL_TRIANGLE_STRIP_ADJACENCY
If you only have a vertex shader, as we do now, the primitivemust match theone being drawn:
glDrawArrays(GL_POINTS, 0, 5);
Even though we're now working with data, the single numbers can still be seen asseparate "points", so we use that primitive mode.
End the transform feedback mode:
glEndTransformFeedback();
Normally, at the end of a drawing operation, we'd swap the buffers to presentthe result on the screen. We still want to make sure the rendering operation hasfinished before trying to access the results, so we flush OpenGL's commandbuffer:
glFlush();
Getting the results back is now as easy as copying the buffer data back to anarray:
GLfloat feedback[5];glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(feedback), feedback);
If you now print the values in the array, you should see the square roots of theinput in your terminal:
printf("%f %f %f %f %f\n", feedback[0], feedback[1], feedback[2], feedback[3], feedback[4]);
Congratulations, you now know how to make your GPU perform general purposetasks with vertex shaders! Of course a real GPGPU framework likeOpenCLis generally better at this, but the advantage of transform feedback is that youcan directly repurpose the data in drawing operations, by for example bindingthe transform feedback buffer as array buffer and performing normal drawingcalls.
If you have a graphics card and driver that supports it, you could also usecompute shaders in OpenGL 4.3instead, which were actually designed for tasks that are less related to drawing.
You can find the full codehere.
Feedback transform and geometry shaders
When you include a geometry shader, the transform feedback operation willcapture the outputs of the geometry shader instead of the vertex shader. Forexample:
// Vertex shaderconst GLchar* vertexShaderSrc = R"glsl( in float inValue; out float geoValue; void main() { geoValue = sqrt(inValue); })glsl";// Geometry shaderconst GLchar* geoShaderSrc = R"glsl( layout(points) in; layout(triangle_strip, max_vertices = 3) out; in float[] geoValue; out float outValue; void main() { for (int i = 0; i < 3; i++) { outValue = geoValue[0] + i; EmitVertex(); } EndPrimitive(); })glsl";
The geometry shader takes a point processed by the vertex shader and generates2 more to form a triangle with each point having a 1 higher value.
GLuint geoShader = glCreateShader(GL_GEOMETRY_SHADER);glShaderSource(geoShader, 1, &geoShaderSrc, nullptr);glCompileShader(geoShader);...glAttachShader(program, geoShader);
Compile and attach the geometry shader to the program to start using it.
const GLchar* feedbackVaryings[] = { "outValue" };glTransformFeedbackVaryings(program, 1, feedbackVaryings, GL_INTERLEAVED_ATTRIBS);
Although the output is now coming from the geometry shader, we've not changedthe name, so this code remains unchanged.
Because each input vertex will generate 3 vertices as output, the transformfeedback buffer now needs to be 3 times as big as the input buffer:
glBufferData(GL_ARRAY_BUFFER, sizeof(data) * 3, nullptr, GL_STATIC_READ);
When using a geometry shader, the primitive specified toglBeginTransformFeedback
must match the output type of the geometry shader:
glBeginTransformFeedback(GL_TRIANGLES);
Retrieving the output still works the same:
// Fetch and print resultsGLfloat feedback[15];glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(feedback), feedback);for (int i = 0; i < 15; i++) { printf("%f\n", feedback[i]);}
Although you have to pay attention to the feedback primitive type and the sizeof your buffers, adding a geometry shader to the equation doesn't change muchother than the shader responsible for output.
The full code can be foundhere.
Variable feedback
As we've seen in the previous chapter, geometry shaders have the unique propertyto generate a variable amount of data. Luckily, there are ways to keep track ofhow many primitives were written by usingquery objects.
Just like all the other objects in OpenGL, you'll have to create one first:
GLuint query;glGenQueries(1, &query);
Then, right before callingglBeginTransformFeedback
, you have to tell OpenGLto keep track of the number of primitives written:
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, query);
AfterglEndTransformFeedback
, you can stop "recording":
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
Retrieving the result is done as follows:
GLuint primitives;glGetQueryObjectuiv(query, GL_QUERY_RESULT, &primitives);
You can then print that value along with the other data:
printf("%u primitives written!\n\n", primitives);
Notice that it returns the number of primitives, not the number of vertices.Since we have 15 vertices, with each triangle having 3, we have 5 primitives.
Query objects can also be used to record things such asGL_PRIMITIVES_GENERATED
when dealing with just geometry shaders andGL_TIME_ELAPSED
to measure timespent on the server (graphics card) doing work.
Seethe full code if you got stuck somewhere on the way.
Conclusion
You now know enough about geometry shaders and transform feedback to make yourgraphics card do some very interesting work besides just drawing! You can evencombine transform feedback and rasterization to update vertices and draw themat the same time!
Exercises
- Try writing a vertex shader that simulates gravity to make points hover aroundthe mouse cursor using transform feedback to update the vertices. (Solution)