Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Realidad Virtual

Curso 2023/2024

 

Práctica 12

Animación

 

Objetivos

 

El objetivo de esta práctica es representar objetos animados, es decir, objetos que no son rígidos. Para ello se utilizarán dos técnicas. La primera consiste en incorporar el tiempo como una variable uniforme en los shader e incluir en ellos las ecuaciones temporales para calcular su forma. La segunda consiste en calcular las posiciones en función de las calculadas en la iteración anterior. En este caso es necesario utilizar una característica de OpenGL conocida como Transform Feedback.

La práctica se divide en tres proyectos. El proyecto Project12a dibuja una superficie ondulatoria utilizando el tiempo como una variable uniforme y proramando las ecuaciones temporales en los shaders. El proyecto Project12b dibuja una fuente por medio de partículas de agua genradas mediante Transform Feedback. El proyecto Project12c utiliza la técnica de Transform Feedback para generar las partículas que forman las llamas de un fuego de campamento.

 

 

Código de la práctica

 

 

Project12a - Animación de una superficie basada en ecuaciones temporales

 

Una forma de representar superficies que no son rígidas consiste en utilizar ecuaciones temporales que describen la forma de la superficie en función del tiempo. Para ello hay que considerar el tiempo como una variable uniforme dentro del programa gráfico y utilizar las ecuaciones temporales`para calcular las posiciones de los puntos de la superficie en cada instante.

El proyecto Project12a desarrolla un ejemplo de este tipo de animación. Se trata de mostrar una superficie cuadrada que contenga ondulaciones que se desplacen a lo largo del tiempo. Si consideramos la superficie como un conjunto de primitivas cuyos vértices están situados en el plano XZ, la ondulación consiste en modificar la altura de los puntos (coordenada Y) siguiendo una ecuación de onda:

Ecuación de onda

En la ecuación anterior la constante A es la amplitud (valor máximo de la coordenada y), la constante k es el número de onda (que es igual a 2·π/λ, siendo λ la longitud de onda, es decir, la distancia entre dos máximos), la constante v es la velocidad con que se mueve la onda y t es el tiempo. El VertexShader es el encargado de calcular la coordenada y de cada vértice siguiendo la ecuación de onda, donde las diferentes constantes se introducen como variables uniformes. El vector normal se obtiene por medio de la derivada de la función de onda respecto de x.

#version 400

layout(location=0) in vec3 VertexPosition;

uniform float Time;     // The animation time
uniform float K;        // Wavenumber
uniform float Velocity; // Wave's velocity
uniform float Amp;      // Wave's amplitude

uniform mat4 MVP;
uniform mat4 ViewMatrix;
uniform mat4 ModelViewMatrix;

out vec3 Position;
out vec3 Normal;

void main()
{
  vec4 pos = vec4(VertexPosition, 1.0);

  // Translate the y coordinate
  float u = K * (pos.x - Velocity * Time);
  pos.y = Amp * sin( u );
  // Compute the normal vector
  vec4 n = normalize(vec4(-K*Amp*cos( u ), 1.0,0.0,0.0));

  vec4 n4 = ModelViewMatrix * n;
  vec4 v4 = ModelViewMatrix * pos;
  Normal = vec3(n4);
  Position = vec3(v4);
  gl_Position = MVP * pos;
}
 

En este ejemplo se ha considerado tan solo el modelo básico de iluminación de Phong para mostrar una superficie de un material sin textura. El FragmentShader es el siguiente.

#version 400

in vec3 Position;
in vec3 Normal;

uniform mat4 ViewMatrix;

struct LightInfo
{
  vec3 Ldir;
  vec3 La;
  vec3 Ld;
  vec3 Ls;
};
uniform LightInfo Light;

struct MaterialInfo
{
  vec3 Ka;
  vec3 Kd;
  vec3 Ks;
  float Shininess;
};
uniform MaterialInfo Material;

out vec4 FragColor;

vec3 ads()
{
  vec4 s4 = ViewMatrix*vec4(Light.Ldir, 0.0);
  vec3 n = normalize(Normal);
  vec3 v = normalize(-Position);
  vec3 s = normalize(-vec3(s4));
  vec3 r = reflect(-s, n);
  float dRate = max(dot(s, n), 0.0);
  float sRate = pow(max(dot(r, v), 0.0), Material.Shininess);
  vec3 ambient = Light.La * Material.Ka;
  vec3 difusse = Light.Ld * Material.Kd * dRate;
  vec3 specular = Light.Ls * Material.Ks * sRate;
  return ambient + difusse + specular;
}

void main()
{
  vec3 Color = ads();
  FragColor = vec4(Color,1.0);
}

Para modelar la superficie se ha creado una nueva clase que hemos denominado CGCloth. Esta clase se limita a crear una superficie cuadrada formada por 20.000 triángulos (cada lado de la superficie se ha dividido en 100 segmentos). La clase contiene también los valores de configuración de la ecuación de onda.

#pragma once

#include <GL/glew.h>
#include <glm/glm.hpp>
#include "CGMaterial.h"
#include "CGShaderProgram.h"

#define VERTEX_DATA 0
#define INDEX_DATA 1
#define NORMAL_DATA 2

class CGCloth {
private:
  GLfloat K;           // Wavenumber 
  GLfloat Velocity;    // Wave's velocity 
  GLfloat Amp;         // Wave's amplitude

  GLushort* indexes;   // Array of indexes 
  GLfloat* vertices;   // Array of vertices

  GLuint numFaces;     // Number of faces
  GLuint numVertices;  // Number of vertices
  GLuint VBO[2];
  GLuint VAO;

  glm::mat4 location; // Model matrix
  CGMaterial* material;

public:
  CGCloth(GLfloat l1, GLfloat l2);
  ~CGCloth();
  void InitBuffers();
  void SetMaterial(CGMaterial* mat);
  void ResetLocation();
  void Translate(glm::vec3 t);
  void Rotate(GLfloat angle, glm::vec3 axis);
  void Draw(CGShaderProgram* program, glm::mat4 projection, glm::mat4 view);
};

El código de la clase es similar al de las figuras que hemos considerado en prácticas anteriores. En este caso los vértices de la figura solo utilizan el atributo de posición. El método Draw() debe asignar los valores de las variables uniformes antes de lanzar el dibujo de las primitivas que forman la figura.

#include "CGCloth.h"
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

//
// FUNCIÓN: CGCloth::CGCloth()
//
// PROPÓSITO: Constructor de la figura
//
CGCloth::CGCloth(GLfloat width, GLfloat height)
{
  K = 0.4f; // Wavenumber 
  Velocity = 4.0f; // Wave's velocity 
  Amp = 5.0f; // Wave's amplitude

  int numRows = 101;
  int numColumns = 101;

  numVertices = numRows * numColumns;
  numFaces = 2 * (numRows - 1) * (numColumns - 1);

  indexes = new GLushort[numFaces * 3]; // Array of indexes 
  vertices = new GLfloat[3 * numVertices]; // Array of vertices

  for (int i = 0; i < numRows; i++)
    for (int j = 0; j < numColumns; j++)
    {
      vertices[3 * (i * numRows + j)] = 2 * width * i / (numRows - 1) - width;
      vertices[3 * (i * numRows + j) + 1] = 0.0f;
      vertices[3 * (i * numRows + j) + 2] = 2*height*j / (numColumns - 1) - height;
    }
  for (int i = 0; i < numRows - 1; i++)
    for (int j = 0; j < numColumns - 1; j++)
    {
      indexes[6 * (i * (numColumns - 1) + j)] = i * numColumns + j;
      indexes[6 * (i * (numColumns - 1) + j) + 1] = i * numColumns + j + 1;
      indexes[6 * (i * (numColumns - 1) + j) + 2] = (i + 1) * numColumns + j;

      indexes[6 * (i * (numColumns - 1) + j) + 3] = (i + 1) * numColumns + j;
      indexes[6 * (i * (numColumns - 1) + j) + 4] = i * numColumns + j + 1;
      indexes[6 * (i * (numColumns - 1) + j) + 5] = (i + 1) * numColumns + j + 1;
    }

  InitBuffers();
}

//
// FUNCIÓN: CGCloth::~CGCloth()
//
// PROPÓSITO: Destructor de la figura
//
CGCloth::~CGCloth()
{
  if (vertices != NULL) delete[] vertices;
  if (indexes != NULL) delete[] indexes;

  // Delete vertex buffer objects
  glDeleteBuffers(2, VBO);
  glDeleteVertexArrays(1, &VAO);
}

//
// FUNCIÓN: CGCloth::InitBuffers()
//
// PROPÓSITO: Crea el VAO y los VBO y almacena todos los datos en la GPU.
//
void CGCloth::InitBuffers()
{
  // Create the Vertex Array Object
  glGenVertexArrays(1, &VAO);
  glBindVertexArray(VAO);

  // Create the Vertex Buffer Objects
  glGenBuffers(2, VBO);

  // Copy data to video memory
  // Vertex data
  int buffsize = sizeof(GLfloat) * numVertices * 3;
  glBindBuffer(GL_ARRAY_BUFFER, VBO[VERTEX_DATA]);
  glBufferData(GL_ARRAY_BUFFER, buffsize, vertices, GL_STATIC_DRAW);

  // Indexes
  buffsize = sizeof(GLushort) * numFaces * 3;
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBO[INDEX_DATA]);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffsize, indexes, GL_STATIC_DRAW);

  delete[] vertices;
  delete[] indexes;

  vertices = NULL;
  indexes = NULL;

  glEnableVertexAttribArray(0); // Vertex position
  glBindBuffer(GL_ARRAY_BUFFER, VBO[VERTEX_DATA]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

  location = glm::mat4(1.0f);
}

//
// FUNCIÓN: CGCloth::SetMaterial(CGMaterial* m)
//
// PROPÓSITO: Asigna el material de la figura
//
void CGCloth::SetMaterial(CGMaterial* mat)
{
  material = mat;
}

//
// FUNCIÓN: CGCloth::ResetLocation()
//
// PROPÓSITO: Asigna la posición inicial de la figura 
//
void CGCloth::ResetLocation()
{
  location = glm::mat4(1.0f);
}

//
// FUNCIÓN: CGCloth::Translate(glm::vec3 t)
//
// PROPÓSITO: Añade un desplazamiento a la matriz de posición de la figura 
//
void CGCloth::Translate(glm::vec3 t)
{
  location = glm::translate(location, t);
}

//
// FUNCIÓN: CGCloth::Rotate(GLfloat angle, glm::vec3 axis)
//
// PROPÓSITO: Añade una rotación a la matriz de posición de la figura 
//
void CGCloth::Rotate(GLfloat angle, glm::vec3 axis)
{
  location = glm::rotate(location, glm::radians(angle), axis);
}

//
// FUNCIÓN: CGCloth::Draw(CGShaderProgram* program, glm::mat4 proj, glm::mat4 view)
//
// PROPÓSITO: Dibuja la figura
//
void CGCloth::Draw(CGShaderProgram* program, glm::mat4 projection, glm::mat4 view)
{
  program->SetUniformF("K", K);
  program->SetUniformF("Velocity", Velocity);
  program->SetUniformF("Amp", Amp);

  glm::mat4 mvp = projection * view * location;
  program->SetUniformMatrix4("MVP", mvp);
  program->SetUniformMatrix4("ViewMatrix", view);
  program->SetUniformMatrix4("ModelViewMatrix", view * location);
  material->SetUniforms(program);

  glBindVertexArray(VAO);
  glDrawElements(GL_TRIANGLES, numFaces * 3, GL_UNSIGNED_SHORT, NULL);
}

La escena, descrita en la clase CGScene, está formada por la luz y un objeto CGCloth. El modelo se describe en la clase CGModel. Respecto a otras prácticas, la única modificación del modelo es que añade un campo time que mide el tiempo. En cada actualización del modelo (método update() ) el tiempo se incrementa en 1/60 s. El método render() asigna el valor de la variable uniforme Time antes de llamar a la función de dibujo de la escena.

#define TIME_LAPSE 0.0166666f

//
// FUNCIÓN: CGModel::initialize(int, int)
//
// PROPÓSITO: Initializa el modelo 3D
//
void CGModel::initialize(int w, int h)
{
  // Crea el programa
  program = new CGShaderProgram(IDR_SHADER1, IDR_SHADER2, -1, -1, -1);
  // program = new CGShaderProgram("shaders/VertexShader.glsl", 
  //                               "shaders/FragmentShader.glsl", NULL, NULL, NULL);
  if (program->IsLinked() == GL_FALSE) return;
  program->Use();

  // Crea la cámara
  camera = new CGCamera();
  camera->SetPosition(0.0f, 5.0f, 100.0f);

  // Inicializa el tiempo
  time = 0.0f;

  // Crea la escena
  scene = new CGScene();

  // Asigna el viewport y el clipping volume
  resize(w, h);

  // Opciones de dibujo
  glEnable(GL_DEPTH_TEST);
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
  glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  program->SetUniformF("Time", time);

  glm::mat4 view = camera->ViewMatrix();
  scene->Draw(program, projection, view);
}

//
// FUNCIÓN: CGModel::update()
//
// PROPÓSITO: Anima la escena
//
void CGModel::update()
{
  time += TIME_LAPSE;
  camera->MoveFront();
}

... 

A continuación se muestra una captura del resultado.

Project12a

 

 

Project12b - Animación de una fuente basada en sistema de partículas con Transform Feedback

 

Los sistemas de partículas permiten modelar fenómenos difusos como fuego, humo, sprays o explosiones. Para ello se utiliza un conjunto de partículas que se representan como primitivas GL_POINTS o como pequeños cuadrados con texturas (point sprites). OpenGL permite generar estos cuadrados activando la opción GL_POINT_SPRITE y asignando el tamaño de los puntos con el comando glPointSize().

El comportamiento de las partículas se puede modelar mediante una función temporal. En ese caso los atributos de cada vértice corresponderían a los datos iniciales de la partícula (posición y velocidad inicial) y se calcularía la posición de cada instante en función del tiempo. Este esquema es válido cuando la ecuación de movimiento depende solo del tiempo y de valores constantes (aceleración, posición inicial y velocidad inicial). Sin embargo no permite considerar efectos como que la aceleración pueda cambiar, por ejemplo si se añade un efecto de viento en algún momento indeterminado. Para poder modelar estos efectos resulta más adecuado calcular la posición y velocidad de la partícula en un instante a partir de su posición y velocidad en un instante anterior por medio del método de Euler:

Método de Euler

Si almacenamos los valores de posición y velocidad como atributos de las partículas utilizando Vertex Buffer Objects para poder desarrollar el método de Euler es necesario que el renderizado permita almacenar los valores de los atributos y utilizarlos de nuevo en una iteración posterior. Esto se puede configurar en OpenGL por medio de Transform Feedback Objects. Para modelar el comportamiento de as partículas se utiliza una técnica conocida como buffer ping-pong porque realiza el cálculo en dos pasadas. En la primera pasada se utiliza el método de Euler para generar las nuevas posiciones y velocidades en función de las posiciones y velocidades anteriores En la segunda pasada se realiza el proceso de renderizado habitual considerando las nuevas posiciones y velocidades.

buffer ping-pong

El proyecto Project12b desarolla un ejemplo de esta técnica. Se trata de simular una fuente por medio de un conjunto de partículas representadas por medio de una textura en tono verde-azulado. Las partículas tienen una pequeña aceleración hacia abajo y se van difuminando (aumentando su nivel de transparencia) a medida que transcurre el tiempo. Parten inicialmente del origen de coordenadas de la escena y con una pequeña velocidad inicial vertical también aleatoria con un grado de abertura de 30 grados. Las partículas tienen un tiempo de vida y, una vez alcanzado, vuelven a surgir de nuevo en su posición inicial de manera que la fuente se sigue mostrando indefinidamente.

El VertexShader recibe como atributos de entrada la posición, la velocidad, el instante de nacimiento y la velocidad inicial de la partícula y genera como salida los nuevos valores de posición, velocidad e instante de nacimiento, así como el nivel de transparencia. En la primera pasada, descrita por la rutina update(), se calculan los nuevos valores de posición, velocidad e instante de nacimiento siguiendo el modelo de Euler. Estos valores se almacenarán en otros VBO. En la segunda pasada, descrita en la rutina render(), se calcula el nivel de transparencia y la posición en coordenadas clip.

#version 400

subroutine void RenderPassType();
subroutine uniform RenderPassType RenderPass;

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() {

  // Update position & velocity for next frame
  Position = VertexPosition;
  Velocity = VertexVelocity;
  StartTime = VertexStartTime;

  if( Time >= StartTime ) {
    float age = Time - StartTime;

    if( age > ParticleLifetime ) {
      // The particle is past it's lifetime, recycle.
      Position = vec3(0.0);
      Velocity = VertexInitialVelocity;
      StartTime += ParticleLifetime;
    } else {
      // The particle is alive, update.
      Position += Velocity * H;
      Velocity += Accel * H;
    }
  }
}

subroutine (RenderPassType)
void render() {
  float age = Time - VertexStartTime;
  Transp = 0.0;
  if( Time >= VertexStartTime ) Transp = 1.0 - age / ParticleLifetime;
  gl_Position = MVP * vec4(VertexPosition, 1.0);
}

void main()
{
  // This will call either render() or update()
  RenderPass();
}

El FragmentShader se limita a obtener el color del fragmento a partir de la textura asociada a la partícula. La entrada predefinida gl_PointCoord contiene la coordenada del pixel dentro del point sprite. El nivel de transparencia del fragmento se obtiene combinando el nivel leído de la textura con el nivel de transparencia de la partícula generado en el VertexShader.

#version 400

uniform sampler2D ParticleTex;

in float Transp;

layout ( location = 0 ) out vec4 FragColor;

void main()
{
  FragColor = texture(ParticleTex, gl_PointCoord);
  FragColor = vec4(mix( vec3(0,0,0), FragColor.xyz, Transp ), FragColor.a);
  FragColor.a *= Transp;
}

Para utilizar la opción de Transform Feedback es necesario indicar al programa gráfico cuales son las variables de salida del VertexShader que van a ser dirigidas a buffers de memoria en la tarjeta gráfica. Esto debe realizarse después de compilar los shaders y antes de linkar el programa. Para esto es necesario modificar la clase CGShaderProgram de manera que la carga de los shader se realice en el constructor, pero se incluyan métodos para configurar la opción de Transform Feddback y para linkar el programa. La modificaciones del fichero de cabecera de la clase CGShaderProgram serían las siguientes:

class CGShaderProgram {
  ...

public:
  ...
  GLboolean IsLinked();
  GLvoid Link();
  GLvoid Use();
  GLvoid SetTransformFeedback(const char* name[], int length);
};

Para utilizar la opción de Transform Feedback es necesario indicar al programa gráfico cuales son las variables de salida del VertexShader que van a ser dirigidas a buffers de memoria en la tarjeta gráfica. Esto debe realizarse después de compilar los shaders y antes de linkar el programa. Para esto es necesario modificar la clase CGShaderProgram de manera que la carga de los shader se realice en el constructor, pero se incluyan métodos para configurar la opción de Transform Feddback y para linkar el programa. La modificaciones del fichero de cabecera de la clase CGShaderProgram serían las siguientes:

//
// FUNCIÓN: CGShaderProgram::CGShaderProgram()
//
// PROPÓSITO: Crea un programa gráfico 
//
CGShaderProgram::CGShaderProgram(int vs, int fs, int gs, int tcs, int tes)
{
  vertexShader = NO_SHADER;
  fragmentShader = NO_SHADER;
  geometryShader = NO_SHADER;
  tessControlShader = NO_SHADER;
  tessEvaluationShader = NO_SHADER;
  linked = GL_FALSE;

  // Crea y compila los shaders
  if(vs != -1) vertexShader = CreateShader(GL_VERTEX_SHADER, vs);
  if(fs != -1) fragmentShader = CreateShader(GL_FRAGMENT_SHADER, fs);
  if(gs != -1) geometryShader = CreateShader(GL_GEOMETRY_SHADER, gs);
  if(tcs != -1) tessControlShader = CreateShader(GL_TESS_CONTROL_SHADER, tcs);
  if(tes != -1) tessEvaluationShader = CreateShader(GL_TESS_EVALUATION_SHADER, tes);

  //Crea el programa y carga los shaders
  program = glCreateProgram();
  if(vertexShader != NO_SHADER) glAttachShader(program, vertexShader);
  if(fragmentShader != NO_SHADER) glAttachShader(program, fragmentShader);
  if(geometryShader != NO_SHADER) glAttachShader(program, geometryShader);
  if(tessControlShader != NO_SHADER) glAttachShader(program, tessControlShader);
  if(tessEvaluationShader!=NO_SHADER) glAttachShader(program, tessEvaluationShader);
}

//
// FUNCIÓN: CGShaderProgram::CGShaderProgram()
//
// PROPÓSITO: Crea un programa gráfico 
//
CGShaderProgram::CGShaderProgram(const char* vs, const char* fs, const char* gs, 
                                 const char* tcs, const char* tes)
{
  vertexShader = NO_SHADER;
  fragmentShader = NO_SHADER;
  geometryShader = NO_SHADER;
  tessControlShader = NO_SHADER;
  tessEvaluationShader = NO_SHADER;
  linked = GL_FALSE;

  // Crea y compila los shaders
  if(vs != NULL) vertexShader = CreateShader(GL_VERTEX_SHADER, vs);
  if(fs != NULL) fragmentShader = CreateShader(GL_FRAGMENT_SHADER, fs);
  if(gs != NULL) geometryShader = CreateShader(GL_GEOMETRY_SHADER, gs);
  if(tcs != NULL) tessControlShader = CreateShader(GL_TESS_CONTROL_SHADER, tcs);
  if(tes != NULL) tessEvaluationShader=CreateShader(GL_TESS_EVALUATION_SHADER, tes);

  //Crea el programa y carga los shaders
  program = glCreateProgram();
  if(vertexShader != NO_SHADER) glAttachShader(program, vertexShader);
  if(fragmentShader != NO_SHADER) glAttachShader(program, fragmentShader);
  if(geometryShader != NO_SHADER) glAttachShader(program, geometryShader);
  if(tessControlShader != NO_SHADER) glAttachShader(program, tessControlShader);
  if(tessEvaluationShader!=NO_SHADER) glAttachShader(program, tessEvaluationShader);
}

//
// FUNCIÓN: CGShaderProgram::Link()
//
// PROPÓSITO: Enlaza los shaders para dejar el programa preparado para su uso
//
GLvoid CGShaderProgram::Link()
{
  glLinkProgram(program);

  GLint status;
  glGetProgramiv(program, GL_LINK_STATUS, &status);
  if (status == GL_FALSE)
  {
    linked = GL_FALSE;
    GLint logLength;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength);
    char* logInfo = (char*)malloc(sizeof(char) * logLength);
    GLsizei written;
    glGetProgramInfoLog(program, logLength, &written, logInfo);
    std::cout << logInfo << std::endl;
    return;
  }

  linked = GL_TRUE;
}

//
// FUNCIÓN: CGShaderProgram::SetTransformFeedback(const char* names[], int length)
//
// PROPÓSITO: Configura las variables a almacenar como TransformFeedback
//
GLvoid CGShaderProgram::SetTransformFeedback(const char* names[], int length)
{
  glTransformFeedbackVaryings(program, length, names, GL_SEPARATE_ATTRIBS);
}

...

El conjunto de partículas se ha descrito en la clase CGParticles. Esta clase define los VBOs de almacenamiento de los atributos de las partículas (VertexPosition, VertexVelocity, VertexStartTime y VertexInitialVelocity). Para programar la técnica de buffer ping-pong se utilizan dos VAOs, formados por 4 VBOs que contiene las dos versiones de los atributos. El atributo VertexInitialVelocity no sufre modificaciones así que no es necesario duplicarlo. También es necesario una pareja de TransformFeedbackObjects.

#pragma once

#include <GL/glew.h>
#include <glm/glm.hpp>
#include "CGShaderProgram.h"

class CGParticles
{
public:
  CGParticles(int n);
  ~CGParticles();
  void Draw(CGShaderProgram* program, glm::mat4 proj, glm::mat4 view);
  void Update();

private:
  int nParticles;
  GLuint textureId;
  GLfloat time;

  GLuint posBuf[2];
  GLuint velBuf[2];
  GLuint startTime[2];
  GLuint initVel;
  GLuint particleArray[2];
  GLuint feedback[2];

  int drawBuf;

  void InitTexture(const char* filename);
  void InitTexture(int idr);
  void InitBuffers();
  float randFloat();
};

Al construir las partículas se crean los buffers y se calculan sus valores iniciales. Las velocidades iniciales se calculan de forma aleatoria. El instante de nacimiento se calcula con una tasa fija de manera que se estén produciendo partículas de manera constante. Los VBOs se definen en este caso como GL_DYNAMIC_COPY ya que su contenido se va a modificar. Además de los VertexArrayObjects es necesario crear y configurar los TransformFeedbackObjects. El método Draw() se encarga de lanzar el dibujo de las partículas realizando dos pasadas. En la primera pasada se desactiva la rasterización para que solo se ejecute el VertexShader, se selecciona el TFO y se activa la opción de Transform Feedback con el comando glBeginTransformFeedback(). En este paso se escoge la rutina update() para dirigir la salida del VertexShader a los segundos VBOs. En la segunda pasada hay que volver a activar la rasterización, desactivar la opción de Transform Feedback, seleccionar la rutina render() y lanzar el comando de dibujo indicando que los atributos de los vértices se lean de un TFO mediante el comando glDrawTransformFeedback().

#include "CGParticles.h"
#include <GL/glew.h>
#include <Windows.h>
#include <FreeImage.h>
#include "resource.h"

#ifndef TIME_LAPSE
#define TIME_LAPSE 0.0166666f
#endif

//
// FUNCIÓN: CGParticles::CGParticles(n)
//
// PROPÓSITO: Crea el conjunto de partículas
//
CGParticles::CGParticles(int n)
{
  time = 0.0f;
  nParticles = n;
  drawBuf = 1;

  InitBuffers();
  InitTexture(IDR_IMAGE1);
  // InitTexture("textures/bluewater.png");
}

//
// FUNCIÓN: void CGParticles::InitTexture(const char* filename)
//
// PROPÓSITO: Carga una textura desde un fichero
//
void CGParticles::InitTexture(const char* filename)
{
  FREE_IMAGE_FORMAT format = FreeImage_GetFileType(filename, 0);
  FIBITMAP* bitmap = FreeImage_Load(format, filename);
  FIBITMAP* pImage = FreeImage_ConvertTo32Bits(bitmap);
  int nWidth = FreeImage_GetWidth(pImage);
  int nHeight = FreeImage_GetHeight(pImage);

  glGenTextures(1, &textureId);
  glBindTexture(GL_TEXTURE_2D, textureId);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, nWidth, nHeight,
               0, GL_BGRA, GL_UNSIGNED_BYTE, (void*)FreeImage_GetBits(pImage));

  FreeImage_Unload(pImage);
}

//
// FUNCIÓN: void CGParticles::InitTexture(int idr)
//
// PROPÓSITO: Carga una textura a partir de un recurso
//
void CGParticles::InitTexture(int idr)
{
  HRSRC handle = FindResource(NULL, MAKEINTRESOURCE(idr), L"IMAGE");
  HGLOBAL hGlobal = LoadResource(NULL, handle);
  LPCTSTR rsc_ptr = static_cast<LPCTSTR>(LockResource(hGlobal));
  DWORD mem_size = SizeofResource(NULL, handle);
  BYTE* mem_buffer = (BYTE*)malloc((mem_size) * sizeof(BYTE));
  memcpy(mem_buffer, rsc_ptr, mem_size * sizeof(BYTE));
  FreeResource(hGlobal);

  FIMEMORY* hmem = FreeImage_OpenMemory(mem_buffer, mem_size * sizeof(BYTE));
  FREE_IMAGE_FORMAT fif = FreeImage_GetFileTypeFromMemory(hmem, 0);
  FIBITMAP* check = FreeImage_LoadFromMemory(fif, hmem, 0);
  FIBITMAP* pImage = FreeImage_ConvertTo32Bits(check);
  int nWidth = FreeImage_GetWidth(pImage);
  int nHeight = FreeImage_GetHeight(pImage);

  glGenTextures(1, &textureId);
  glBindTexture(GL_TEXTURE_2D, textureId);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, nWidth, nHeight,
               0, GL_BGRA, GL_UNSIGNED_BYTE, (void*)FreeImage_GetBits(pImage));

  FreeImage_Unload(pImage);
  FreeImage_CloseMemory(hmem);
  free(mem_buffer);
}

//
// FUNCIÓN: CGParticles::InitBuffers()
//
// PROPÓSITO: Crea el conjunto de partículas
//
void CGParticles::InitBuffers()
{
  GLfloat radius = 3.0f;

  // Fill the first position and first velocity buffers with random values
  GLfloat* firstVel = new GLfloat[nParticles * 3];
  GLfloat* firstPos = new GLfloat[nParticles * 3];
  for (int i = 0; i < nParticles * 3; i += 3) {
    GLfloat theta = glm::radians(30.0f * randFloat());
    GLfloat phi = glm::radians(360.0f * randFloat());
    GLfloat velocity = 1.25f + 0.25f * randFloat();

    firstPos[i] = 0.0f;
    firstPos[i + 1] = 0.0f;
    firstPos[i + 2] = 0.0f;

    firstVel[i] = sinf(theta) * cosf(phi) * velocity; 
    firstVel[i+1] = cosf(theta) * velocity; 
    firstVel[i+2] = sinf(theta) * sinf(phi) * velocity;
  }

  GLfloat* start = new GLfloat[nParticles];
  float time = 0.0f;
  float rate = 0.00075f;
  for (int i = 0; i < nParticles; i++) {
    start[i] = time;
    time += rate;
  }

  // Generate the buffers
  glGenBuffers(2, posBuf); // position buffers
  glGenBuffers(2, velBuf); // velocity buffers
  glGenBuffers(2, startTime); // Start time buffers
  glGenBuffers(1, &initVel);  // Initial velocity buffer (only need one)

  // Allocate space for all buffers
  int size = nParticles * 3 * sizeof(float);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[0]);
  glBufferData(GL_ARRAY_BUFFER, size, firstPos, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[1]);
  glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_DYNAMIC_COPY); 
  glBindBuffer(GL_ARRAY_BUFFER, velBuf[0]);
  glBufferData(GL_ARRAY_BUFFER, size, firstVel, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, velBuf[1]);
  glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, initVel);
  glBufferData(GL_ARRAY_BUFFER, size, firstVel, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, startTime[0]);
  glBufferData(GL_ARRAY_BUFFER, nParticles * sizeof(float), start, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, startTime[1]);
  glBufferData(GL_ARRAY_BUFFER, nParticles * sizeof(float), NULL, GL_DYNAMIC_COPY);

  delete[] firstPos;
  delete[] firstVel;
  delete[] start;

  // Create vertex arrays for each set of buffers
  glGenVertexArrays(2, particleArray);

  // Set up particle array 0
  glBindVertexArray(particleArray[0]);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[0]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(0);

  glBindBuffer(GL_ARRAY_BUFFER, velBuf[0]);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(1);

  glBindBuffer(GL_ARRAY_BUFFER, startTime[0]);
  glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(2);

  glBindBuffer(GL_ARRAY_BUFFER, initVel);
  glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(3);

  // Set up particle array 1
  glBindVertexArray(particleArray[1]);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[1]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(0);

  glBindBuffer(GL_ARRAY_BUFFER, velBuf[1]);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(1);

  glBindBuffer(GL_ARRAY_BUFFER, startTime[1]);
  glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(2);

  glBindBuffer(GL_ARRAY_BUFFER, initVel);
  glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(3);

  glBindVertexArray(0);

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

  glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
}

//
// FUNCIÓN: CGParticles::~CGParticles()
//
// PROPÓSITO: Destruye el conjunto de partículas
//
CGParticles::~CGParticles()
{
  // Destruye buffers
  glDeleteBuffers(2, posBuf); 
  glDeleteBuffers(2, velBuf); 
  glDeleteBuffers(2, startTime); 
  glDeleteBuffers(1, &initVel); 

  // Destruye los VAOs
  glDeleteVertexArrays(2, particleArray);

  // Destruye los TFOs
  glDeleteTransformFeedbacks(2, feedback);

  // Destruye la textura
  glDeleteTextures(1, &textureId);
}

//
// FUNCIÓN: CGParticles::Draw(CGShaderProgram* prog, glm::mat4 proj, glm::mat4 view)
//
// PROPÓSITO: Dibuja el conjunto de partículas
//
void CGParticles::Draw(CGShaderProgram* program, glm::mat4 proj, glm::mat4 view)
{
  // Opciones de dibujo
  glDisable(GL_DEPTH_TEST);
  glEnable(GL_POINT_SPRITE);

  // Asigna las variables uniformes
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, textureId);
  program->SetUniformI("ParticleTex", 0);
  program->SetUniformF("ParticleLifetime", 1.2f);
  program->SetUniformVec3("Accel", glm::vec3(0.0f, -0.05f, 0.0f));
  program->SetUniformMatrix4("MVP", proj*view);
  program->SetUniformF("Time", time);
  program->SetUniformF("H", TIME_LAPSE);

  // Update pass
  program->SetUniformSubroutine(GL_VERTEX_SHADER, "update");

  glEnable(GL_RASTERIZER_DISCARD);

  glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback[drawBuf]);

  glBeginTransformFeedback(GL_POINTS);
  glBindVertexArray(particleArray[1 - drawBuf]);
  glDrawArrays(GL_POINTS, 0, nParticles);
  glEndTransformFeedback();

  glDisable(GL_RASTERIZER_DISCARD);

  // Render pass
  program->SetUniformSubroutine(GL_VERTEX_SHADER,"render");

  glBindVertexArray(particleArray[drawBuf]);
  glDrawTransformFeedback(GL_POINTS, feedback[drawBuf]);

  // Swap buffers
  drawBuf = 1 - drawBuf;

  // Reactiva el test de profundidad
  glDisable(GL_POINT_SPRITE);
  glEnable(GL_DEPTH_TEST);
}

//
// FUNCIÓN: CGParticles::Update()
//
// PROPÓSITO: Actualiza el contador de tiempo
//
void CGParticles::Update()
{
  time += TIME_LAPSE;
}

//
// FUNCIÓN: CGParticles::randFloat()
//
// PROPÓSITO: Genera un float aleatorio entre 0.0 y 1.0
//
float CGParticles::randFloat() 
{
  return ((float)rand() / RAND_MAX);
}

La clase CGModel contiene en este caso el objeto CGParticles en vez de una escena. Para obtener el resultado que se muestra a continuación se ha activado la opción de mezcla (para poder considerar la transparencia de las partículas), la opción de point sprites y se ha asignado un tamaño de punto adecuado. La creación del programa gráfico se realiza en tres pasos: creación, configuración de Transform Feedback y linkado.

#include "CGModel.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <GLFW/glfw3.h>
#include <iostream>
#include "CGCamera.h"
#include "CGParticles.h"
#include "resource.h"

//
// FUNCIÓN: CGModel::initialize(int, int)
//
// PROPÓSITO: Initializa el modelo 3D
//
void CGModel::initialize(int w, int h)
{
  // Crea el programa gráfico para dibujar el fuego
  particlesProgram = new CGShaderProgram(IDR_SHADER1, IDR_SHADER2, -1, -1, -1);
  // particlesProgram = new CGShaderProgram("shaders/ParticlesVertexShader.glsl", 
  //                                        "shaders/ParticlesFragmentShader.glsl",
  //                                        NULL, NULL, NULL);
  const char* particlesVars[] = { "Position", "Velocity", "StartTime" };
  particlesProgram->SetTransformFeedback(particlesVars, 3);
  particlesProgram->Link();
  if (particlesProgram->IsLinked() == GL_FALSE) return;

  // Crea la cámara
  camera = new CGCamera();
  camera->SetPosition(0.0f, 5.0f, 30.0f);

  // Crea las partículas
  particles = new CGParticles(4000);

  // Asigna el viewport y el clipping volume
  resize(w, h);

  // Opciones de dibujo
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_CULL_FACE);
  glEnable(GL_BLEND);
  glFrontFace(GL_CCW);
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glEnable(GL_POINT_SPRITE);
  glPointSize(30.0f);
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}

//
// FUNCIÓN: CGModel::finalize()
//
// PROPÓSITO: Libera los recursos del modelo 3D
//
void CGModel::finalize()
{
  delete camera;
  delete particles;
  delete particlesProgram;
}

//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glm::mat4 view = camera->ViewMatrix();

  particlesProgram->Use();
  particles->Draw(particlesProgram, projection, view);
}

//
// FUNCIÓN: CGModel::update()
//
// PROPÓSITO: Anima la escena
//
void CGModel::update()
{
  particles->Update();
  camera->MoveFront();
}

El resultado de la aplicación gráfica es el siguiente

Project12b

 

 

Project12c - Animación de un fuego basado en sistema de partículas con Transform Feedback

 

En este último proyecto se va a utilizar un sistema de partículas para modelar un fuego de campamento. La leña del fuego se va a generar por medio de un modelo 3D diseñado por el artista gráfico Zelad y obtenido de la plataforma TurboSquid. Sobre este modelo vamos a dibujar una llamas generadas mediante un sistema de partículas. En este caso la textura de cada partícula está formada por valores anaranjados.

Campfire

Campfire

Para generar la aplicación se va a utilizar un modelo formado por una escena, que incluye el objeto Campfire y la luz,  un conjunto de partículas y una cámara. La clase CGModel debe incluir también los programas gráficos utilizados por la escena y el conjunto de partículas. El fichero de cabecera de la clase es el siguiente:

class CGModel
{
public:
  void initialize(int w, int h);
  void finalize();
  void render();
  void update();
  void key_pressed(int key);
  void mouse_button(int button, int action);
  void mouse_move(double xpos, double ypos);
  void resize(int w, int h);

private:
  CGShaderProgram* sceneProgram;
  CGShaderProgram* particlesProgram;
  CGScene* scene;
  CGParticles* particles;
  CGCamera* camera;
  glm::mat4 projection;
};

El constructor del modelo debe inicializar los programas gráficos y crear los objetos CGScene y CGParticles. El programa gráfico de la escena es el habitual para representar objetos con texturas y con el modelo de iluminación de Phong. El programa gráfico de las partículas es el comentado en el apartado anterior. En este caso hay que configurar la opción de Transform Feedback en este programa gráfico.

//
// FUNCIÓN: CGModel::initialize(int, int)
//
// PROPÓSITO: Initializa el modelo 3D
//
void CGModel::initialize(int w, int h)
{
  // Crea el programa gráfico para dibujar la escena
  sceneProgram = new CGShaderProgram(IDR_SHADER1, IDR_SHADER2, -1, -1, -1);
  // sceneProgram = new CGShaderProgram("shaders/VertexShader.glsl", 
  //                             "shaders/FragmentShader.glsl", NULL, NULL, NULL);
  sceneProgram->Link();
  if (sceneProgram->IsLinked() == GL_FALSE) return;

  // Crea el programa gráfico para dibujar el fuego
  particlesProgram = new CGShaderProgram(IDR_SHADER3, IDR_SHADER4, -1, -1, -1);
  // particlesProgram = new CGShaderProgram("shaders/ParticlesVertexShader.glsl", 
  //                    "shaders/ParticlesFragmentShader.glsl", NULL, NULL, NULL);
  const char* particlesVars[] = { "Position", "Velocity", "StartTime" };
  particlesProgram->SetTransformFeedback(particlesVars, 3);
  particlesProgram->Link();
  if (particlesProgram->IsLinked() == GL_FALSE) return;

  // Crea la cámara
  camera = new CGCamera();
  camera->SetPosition(0.0f, 5.0f, 30.0f);

  // Crea la escena
  scene = new CGScene();

  // Crea el fuego
  particles = new CGParticles(10000);

  // Asigna el viewport y el clipping volume
  resize(w, h);

  // Opciones de dibujo
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_CULL_FACE);
  glEnable(GL_BLEND);
  glFrontFace(GL_CCW);
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glEnable(GL_POINT_SPRITE);
  glPointSize(40.0f);
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}

//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glm::mat4 view = camera->ViewMatrix();

  sceneProgram->Use();
  scene->Draw(sceneProgram, projection, view);

  particlesProgram->Use();
  particles->Draw(particlesProgram, projection, view);
}

...

La simulación del fuego requiere realizar algunas modificaciones en la definición de la clase CGParticles. En este caso las partículas deben tener una aceleración ligeramente positiva sobre el eje Y de manera que tiendan a elevarse mientras se van difuminando. La posición y velocidad inicial se ha programado de manera que las partículas nazcan aproximadamente sobre la pila de leña y tengan una cierta velocidad radial. Estas posiciones y velocidades iniciales se generan de forma aleatoria formando un círculo respecto al centro de la pila de leña. Las partículas se dibujan como point sprites. El tamaño de los puntos se ha configurado para que se tenga en cuenta la distancia a la que se observa el fuego.

//
// FUNCIÓN: CGParticles::CGParticles(n)
//
// PROPÓSITO: Crea el conjunto de partículas
//
CGParticles::CGParticles(int n)
{
  time = 0.0f;
  nParticles = n;
  drawBuf = 1;

  InitBuffers();
  InitTexture("textures/fire.png");
}

//
// FUNCIÓN: CGParticles::InitBuffers()
//
// PROPÓSITO: Crea el conjunto de partículas
//
void CGParticles::InitBuffers()
{
  GLfloat radius = 3.0f;

  // Fill the first position and first velocity buffers with random values
  GLfloat* firstVel = new GLfloat[nParticles * 3];
  GLfloat* firstPos = new GLfloat[nParticles * 3];
  for (int i = 0; i < nParticles * 3; i += 3) {
    GLfloat x = 2.0f * randFloat() - 1.0f;
    GLfloat z = 2.0f * randFloat() - 1.0f;
    while (x * x + z * z > 1.0f)
    {
      x = 2.0f * randFloat() - 1.0f;
      z = 2.0f * randFloat() - 1.0f;
    }
    firstPos[i] = radius * x;
    firstPos[i + 1] = (radius +1.0f) * (1.0f - glm::sqrt(x*x+z*z)) + 1.0f;
    firstPos[i + 2] = radius*z;

    firstVel[i] = x / 5.0f;
    firstVel[i + 1] = glm::mix(0.1f, 0.5f, randFloat());
    firstVel[i + 2] = z / 5.0f;
  }

  GLfloat* start = new GLfloat[nParticles];
  float time = 0.0f;
  float rate = 0.0009f;
  for (int i = 0; i < nParticles; i+=2) {
    start[i] = time;
    start[i+1] = time;
    time += rate;
  }

  // Generate the buffers
  glGenBuffers(2, posBuf); // position buffers
  glGenBuffers(2, velBuf); // velocity buffers
  glGenBuffers(2, startTime); // Start time buffers
  glGenBuffers(1, &initPos); // Initial position buffer (only need one)
  glGenBuffers(1, &initVel); // Initial velocity buffer (only need one)


  // Allocate space for all buffers
  int size = nParticles * 3 * sizeof(float);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[0]);
  glBufferData(GL_ARRAY_BUFFER, size, firstPos, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[1]);
  glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, velBuf[0]);
  glBufferData(GL_ARRAY_BUFFER, size, firstVel, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, velBuf[1]);
  glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, initPos);
  glBufferData(GL_ARRAY_BUFFER, size, firstPos, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, initVel);
  glBufferData(GL_ARRAY_BUFFER, size, firstVel, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, startTime[0]);
  glBufferData(GL_ARRAY_BUFFER, nParticles * sizeof(float), start, GL_DYNAMIC_COPY);
  glBindBuffer(GL_ARRAY_BUFFER, startTime[1]);
  glBufferData(GL_ARRAY_BUFFER, nParticles * sizeof(float), NULL, GL_DYNAMIC_COPY);

  delete[] firstPos;
  delete[] firstVel;
  delete[] start;

  // Create vertex arrays for each set of buffers
  glGenVertexArrays(2, particleArray);

  // Set up particle array 0
  glBindVertexArray(particleArray[0]);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[0]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(0);

  glBindBuffer(GL_ARRAY_BUFFER, velBuf[0]);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(1);

  glBindBuffer(GL_ARRAY_BUFFER, startTime[0]);
  glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(2);

  glBindBuffer(GL_ARRAY_BUFFER, initPos);
  glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(3);

  glBindBuffer(GL_ARRAY_BUFFER, initVel);
  glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(4);

  // Set up particle array 1
  glBindVertexArray(particleArray[1]);
  glBindBuffer(GL_ARRAY_BUFFER, posBuf[1]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(0);

  glBindBuffer(GL_ARRAY_BUFFER, velBuf[1]);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(1);

  glBindBuffer(GL_ARRAY_BUFFER, startTime[1]);
  glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(2);

  glBindBuffer(GL_ARRAY_BUFFER, initPos);
  glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(3);

  glBindBuffer(GL_ARRAY_BUFFER, initVel);
  glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray(4);

  glBindVertexArray(0);

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

  glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
}

//
// FUNCIÓN: CGParticles::Draw(CGShaderProgram* prog, glm::mat4 proj, glm::mat4 view)
//
// PROPÓSITO: Dibuja el conjunto de partículas
//
void CGParticles::Draw(CGShaderProgram* program, glm::mat4 proj, glm::mat4 view)
{
  glm::vec4 dist = view * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
  GLfloat d = glm::clamp(glm::abs(dist.z), 5.0f, 200.0f);
  GLfloat f = (200.0f - d) / (200.0f - 5.0f);
  GLfloat psize = 30.0f * f + 1.0f;

  // Opciones de dibujo
  glDisable(GL_DEPTH_TEST);
  glEnable(GL_POINT_SPRITE);
  glPointSize(psize);

  // Asigna las variables uniformes
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, textureId);
  program->SetUniformI("ParticleTex", 0);
  program->SetUniformF("ParticleLifetime", 1.2f);
  program->SetUniformVec3("Accel", glm::vec3(0.0f, 0.03f, 0.0f));
  program->SetUniformMatrix4("MVP", proj*view);
  program->SetUniformF("Time", time);
  program->SetUniformF("H", TIME_LAPSE);

  // Update pass
  program->SetUniformSubroutine(GL_VERTEX_SHADER, "update");

  glEnable(GL_RASTERIZER_DISCARD);

  glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback[drawBuf]);

  glBeginTransformFeedback(GL_POINTS);
  glBindVertexArray(particleArray[1 - drawBuf]);
  glDrawArrays(GL_POINTS, 0, nParticles);
  glEndTransformFeedback();

  glDisable(GL_RASTERIZER_DISCARD);

  // Render pass
  program->SetUniformSubroutine(GL_VERTEX_SHADER,"render");

  glBindVertexArray(particleArray[drawBuf]);
  glDrawTransformFeedback(GL_POINTS, feedback[drawBuf]);

  // Swap buffers
  drawBuf = 1 - drawBuf;

  // Reactiva el test de profundidad
  glDisable(GL_POINT_SPRITE);
  glEnable(GL_DEPTH_TEST);
}

//
// FUNCIÓN: CGParticles::Update()
//
// PROPÓSITO: Actualiza el contador de tiempo
//
void CGParticles::Update()
{
  time += TIME_LAPSE;
}

...

El resultado de la aplicación gráfica es el siguiente

Project12b