Creating a particle system using transform feedback

Transform Feedback provides a way to capture the output of the vertex (or geometry) shader to a buffer for use in subsequent passes. Originally introduced into OpenGL with version 3.0, this feature is particularly well suited for particle systems because among other things, it enables us to do discrete simulations. We can update a particle's position within the vertex shader and render that updated position in a subsequent pass (or the same pass). Then the updated positions can be used in the same way as input to the next frame of animation.

In this example, we'll implement the same particle system from the previous recipe (Creating a particle fountain), this time making use of transform feedback. Instead of using an equation that describes the particle's motion for all time, we'll update the particle positions incrementally, solving the equations of motion based on the forces involved at the time each frame is rendered.

A common technique is to make use of the Euler method, which approximates the position and velocity at time t based on the position, velocity, and acceleration at an earlier time.

Creating a particle system using transform feedback

In the previous equation the subscripts represent the time step (or animation frame), P is the particle position, and v is the particle velocity. The equations describe the position and velocity at frame n + 1 as a function of the position and velocity during the previous frame (n). The variable h represents the time step size, or the amount of time that has elapsed between frames. The term an represents the instantaneous acceleration that is computed based on the positions of the particles. For our simulation, this will be a constant value, but in general it might be a value that changes depending on the environment (wind, collisions, inter-particle interactions, and so on).

Note

The Euler method is actually numerically integrating the Newtonian equation of motion. It is one of the simplest techniques for doing so. However, it is a first-order technique, which means that it can introduce a significant amount of error. More accurate techniques include Verlet integration, and Runge-Kutta integration. Since our particle simulation is designed to look good and physical accuracy is not of high importance, the Euler method should suffice.

To make our simulation work, we'll use a technique sometimes called buffer "ping-ponging." We maintain two sets of vertex buffers and swap their uses each frame. For example, we use buffer A to provide the positions and velocities as input to the vertex shader. The vertex shader updates the positions and velocities using the Euler method and sends the results to buffer B using transform feedback. Then in a second pass, we render the particles using buffer B.

Creating a particle system using transform feedback

In the next frame of animation, we repeat the same process, swapping the two buffers.

In general, transform feedback allows us to define a set of shader output variables that are to be written to a designated buffer (or set of buffers). There are several steps involved that will be demonstrated, but the basic idea is as follows. Just before the shader program is linked, we define the relationship between buffers and shader output variables using the function glTransformFeedbackVaryings. During rendering, we initiate a transform feedback pass. We bind the appropriate buffers to the transform feedback binding points. (If desired, we can disable rasterization so that the particles are not rendered.) We enable transform feedback using the function glBeginTransformFeedback and then draw the point primitives. The output from the vertex shader will be stored in the appropriate buffers. Then we disable transform feedback by calling glEndTransformFeedback.

Getting ready

Create and allocate three pairs of buffers. The first pair will be for the particle positions, the second for the particle velocities, and the third for the "start time" for each particle (the time when the particle comes to life). For clarity, we'll refer to the first buffer in each pair as the A buffer, and the second as the B buffer. Also, we'll need a single buffer to contain the initial velocity for each particle.

Create two vertex arrays. The first vertex array should link the A position buffer with the first vertex attribute (attribute index 0), the A velocity buffer with vertex attribute one, the A start time buffer with vertex attribute two, and the initial velocity buffer with vertex attribute three. The second vertex array should be set up in the same way using the B buffers and the same initial velocity buffer. In the following code, the handles to the two vertex arrays will be accessed via the GLuint array named particleArray.

Initialize the A buffers with appropriate initial values. For example, all of the positions could be set to the origin, and the velocities and start times could be initialized in the same way as described in the previous recipe Creating a particle fountain. The initial velocity buffer could simply be a copy of the velocity buffer.

When using transform feedback, we define the buffers that will receive the output data from the vertex shader by binding the buffers to the indexed binding points under the GL_TRANSFORM_FEEDBACK_BUFFER target. The index corresponds to the index of the vertex shader's output variable as defined by glTransformFeedbackVaryings.

