Please send suggestions or corrections for this page to the author at sbuss@ucsd.edu.
This page describes the use of Transform Feedback in OpenGL as used in the ParticlesTransformFeedback program. This is part of a series of programs illustrating features of Modern OpenGL. On this page, we only discuss aspects of the software that deal with the use of Transform Feedback, specifically how to use data generated while rendering one image to render the next image, as illustrated with a particle system. For another use of Transform Feedback, see the program Chap1TransformFeedback.
The code fragments below are taken from the C++ program ParticlesTransformFeedback.cpp and the GLSL Shader code in ParticlesTransformFeedback.glsl. It is recommended to refer to the source code as you read this web page to see how the code fits together. We cannot cover all features of commands here, and it strongly recommended to search to find on-line documentation for the OpenGL commands and keywords to learn more about their functionality.
ParticlesTransformFeedback animates a particle system by initializing a VBO filled with the initial positions of the particles, and then each time the particles are rendered, transform feedback is used to fill another VBO with the data for the particles' positions at the next time step. When rendering the next image, the roles of the the VBOs are reversed, so that the second VBO is used to render the next image, and the data for the next image is stored into the first VBO. Apart from the initial setup of the VBO's, the VBO's are stored entirely on the GPU (at least, we hope OpenGL does this!) avoiding the overhead of transferring data back-and-forth between the GPU and CPU.
The first step is to setup the OpenGL buffers and transform feedback objects. ParticlesTransformFeedback uses two VAO's and two VBO's to avoid needing to repeatedly initialize the VAO. The first VAO is used to render from the first VBO; the second VAO is used for rendering from the second VBO. There are also two Transform Feedback Objects (FBO's): the first one uses the first VBO, the second uses the second VBO.
The following code is used to allocate the two VAO's, the two VBO's and the two FBO's:
glGenVertexArrays(2,&myVAO[0]);
glGenBuffers(2, &myVBO[0]);
glGenTransformFeedbacks(2, &myTF[0]);
The VBO's will hold each particle's 2D position,
2D velocity, and a particle ID number. (The particle ID number is redundant
and not really needed; see the discussion below.) The initial data is
specified with the following code which loads the temporary array
initParticles
.
// Each particle has: position - vec2 (two floats)
// velocity - vec2 (two floats)
// particle ID - 32 bit integer, >=0 for particles, -1 for the target.
// initParticles holds all these in one array of floats,
// but every fifth entry is actually an integer.
int numWords = 5 * (1 + N * N);
float* initParticles = new float[numWords];
initParticles[0] = initParticles[1] = initParticles[2] = initParticles[3] = 0.0;
*(GLint*)(initParticles+4) = -1;
float delta = 2.0f / (float)N; // Spacing between particles
for (int i = 0; i < N*N; i++) {
float* thisParticle = initParticles + 5 * (i + 1);
thisParticle[0] = -1.0f + (0.5f + (float)(i%N))*delta; // Initial x position
thisParticle[1] = -1.0f + (0.5f + (float)(i/N))*delta; // Initial y position
thisParticle[2] = thisParticle[3] = 0.0f; // Initial velocity is zero
*(GLint*)(thisParticle + 4) = i+1; // Particle ID
}
The next code sets up the two VAO's and VBO's. The first VBO is loaded with the
data from initParticles
using the call to glBufferData
.
The second VBO is allocated to the proper size to hold the data, but its contents
are not initialized due to the parameter (void*)0
to
glBufferData
. The advantage of using two VAO's over a single VAO is
that we avoid having to repeatedly call glVertexAttribPointer
and
glEnableVertexAttribArray
every time we swap buffers: instead, we
can get away with setting up the VAOs only once.
// Set the vertex attribute information in both VAO's.
// myVAO[0] uses myVBO[0]. myVAO[1] uses myVBO[1].
for (int i = 0; i < 2; i++) {
glBindVertexArray(myVAO[i]);
glBindBuffer(GL_ARRAY_BUFFER, myVBO[i]);
// Load data into myVBO[0]. Just allocate buffer size for myVBO[1].
glBufferData(GL_ARRAY_BUFFER, numWords*sizeof(float), (i==0 ? initParticles : (void*)0), GL_DYNAMIC_DRAW);
glVertexAttribPointer(vertPos_loc, 2, GL_FLOAT, GL_FALSE, 5*sizeof(float), (void*)0);
glEnableVertexAttribArray(vertPos_loc);
glVertexAttribPointer(vertVel_loc, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
glEnableVertexAttribArray(vertVel_loc);
glVertexAttribIPointer(vertID_loc, 1, GL_INT, 5 * sizeof(float), (void*)(4 * sizeof(float)));
glEnableVertexAttribArray(vertID_loc);
}
The Transform Feedback Objects (FBO's) merely hold the information about the destination
for the transform feedback. In our case, the first FBO uses the first VBO, and the
second FBO uses the second VBO. This is specified by the following code. The call
to glBindBufferBase
states that the transform feedback will be written
starting at the beginning of the VBO (hence the "Base" in the name). The parameter 0
means the first binding point of the FBO; we have only one binding point since the transform
feedback is all written into the same buffer (in "interleaved" mode).
// Let the two transform feedback buffers point to the two VBO buffers.
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, myTF[0]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, myVBO[0]);
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, myTF[1]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, myVBO[1]);
The transform feedback data will consist of five values per vertex (two
for the position, two for the velocity, and one for the vertex ID). This information
must generally be specified as the shader program is being compiled and linked.
For this the following command is used after the shader programs are compiled
but before they are linked. The five strings in the array feedbackValues
are the variable names in the shader program that are to output by the transform feedback.
The order of the variables specifies the order of the data as output. The
parameter GL_INTERLEAVED_ATTRIBS
indicates the values will all be
written to the same buffer ("interleaved" mode) instead of to different
output buffers.
const char* feedbackValues[5] = { "posX", "posY", "velX", "velY", "vertID" };
glTransformFeedbackVaryings(shaderParticles, 5, feedbackValues, GL_INTERLEAVED_ATTRIBS);
In ParticlesTransformFeedback, transform feedback data is output once for every vertex.
If there is a geometry shader, the transform feedback data is output
by the geometry shader, with each call to EmitVertex
outputting
a complete date of transform feedback data. If there is a tessellation shader,
but not a geometry shader, then the transform feedback data is output by the
tessellation shader. If there is neither a geometry shader nor a tessellation
shader, then the transform feedback data is output by the vertex shader,
with each call to the vertex shader outputting transform feedback data.
In ParticlesTransformFeedback
the transform feedback data is
output by the vertex shader. The vertex shader includes the following
lines to declare the output variables.
// Feedback transform outputs
out float posX, posY; // Updated particle position
out float velX, velY; // Updated particle velocity
out int vertID; // Vertex ID - passed thru
The vertex shader must set these values explicitly for each vertex.
As a side remark: The transform feedback data is always written to the output
buffer in the same order as the invocations of the vertex shader.
In other words, even though the vertex shaders may be running in parallel, and may be even
out of order, on multiple GPU cores, the data always is put into the transform
feedback buffer in the order reflecting the order of the vertices in
the VBO. The purpose of the variable vertID
was solely to test this; namely,
to test that vertID
is one less than gl_VertexID
(since
ParticlesTransformFeedback assigned vertex ID number starting with -1, but the built-in
gl_VertexID
reflecting the position in the VBO starts with 0.
The code below shows the core rendering loop in the C++ program. The
variable swapIndex
alternates between the values 0 and 1.
It uses one VAO, myVAO[swapIndex]
, to render from
the corresponding VBO. It uses the other FBO,
myTF[1-swapIndex]
, so that the output from the transform feedback
is written into the other VBO. Recall that both VBOs have been allocated sufficient
space to hold the output from the transform feedback.
The calls to glBeginTransformFeedback
and
glEndTransformFeedback
start and stop the
shader program's outputting of transform feedback data.
(It is also possible to "pause" the outputting of transform
feedback data.)
The call to glDrawTransformFeedback
uses the number of objects output into the indicated FBO to
control how many vertices are rendered. For this, the
FBO myTF[swapIndex]
is used (not myTF[1-swapIndex]
),
since this is the FBO that was used to fill the data into the
current VBO, myVBO[swapIndex]
.
(You can use a transform feedback query object
to learn the number of entries written to the FBO: for this
see Chap1TransformFeedback.)
The first time the image is rendered, glDrawArrays
is used
instead of glDrawTransformFeedback
: the sole reason for this is
that we must explicitly give the number (1+N*N
) of vertices to be rendered.
// Render using the VAO and VBO for swapIndex
glBindVertexArray(myVAO[swapIndex]);
// Transform feedback goes into the other VBO.
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, myTF[1-swapIndex]);
glBeginTransformFeedback(GL_POINTS);
if ( firstTimeRendering ) {
glDrawArrays(GL_POINTS, 0, 1+N*N); // Must use glDrawArrays the first time to specify number of points
firstTimeRendering = false;
}
else {
// This uses the size of the Transform Feedback data to specify the number of points .
// Apart from that, the next line is equivalent to using glDrawArrays(GL_POINTS, 0, ).
glDrawTransformFeedback(GL_POINTS, myTF[swapIndex]);
}
glEndTransformFeedback();
swapIndex = 1 - swapIndex; // Alternate between buffers
The C++ code is written above so that the VBO data will stay strictly on the GPU, and not needed to transferred between the GPU and CPU. It is also possible download the transform feedback data back to the CPU; for an example of this, see Chap1TransformFeedback.
(This has nothing to do with transform feedback.)
The vertex shader sets the variable glPointSize
to control
the size of points. In particular, the central gavitationally-attractive white point is
drawn as a large point than the animated vertices.
For this, it was necessary to call the following function
in the C++ program to enable setting point size inside the
shader program.
glEnable(GL_PROGRAM_POINT_SIZE);
Note that the shader program glPointSize
specification can be used
conjunction with
glEnable(GL_POINT_SMOOTH);
glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); // Make round points, not square points
and related commands. In our code, we disabled point smoothing, and also
GL_BLEND
so that the blending around
edges of points will not obscure neighoring points. Indeed,
when we first tried a 2000x2000 array of points, the blending around the
borders of points would completely obscures all of the points!
However this will be different in different implementations of OpenGL.