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.

I. Setting up the OpenGL buffers and transform feedback objects

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]);

II. Compiling the shader for transform feedback

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);

III. Outputting transform feedback values from the shader.

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.

IV. Capturing transform feedback data and swapping buffers.

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.

V. Specifying point size in the shader.

(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.