To help simplify things, we'll make use of transform feedback objects. Use the following code to set up two transform feedback objects for each set of buffers:

GLuint feedback[2];  // Transform feedback objects
GLuint posBuf[2];    // Position buffers (A and B)
GLuint velBuf[2];    // Velocity buffers (A and B)
GLuint startTime[2]; // Start time buffers (A and B)

// Create and allocate buffers A and B for posBuf, velBuf
// and startTime

// Setup the feedback objects
glGenTransformFeedbacks(2, feedback);

// Transform feedback 0
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback[0]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,0,posBuf[0]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,1,velBuf[0]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,2,startTime[0]);

// Transform feedback 1
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback[1]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,0,posBuf[1]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,1,velBuf[1]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,2,startTime[1]);

Similar to vertex arrays, transform feedback objects store the buffer bindings for the GL_TRANSFORM_FEEDBACK_BUFFER binding point so that they can be reset quickly at a later time. In the previous code, we create two transform feedback objects, and store their handles in the array named feedback. For the first object, we bind posBuf[0] to index 0, velBuf[0] to index 1 and startTime[0] to index 2 of the binding point (buffer set A). These bindings are connected to the shader output variables with glTransformFeedbackVaryings (or via a layout qualifier, see the There's More following section). The last argument for each is the buffer's handle. For the second object, we do the same thing using the buffer set B.

Once this is set up, we can define the set of buffers to receive the vertex shader's output, by binding to one or the other transform feedback object.

The uniform variables that need to be set are the following:

  • ParticleTex: It is the texture to apply to the point sprites.
  • Time: It defines the simulation time.
  • H: It defines the elapsed time between animation frames.
  • Accel: It is used to define the acceleration.
  • ParticleLifetime: It defines the length of time that a particle exists before it is recycled.

How to do it...

Use the following steps:

  1. Use the following code for your vertex shader:
    subroutine void RenderPassType();
    subroutine uniform RenderPassTypeRenderPass;
    
    layout (location = 0) in vec3 VertexPosition;
    layout (location = 1) in vec3 VertexVelocity;
    layout (location = 2) in float VertexStartTime;
    layout (location = 3) in vec3 VertexInitialVelocity;
    
    out vec3 Position;   // To transform feedback
    out vec3 Velocity;   // To transform feedback
    out float StartTime; // To transform feedback
    out float Transp;    // To fragment shader
    
    uniform float Time;  // Simulation time
    uniform float H;     // Elapsed time between frames
    uniform vec3 Accel;  // Particle acceleration
    uniform float ParticleLifetime;  // Particle lifespan
    
    uniform mat4 MVP;
    
    subroutine (RenderPassType)
    void update() {
    
      Position = VertexPosition;
      Velocity = VertexVelocity;
      StartTime = VertexStartTime;
    
      if( Time >= StartTime ) {
        float age = Time - StartTime;
        if( age >ParticleLifetime ) {
          // The particle is past its lifetime, recycle.
          Position = vec3(0.0);
          Velocity = VertexInitialVelocity;
          StartTime = Time;
        } else {
          // The particle is alive, update.
          Position += Velocity * H;
          Velocity += Accel * H;
        }
      }
    }
    
    subroutine (RenderPassType)
    void render() {
      float age = Time - VertexStartTime;
      Transp = 1.0 - age / ParticleLifetime;
      gl_Position = MVP * vec4(VertexPosition, 1.0);
    }
    
    void main()
    {
      // This will call either render() or update()
      RenderPass();
    }
  2. Use the following code for the fragment shader:
    uniform sampler2D ParticleTex;
    in float Transp;
    layout ( location = 0 ) out vec4 FragColor;
    
    void main()
    {
      FragColor = texture(ParticleTex, gl_PointCoord);
      FragColor.a *= Transp;
    }
  3. After compiling the shader program, but before linking, use the following code to set up the connection between vertex shader output variables and output buffers:
    const char * outputNames[] = { "Position", "Velocity", "StartTime" };
    glTransformFeedbackVaryings(progHandle, 3, outputNames, GL_SEPARATE_ATTRIBS);
  4. In the OpenGL render function, send the particle positions to the vertex shader for updating, and capture the results using transform feedback. The input to the vertex shader will come from buffer A, and the output will be stored in buffer B. During this pass we enable GL_RASTERIZER_DISCARD so that nothing is actually rendered to the framebuffer:
    // Select the subroutine for particle updating
    glUniformSubroutinesuiv(GL_VERTEX_SHADER, 1, &updateSub);
    
    // Set the uniforms: H and Time
    …
    
    // Disable rendering
    glEnable(GL_RASTERIZER_DISCARD);
    
    // Bind the feedback obj. for the buffers to be drawn
    glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback[drawBuf]);
    
    // Draw points from input buffer with transform feedback
    glBeginTransformFeedback(GL_POINTS);
    glBindVertexArray(particleArray[1-drawBuf]);
    glDrawArrays(GL_POINTS, 0, nParticles);
    glEndTransformFeedback();
  5. Render the particles at their updated positions using buffer B as input to the vertex shader:
    // Enable rendering
    glDisable(GL_RASTERIZER_DISCARD);
    
    glUniformSubroutinesuiv(GL_VERTEX_SHADER, 1, &renderSub);
    glClear( GL_COLOR_BUFFER_BIT );
    
    // Initialize uniforms for transform matrices if needed
    …
    
    // Draw the sprites from the feedback buffer
    glBindVertexArray(particleArray[drawBuf]);
    glDrawTransformFeedback(GL_POINTS, feedback[drawBuf]);
  6. Swap the purposes of the buffers:
    // Swap buffers
    drawBuf = 1 - drawBuf;

How it works...

There's quite a bit here to sort through. Let's start with the vertex shader.

The vertex shader is broken up into two subroutine functions. The update function is used during the first pass, and uses Euler's method to update the position and velocity of the particle. The render function is used during the second pass. It computes the transparency based on the age of the particle and sends the position and transparency along to the fragment shader.

The vertex shader has four output variables. The first three: Position, Velocity, and StartTime are used in the first pass to write to the feedback buffers. The fourth (Transp) is used during the second pass as input to the fragment shader.

The update function just updates the particle position and velocity using Euler's method unless the particle is not alive yet, or has passed its lifetime. If its age is greater than the lifetime of a particle, we recycle the particle by resetting its position to the origin, updating the particle's start time to the current time (Time), and setting its velocity to its original initial velocity (provided via input attribute VertexInitialVelocity).

The render function computes the particle's age and uses it to determine the transparency of the particle, assigning the result to the output variable Transp. It transforms the particle's position into clip coordinates and places the result in the built-in output variable gl_Position.

The fragment shader (step 2) is only utilized during the second pass. It colors the fragment based on the texture ParticleTex and the transparency delivered from the vertex shader (Transp).

The code segment in step 3 is placed prior to linking the shader program is responsible for setting up the correspondence between shader output variables and feedback buffers (buffers that are bound to indices of the GL_TRANSFORM_FEEDBACK_BUFFER binding point). The function glTransformFeedbackVaryings takes three arguments. The first is the handle to the shader program object. The second is the number of output variable names that will be provided. The third is an array of output variable names. The order of the names in this list corresponds to the indices of the feedback buffers. In this case, Position corresponds to index zero, Velocity to index one, and StartTime to index two. Check back to the previous code that creates our feedback buffer objects (the glBindBufferBase calls) to verify that this is indeed the case.

Note

glTransformFeedbackVaryings can be used to send data into an interleaved buffer instead (rather than separate buffers for each variable). Take a look at the OpenGL documentation for details.

The previous steps 4 through 6 describes how you might implement the render function within the main OpenGL program. In this example, there two important GLuint arrays: feedback and particleArray. They are each of size two and contain the handles to the two feedback buffer objects, and the two vertex array objects respectively. The variable drawBuf is just an integer used to alternate between the two sets of buffers. At any given frame, drawBuf will be either zero or one.

The code begins in step 4 by selecting the update subroutine to enable the update functionality within the vertex shader, and then setting the uniforms Time and H. The next call, glEnable(GL_RASTERIZER_DISCARD), turns rasterization off so that nothing is rendered during this pass. The call to glBindTransformFeedback selects the set of buffers corresponding to the variable drawBuf, as the target for the transform feedback output.

Before drawing the points (and thereby triggering our vertex shader), we call glBeginTransformFeedback to enable transform feedback. The argument is the kind of primitives that will be sent down the pipeline (in our case GL_POINTS). Output from the vertex (or geometry) shader will go to the buffers that are bound to the GL_TRANSFORM_FEEDBACK_BUFFER binding point until glEndTransformFeedback is called. In this case, we bind the vertex array corresponding to 1 - drawBuf (if drawBuf is 0, we use 1 and vice versa) and draw the particles.

At the end of the update pass (step 5), we re-enable rasterization with glEnable(GL_RASTERIZER_DISCARD), and move on to the render pass.

The render pass is straightforward; we just select the render subroutine, and draw the particles from the vertex array corresponding to drawBuf. However, instead of using glDrawArrays, we use the function glDrawTransformFeedback. The latter is used here because it is designed for use with transform feedback. A transform feedback object keeps track of the number of vertices that were written. A call to glDrawTransformFeedback takes the feedback object as the third parameter. It uses the number of vertices that were written to that object as the number of vertices to draw. In essence it is equivalent to calling glDrawArrays with a value of zero for the second parameter and the count taken from the transform feedback.

Finally, at the end of the render pass (step 6), we swap our buffers by setting drawBuf to 1 – drawBuf.

There's more...

You might be wondering why it was necessary to do this in two passes. Why couldn't we just keep the fragment shader active and do the render and update in the same pass? This is certainly possible for this example, and would be more efficient. However, I've chosen to demonstrate it this way because it is probably the more common way of doing this in general. Particles are usually just one part of a larger scene, and the particle update logic is not needed for most of the scene. Therefore, in most real-world situations it will make sense to do the particle update in a pass prior to the rendering pass so that the particle update logic can be decoupled from the rendering logic.

Using layout qualifiers

OpenGL 4.4 introduced layout qualifiers that make it possible to specify the relationship between the shader output variables and feedback buffers directly within the shader instead of using glTransformFeedbackVaryings. The layout qualifiers xfb_buffer, xfb_stride, and xfb_offset can be specified for each output variable that is to be used with transform feedback.

Querying transform feedback results

It is often useful to determine how many primitives were written during transform feedback pass. For example, if a geometry shader was active, the number of primitives written could be different than the number of primitives that were sent down the pipeline.

OpenGL provides a way to query for this information using query objects. To do so, start by creating a query object:

GLuint query;
glGenQueries(1, &query);

Then, prior to starting the transform feedback pass, start the counting process using the following command:

glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, query);

After the end of the transform feedback pass, call glEndQuery to stop counting:

glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);

Then we can get the number of primitives using the following code:

GLuintprimWritten;
glGetQueryObjectuiv(query, GL_QUERY_RESULT, &primWritten);
printf("Primitives written: %d
", primWritten);

Recycling particles

In this example, we recycled particles by resetting their position and initial velocity. This can cause the particles to begin to "clump" together over time. It would produce better results to generate a new random velocity and perhaps a random position (depending on the desired results). Unfortunately, there is currently no support for random number generation within shader programs. The solution might be to create your own random number generator function, use a texture with random values, or use a noise texture (see Chapter 8, Using Noise in Shaders).

See also

  • The Creating a particle fountain recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.72.78