In this tutorial we will cover how to implement animated particles using OpenGL 4.0, GLSL, and C++.
We will be combining the information from tutorial 38: Particle Systems, and from tutorial 33: Fire.
There are two parts required to implement animated particles.
The first part is the animation within the particle system.
And the second part is the animation within the pixel shader for each individual particle.
We will discuss the particle system animation first.
In tutorial 38: Particle Systems we simply had the particles fall downwards.
However, most particle systems require more advanced animations to assist in creating a particular effect.
For example, an explosion would require fast particles moving in random directions from a central emission point.
The explosion particles would also have gravity affecting them and the acceleration would have a sharp decline after being emitted originally.
And so, you can see the particle system animation plays a crucial role in creating the desired effect.
The second part of animating particles was demonstrating in tutorial 33: Fire.
In that tutorial we used a single quad and achieved an animated fire effect that looked very realistic using a RGB texture, an alpha map, and an alpha mask.
However, the difference in this tutorial is that we will combine a large number of particles that are all being individually animated at different rates.
This will then create a combined effect.
For example, we could now just do small flames licks on each particle, but then combine all of them using the particle system to create the same kind of fire effect we had in tutorial 33.
The difference being though is that this is a far more extensible solution that can create millions of different effects.
For this tutorial we will demonstrate animated particles by creating an illuminated looking blue smoke effect that moves in a circular pattern.
To implement this blue smoke effect, we will first start with the particle system and create a particle quad that moves in a circle.
In the case of this tutorial, we will use 400 quads and emit them around in a circle shape using the circle formula.
The resulting animated particle system will then look like the following:
Then for the second part of the animated particles effect we will use a single RGB texture with an alpha map.
The alpha map will be used to manipulate the RGB portion of the texture each frame.
So just like the fire tutorial the alpha map will scroll each frame and be used to expose or remove portions of the RGB texture to create the animated color portion of the particle.
We won't go as far as using multiple alpha maps, distortion, and other things discussed in the fire tutorial, but I will leave that for you to add later.
Now to create the texture I have used terrain height maps to create the RGB portion and the alpha portion as seen here:
Using alpha maps that have lots of interesting black to white transitions are what makes up the majority of the animation for these particles.
Multiplying different scaled alpha maps can also do the same thing.
The final part of the technique is that we scroll the alpha portion of the texture differently for every single particle.
That way all 400 particles will be animating slightly differently which hides the repetition and creates a larger combined effect from many different and unique particles.
For this tutorial we will create the following effect (which looks way better in motion):
One thing you will notice is that the overlapping particles do not add up to create a cumulative shimmering white effect.
The reason is that we will not be using additive blending in this tutorial like we did in the previous particle tutorial.
We will instead be using premultiplied alpha which results in the actual RGB colors of the texture being displayed and allows us to maintain the specific color effect we are looking for.
Now when you are animating particles there will be a lot of experimentation required to get a good-looking effect.
One of the ways to speed up testing changes is that we put all of our particle system variables in a text file.
Then we modify the rendering program to dynamically read that text file while the program is running.
This way we can change a parameter in the text file and then click a button to see the change live without stopping the program.
For this tutorial I will put the parameters for this effect into a text file called particle_config_01.txt.
And whenever the R key is pressed on the keyboard it will reload the newly saved file and apply the updated parameters live.
particle_config_01.txt
Particle Count: 400
Particles Per Second: 100.0
Particle Size: 0.5
Particle Life Time: 2.0
Texture 1: ../Engine/data/ice003.tga
Framework
We will use the same framework as we did in the particle systems tutorial.
We will start the code section by looking at the modified particle system.
We have made changes to it that feed into the particle shader, so reviewing the particle system first will make the particle shader changes easier to understand.
Particlesystemclass.h
The ParticleSystemClass has been changed entirely to support animated particles.
Since each particle needs to animate independently, we need to change how the particle system is working.
////////////////////////////////////////////////////////////////////////////////
// Filename: particlesystemclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _PARTICLESYSTEMCLASS_H_
#define _PARTICLESYSTEMCLASS_H_
//////////////
// INCLUDES //
//////////////
#include <fstream>
using namespace std;
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "textureclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class Name: ParticleSystemClass
////////////////////////////////////////////////////////////////////////////////
class ParticleSystemClass
{
private:
The VertexType has been modified to carry the information needed for each particle to animate individually.
We store that information in the lifeTime, scroll1X, and scroll1Y which we will call data1 in the shader.
Any other animation data that each particle will need in the vertex shader should be stored here.
struct VertexType
{
float x, y, z;
float tu, tv;
float lifeTime, scroll1X, scroll1Y;
};
The ParticleType has also changed to support the individual animation information that each particle will need to maintain about its current state.
struct ParticleType
{
float positionX, positionY, positionZ;
bool active;
float lifeTime;
float scroll1X, scroll1Y;
};
public:
ParticleSystemClass();
ParticleSystemClass(const ParticleSystemClass&);
~ParticleSystemClass();
bool Initialize(OpenGLClass*, char*);
void Shutdown();
void Frame(float);
void Render();
We have added a Reload function so that the particle system can dynamically load its settings from the config file and apply the settings real time.
bool Reload();
private:
bool LoadParticleConfiguration();
void InitializeParticleSystem();
void ShutdownParticleSystem();
void EmitParticles(float);
void UpdateParticles(float);
void KillParticles();
void CopyParticle(int, int);
void InitializeBuffers();
void ShutdownBuffers();
void RenderBuffers();
void UpdateBuffers();
bool LoadTexture();
void ReleaseTexture();
private:
OpenGLClass* m_OpenGLPtr;
ParticleType* m_particleList;
VertexType* m_vertices;
TextureClass* m_Texture;
unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
int m_vertexCount, m_indexCount;
We have a number of new private member variables for the updated particle system.
char m_configFilename[256];
int m_maxParticles;
float m_particlesPerSecond;
float m_particleSize;
float m_particleLifeTime;
char m_textureFilename[256];
float m_accumulatedTime;
int m_currentParticleCount;
};
#endif
Particlesystemclass.cpp
////////////////////////////////////////////////////////////////////////////////
// Filename: particlesystemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "particlesystemclass.h"
ParticleSystemClass::ParticleSystemClass()
{
m_OpenGLPtr = 0;
m_particleList = 0;
m_vertices = 0;
m_Texture = 0;
}
ParticleSystemClass::ParticleSystemClass(const ParticleSystemClass& other)
{
}
ParticleSystemClass::~ParticleSystemClass()
{
// Release the pointer to the OpenGL object.
m_OpenGLPtr = 0;
}
bool ParticleSystemClass::Initialize(OpenGLClass* OpenGL, char* configFilename)
{
bool result;
// Store a pointer to the OpenGL object.
m_OpenGLPtr = OpenGL;
The Initialize function will now start by storing the config file's name and loading the configuration from file instead of the hard coded variables from the previous particle tutorial.
// Keep a copy of the config file name for loading the particle configuration, and also for mid-app reloading.
strcpy(m_configFilename, configFilename);
// Load the particle configuration file to set all the particle parameters for rendering.
result = LoadParticleConfiguration();
if(!result)
{
return false;
}
// Initialize the particle system.
InitializeParticleSystem();
// Create the buffers that will be used to render the particles with.
InitializeBuffers();
// Load the texture that is used for the particles.
result = LoadTexture();
if(!result)
{
return false;
}
return true;
}
void ParticleSystemClass::Shutdown()
{
// Release the texture used for the particles.
ReleaseTexture();
// Release the buffers.
ShutdownBuffers();
// Release the particle system.
ShutdownParticleSystem();
return;
}
void ParticleSystemClass::Frame(float frameTime)
{
// Release old particles.
KillParticles();
// Emit new particles.
EmitParticles(frameTime);
// Update the position of the particles.
UpdateParticles(frameTime);
// Update the dynamic vertex buffer with the new position of each particle.
UpdateBuffers();
return;
}
void ParticleSystemClass::Render()
{
// Set the texture for the particles.
m_Texture->SetTexture(m_OpenGLPtr, 0);
// Put the vertex and index buffers on the graphics pipeline to prepare them for drawing.
RenderBuffers();
return;
}
The new LoadParticleConfiguration function will load all the particle parameters from a text file.
This makes it easy to define parameters and swap different configuration files to try different effects.
bool ParticleSystemClass::LoadParticleConfiguration()
{
ifstream fin;
int i;
char input;
// Open the particle configuration file.
fin.open(m_configFilename);
if(fin.fail())
{
return false;
}
// Read up to the value of the particle count and read it in.
fin.get(input);
while(input != ':')
{
fin.get(input);
}
fin >> m_maxParticles;
// Read up to the value of the particle per second and read it in.
fin.get(input);
while(input != ':')
{
fin.get(input);
}
fin >> m_particlesPerSecond;
// Read up to the value of the particle size and read it in.
fin.get(input);
while(input != ':')
{
fin.get(input);
}
fin >> m_particleSize;
// Read up to the value of the particle life time and read it in.
fin.get(input);
while(input != ':')
{
fin.get(input);
}
fin >> m_particleLifeTime;
// Read up to the filename of the first texture and read it in.
fin.get(input);
while(input != ':')
{
fin.get(input);
}
fin.get(input);
i=0;
fin.get(input);
while(input != '\n')
{
m_textureFilename[i] = input;
i++;
fin.get(input);
}
m_textureFilename[i-1] = '\0';
// Close the file.
fin.close();
return true;
}
void ParticleSystemClass::InitializeParticleSystem()
{
int i;
When we build our particle list, we now use parameters from the config file such as m_maxParticles.
// Create the particle list.
m_particleList = new ParticleType[m_maxParticles];
// Initialize the particle list.
for(i=0; i<m_maxParticles; i++)
{
m_particleList[i].active = false;
}
// Clear the initial accumulated time for the particle per second emission rate.
m_accumulatedTime = 0.0f;
// Initialize the current particle count to zero since none are emitted yet.
m_currentParticleCount = 0;
return;
}
void ParticleSystemClass::ShutdownParticleSystem()
{
// Release the particle list.
if(m_particleList)
{
delete [] m_particleList;
m_particleList = 0;
}
return;
}
void ParticleSystemClass::EmitParticles(float frameTime)
{
float centerX, centerY, radius, positionX, positionY, positionZ, scroll1X, scroll1Y;
float emitterOrigin[3];
int index, i, j;
bool emitParticle, found;
static float angle = 0.0f;
The EmitParticles function has been changed to emit particles in a circle pattern.
We use the circle formula paired with the frame time to emit particles around in a circle.
// Set the center of the circle.
centerX = 0.0f;
centerY = 0.0f;
// Set the radius of the circle.
radius = 1.0f;
// Update the angle each frame to move any generated particle origin position along the circumference of the circle each frame.
angle += frameTime * 2.0f;
// Calculate the origin that the particle should be emitted on the circle's circumference.
emitterOrigin[0] = centerX + radius * sin(angle);
emitterOrigin[1] = centerY + radius * cos(angle);
emitterOrigin[2] = 0.0f;
// Increment the accumulated time that is used to determine when to emit a particle next.
m_accumulatedTime += frameTime;
// Set emit particle to false for now.
emitParticle = false;
// Check if it is time to emit a new particle or not.
if(m_accumulatedTime > (1.0f / m_particlesPerSecond))
{
m_accumulatedTime = 0.0f;
emitParticle = true;
}
// If there are particles to emit then emit one per frame.
if((emitParticle == true) && (m_currentParticleCount < (m_maxParticles - 1)))
{
m_currentParticleCount++;
When we emit particles, we use the emitter origin which will be placed somewhere on the circumference of the circle based on the frame time and angle.
positionX = emitterOrigin[0];
positionY = emitterOrigin[1];
positionZ = emitterOrigin[2];
Also, when we emit a particle, we give it a random scroll value to sample the alpha map from.
This provides a huge variation in appearance.
And it also hides the repetition since each particle will be sampling from a different scrolling location.
// Create a random X scrolling positive value.
scroll1X = (((float)rand() - (float)rand())/RAND_MAX);
if(scroll1X < 0.0f)
{
scroll1X *= -1.0f;
}
// Set the Y scroll to the same value.
scroll1Y = scroll1X;
// Now since the particles need to be rendered from back to front for blending we have to sort the particle array.
// We will sort using Z depth so we need to find where in the list the particle should be inserted.
index = 0;
found = false;
while(!found)
{
if((m_particleList[index].active == false) || (m_particleList[index].positionZ < positionZ))
{
found = true;
}
else
{
index++;
}
}
// Now that we know the location to insert into we need to copy the array over by one position from the index to make room for the new particle.
i = m_currentParticleCount;
j = i - 1;
while(i != index)
{
CopyParticle(i, j);
i--;
j--;
}
The new particle will use all the new parameters for this tutorial such as particle life time and scrolling.
// Now insert the newly emitted particle into the particle array in the correct depth order.
m_particleList[index].positionX = positionX;
m_particleList[index].positionY = positionY;
m_particleList[index].positionZ = positionZ;
m_particleList[index].active = true;
m_particleList[index].lifeTime = m_particleLifeTime;
m_particleList[index].scroll1X = scroll1X;
m_particleList[index].scroll1Y = scroll1Y;
}
return;
}
The UpdateParticles function has also changed.
We no longer update the position, as all particles will stay where they are.
But we instead update the life time of the particle to fade it out further and further each frame.
We also update the scrolling each frame for each particle to create the individualized animation effect.
void ParticleSystemClass::UpdateParticles(float frameTime)
{
int i;
// Each frame we update all the particles using the frame time.
for(i=0; i<m_currentParticleCount; i++)
{
// Negate the life time of the particle each frame.
m_particleList[i].lifeTime = m_particleList[i].lifeTime - frameTime;
// Update the scrolling position of each particle each frame.
m_particleList[i].scroll1X = m_particleList[i].scroll1X + (frameTime * 0.5f);
if(m_particleList[i].scroll1X > 1.0f)
{
m_particleList[i].scroll1X -= 1.0f;
}
m_particleList[i].scroll1Y = m_particleList[i].scroll1Y + (frameTime * 0.5f);
if(m_particleList[i].scroll1Y > 1.0f)
{
m_particleList[i].scroll1Y -= 1.0f;
}
}
return;
}
The KillParticles now removes particles based on the life time instead of the position.
Since life time is also used to fade out particles over time in the shader, they will be fully faded out by the time the life time reaches zero.
void ParticleSystemClass::KillParticles()
{
int i, j;
// Kill all the particles that have a life time that is now zero.
for(i=0; i<m_maxParticles; i++)
{
if((m_particleList[i].active == true) && (m_particleList[i].lifeTime <= 0.0f))
{
m_particleList[i].active = false;
m_currentParticleCount--;
// Now shift all the live particles back up the array to erase the destroyed particle and keep the array sorted correctly.
for(j=i; j<m_maxParticles-1; j++)
{
CopyParticle(j, j+1);
}
}
}
return;
}
I have added a CopyParticle function since there is always a lot of copying particles.
And instead of duplicating this code in multiple places, you now have a single function to do the work.
This also removes the possibility of bugs when you add or remove particle parameters as there is now only a single location to modify the parameters that get copied.
void ParticleSystemClass::CopyParticle(int dst, int src)
{
m_particleList[dst].positionX = m_particleList[src].positionX;
m_particleList[dst].positionY = m_particleList[src].positionY;
m_particleList[dst].positionZ = m_particleList[src].positionZ;
m_particleList[dst].active = m_particleList[src].active;
m_particleList[dst].lifeTime = m_particleList[src].lifeTime;
m_particleList[dst].scroll1X = m_particleList[src].scroll1X;
m_particleList[dst].scroll1Y = m_particleList[src].scroll1Y;
return;
}
void ParticleSystemClass::InitializeBuffers()
{
unsigned int* indices;
int i;
// Set the maximum number of vertices in the vertex array.
m_vertexCount = m_maxParticles * 6;
// Set the maximum number of indices in the index array.
m_indexCount = m_vertexCount;
// Create the vertex array.
m_vertices = new VertexType[m_vertexCount];
// Create the index array.
indices = new unsigned int[m_indexCount];
// Initialize vertex array to zeros at first.
memset(m_vertices, 0, (sizeof(VertexType) * m_vertexCount));
// Initialize the index array.
for(i=0; i<m_indexCount; i++)
{
indices[i] = i;
}
// Allocate an OpenGL vertex array object.
m_OpenGLPtr->glGenVertexArrays(1, &m_vertexArrayId);
// Bind the vertex array object to store all the buffers and vertex attributes we create here.
m_OpenGLPtr->glBindVertexArray(m_vertexArrayId);
// Generate an ID for the vertex buffer.
m_OpenGLPtr->glGenBuffers(1, &m_vertexBufferId);
// Bind the vertex buffer and load the vertex data into the vertex buffer.
m_OpenGLPtr->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
m_OpenGLPtr->glBufferData(GL_ARRAY_BUFFER, m_vertexCount * sizeof(VertexType), m_vertices, GL_DYNAMIC_DRAW);
We will call the three vertex array attribute Data1.
This is where lifeTime, scroll1X, and scroll1Y will be placed.
// Enable the three vertex array attributes.
m_OpenGLPtr->glEnableVertexAttribArray(0); // Vertex position.
m_OpenGLPtr->glEnableVertexAttribArray(1); // Texture coordinates.
m_OpenGLPtr->glEnableVertexAttribArray(2); // Data1
// Specify the location and format of the position portion of the vertex buffer.
m_OpenGLPtr->glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(VertexType), 0);
// Specify the location and format of the texture coordinates portion of the vertex buffer.
m_OpenGLPtr->glVertexAttribPointer(1, 2, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (3 * sizeof(float)));
// Specify the location and format of the data1 portion of the vertex buffer.
m_OpenGLPtr->glVertexAttribPointer(2, 3, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (5 * sizeof(float)));
// Generate an ID for the index buffer.
m_OpenGLPtr->glGenBuffers(1, &m_indexBufferId);
// Bind the index buffer and load the index data into it.
m_OpenGLPtr->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId);
m_OpenGLPtr->glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount* sizeof(unsigned int), indices, GL_STATIC_DRAW);
// Now that the buffers have been loaded we can release the array data.
delete [] indices;
indices = 0;
return;
}
void ParticleSystemClass::ShutdownBuffers()
{
// Release the vertex array object.
m_OpenGLPtr->glBindVertexArray(0);
m_OpenGLPtr->glDeleteVertexArrays(1, &m_vertexArrayId);
// Release the vertex buffer.
m_OpenGLPtr->glBindBuffer(GL_ARRAY_BUFFER, 0);
m_OpenGLPtr->glDeleteBuffers(1, &m_vertexBufferId);
// Release the index buffer.
m_OpenGLPtr->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
m_OpenGLPtr->glDeleteBuffers(1, &m_indexBufferId);
// Release the vertices.
if(m_vertices)
{
delete [] m_vertices;
m_vertices = 0;
}
return;
}
void ParticleSystemClass::RenderBuffers()
{
// Bind the vertex array object that stored all the information about the vertex and index buffers.
m_OpenGLPtr->glBindVertexArray(m_vertexArrayId);
// Render the vertex buffer using the index buffer.
glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, 0);
return;
}
In the UpdateBuffers function we now have a lifeTime, scroll1X, and scroll1Y (data1) vertex parameter since each particle will now require its own individual scrolling and life time
parameters that will be updated each frame.
This is critical in allowing us to have the individual randomized animation for every single particle.
void ParticleSystemClass::UpdateBuffers()
{
int index, i;
float lifeTime, scroll1X, scroll1Y;
void* dataPtr;
// Initialize vertex array to zeros at first.
memset(m_vertices, 0, (sizeof(VertexType) * m_vertexCount));
// Now build the vertex array from the particle list array. Each particle is a quad made out of two triangles.
index = 0;
for(i=0; i<m_currentParticleCount; i++)
{
// Get the life time and scroll for the current particle. This will be set in the data1 portion of the vertex.
lifeTime = m_particleList[i].lifeTime / m_particleLifeTime;
scroll1X = m_particleList[i].scroll1X;
scroll1Y = m_particleList[i].scroll1Y;
// Bottom left.
m_vertices[index].x = m_particleList[i].positionX - m_particleSize;
m_vertices[index].y = m_particleList[i].positionY - m_particleSize;
m_vertices[index].z = m_particleList[i].positionZ;
m_vertices[index].tu = 0.0f;
m_vertices[index].tv = 0.0f;
m_vertices[index].lifeTime = lifeTime;
m_vertices[index].scroll1X = scroll1X;
m_vertices[index].scroll1Y = scroll1Y;
index++;
// Top left.
m_vertices[index].x = m_particleList[i].positionX - m_particleSize;
m_vertices[index].y = m_particleList[i].positionY + m_particleSize;
m_vertices[index].z = m_particleList[i].positionZ;
m_vertices[index].tu = 0.0f;
m_vertices[index].tv = 1.0f;
m_vertices[index].lifeTime = lifeTime;
m_vertices[index].scroll1X = scroll1X;
m_vertices[index].scroll1Y = scroll1Y;
index++;
// Bottom right.
m_vertices[index].x = m_particleList[i].positionX + m_particleSize;
m_vertices[index].y = m_particleList[i].positionY - m_particleSize;
m_vertices[index].z = m_particleList[i].positionZ;
m_vertices[index].tu = 1.0f;
m_vertices[index].tv = 0.0f;
m_vertices[index].lifeTime = lifeTime;
m_vertices[index].scroll1X = scroll1X;
m_vertices[index].scroll1Y = scroll1Y;
index++;
// Bottom right.
m_vertices[index].x = m_particleList[i].positionX + m_particleSize;
m_vertices[index].y = m_particleList[i].positionY - m_particleSize;
m_vertices[index].z = m_particleList[i].positionZ;
m_vertices[index].tu = 1.0f;
m_vertices[index].tv = 0.0f;
m_vertices[index].lifeTime = lifeTime;
m_vertices[index].scroll1X = scroll1X;
m_vertices[index].scroll1Y = scroll1Y;
index++;
// Top left.
m_vertices[index].x = m_particleList[i].positionX - m_particleSize;
m_vertices[index].y = m_particleList[i].positionY + m_particleSize;
m_vertices[index].z = m_particleList[i].positionZ;
m_vertices[index].tu = 0.0f;
m_vertices[index].tv = 1.0f;
m_vertices[index].lifeTime = lifeTime;
m_vertices[index].scroll1X = scroll1X;
m_vertices[index].scroll1Y = scroll1Y;
index++;
// Top right.
m_vertices[index].x = m_particleList[i].positionX + m_particleSize;
m_vertices[index].y = m_particleList[i].positionY + m_particleSize;
m_vertices[index].z = m_particleList[i].positionZ;
m_vertices[index].tu = 1.0f;
m_vertices[index].tv = 1.0f;
m_vertices[index].lifeTime = lifeTime;
m_vertices[index].scroll1X = scroll1X;
m_vertices[index].scroll1Y = scroll1Y;
index++;
}
// Bind the vertex buffer.
m_OpenGLPtr->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
// Get a pointer to the buffer's actual location in memory.
dataPtr = m_OpenGLPtr->glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// Copy the vertex data into memory.
memcpy(dataPtr, m_vertices, (sizeof(VertexType) * m_vertexCount));
// Unlock the vertex buffer.
m_OpenGLPtr->glUnmapBuffer(GL_ARRAY_BUFFER);
return;
}
bool ParticleSystemClass::LoadTexture()
{
bool result;
// Create and initialize the texture object.
m_Texture = new TextureClass;
result = m_Texture->Initialize(m_OpenGLPtr, m_textureFilename, true);
if(!result)
{
return false;
}
return true;
}
void ParticleSystemClass::ReleaseTexture()
{
// Release the texture object.
if(m_Texture)
{
m_Texture->Shutdown();
delete m_Texture;
m_Texture = 0;
}
return;
}
The new Reload function will allow us to shut down and restart the particle system whenever we press the R key.
So, we start and run the program, and then while the program is running, we change and save the config text file, and then press R and reload the entire particle system live.
bool ParticleSystemClass::Reload()
{
bool result;
// Release all of the data.
Shutdown();
// Reload all of the data.
result = LoadParticleConfiguration();
if(!result)
{
return false;
}
InitializeParticleSystem();
InitializeBuffers();
result = LoadTexture();
if(!result)
{
return false;
}
return true;
}
Particle.vs
The particle shader has been changed for this tutorial to accommodate animated particles.
////////////////////////////////////////////////////////////////////////////////
// Filename: particle.vs
////////////////////////////////////////////////////////////////////////////////
#version 400
The input variables for the vertex now receive the life time, and two scrolling floats in the data1 vec3.
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec2 inputTexCoord;
in vec3 inputData1;
We send out data1 and a newly created texCoords1 in the PixelInputType to the pixel shader.
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec2 texCoord;
out vec4 pixColor;
out vec3 data1;
out vec2 texCoords1;
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
float scroll1X, scroll1Y;
// Calculate the position of the vertex against the world, view, and projection matrices.
gl_Position = vec4(inputPosition, 1.0f) * worldMatrix;
gl_Position = gl_Position * viewMatrix;
gl_Position = gl_Position * projectionMatrix;
// Store the texture coordinates for the pixel shader.
texCoord = inputTexCoord;
Here we are sending the data1 vec3 to the pixel shader. Although we only need the lifeTime portion.
// Store the particle data for the pixel shader.
data1 = inputData1;
Just like the fire shader we get the scrolling values and then create a new set of texture sampling coordinates for the pixel shader.
These are stored in texCoords1.
The main difference from the fire shader is that the scrolling is per particle now, and not a global for all particles in the shader.
So, we will now have a unique scroll for every single particle.
// Get the scrolling values from the data1.yz portion of the vertex input data.
scroll1X = data1.y;
scroll1Y = data1.z;
// Calculate the first texture scroll speed texture sampling coordinates.
texCoords1.x = inputTexCoord.x - scroll1X;
texCoords1.y = inputTexCoord.y + scroll1Y;
}
Particle.ps
////////////////////////////////////////////////////////////////////////////////
// Filename: particle.ps
////////////////////////////////////////////////////////////////////////////////
#version 400
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;
in vec4 pixColor;
in vec3 data1;
in vec2 texCoords1;
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform sampler2D shaderTexture;
////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
vec4 textureColor;
float alpha;
float intensity;
Although we are using a single texture, we will sample the RGB with input.tex, and the alpha portion with input.texCoords1.
This way the RGB stays put, and the alpha will scroll giving us a unique looking particle.
Also, it is important that the texture is set to wrap for this to work correctly.
// Sample the pixel color from the texture using the sampler at this texture coordinate location.
textureColor = texture(shaderTexture, texCoord);
// Sample the alpha value using the translated texture coordinates instead of the regular coordinates.
alpha = texture(shaderTexture, texCoords1).a;
Here we combine the RGB and the alpha to create the animated result.
// Modify the color based on the scrolling alpha values.
textureColor.rgb = textureColor.rgb * alpha;
One extra step is to get the life time out of data1 and use it as the intensity of the particle.
This way as the particle gets older, it will fade out gradually.
// Get the intensity value from the life time of the particle.
intensity = data1.r;
// As the particle gets older use the life time as intensity to fade out the particle.
textureColor.rgb = textureColor.rgb * intensity;
// Manually set the alpha.
textureColor.a = 1.0f;
// Return the resulting color.
outputColor = textureColor;
}
Particleshaderclass.h
////////////////////////////////////////////////////////////////////////////////
// Filename: particleshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _PARTICLESHADERCLASS_H_
#define _PARTICLESHADERCLASS_H_
//////////////
// INCLUDES //
//////////////
#include <iostream>
using namespace std;
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class name: ParticleShaderClass
////////////////////////////////////////////////////////////////////////////////
class ParticleShaderClass
{
public:
ParticleShaderClass();
ParticleShaderClass(const ParticleShaderClass&);
~ParticleShaderClass();
bool Initialize(OpenGLClass*);
void Shutdown();
bool SetShaderParameters(float*, float*, float*);
private:
bool InitializeShader(char*, char*);
void ShutdownShader();
char* LoadShaderSourceFile(char*);
void OutputShaderErrorMessage(unsigned int, char*);
void OutputLinkerErrorMessage(unsigned int);
private:
OpenGLClass* m_OpenGLPtr;
unsigned int m_vertexShader;
unsigned int m_fragmentShader;
unsigned int m_shaderProgram;
};
#endif
Particleshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
// Filename: particleshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "particleshaderclass.h"
ParticleShaderClass::ParticleShaderClass()
{
m_OpenGLPtr = 0;
}
ParticleShaderClass::ParticleShaderClass(const ParticleShaderClass& other)
{
}
ParticleShaderClass::~ParticleShaderClass()
{
}
bool ParticleShaderClass::Initialize(OpenGLClass* OpenGL)
{
char vsFilename[128];
char psFilename[128];
bool result;
// Store the pointer to the OpenGL object.
m_OpenGLPtr = OpenGL;
// Set the location and names of the shader files.
strcpy(vsFilename, "../Engine/particle.vs");
strcpy(psFilename, "../Engine/particle.ps");
// Initialize the vertex and pixel shaders.
result = InitializeShader(vsFilename, psFilename);
if(!result)
{
return false;
}
return true;
}
void ParticleShaderClass::Shutdown()
{
// Shutdown the shader.
ShutdownShader();
// Release the pointer to the OpenGL object.
m_OpenGLPtr = 0;
return;
}
bool ParticleShaderClass::InitializeShader(char* vsFilename, char* fsFilename)
{
const char* vertexShaderBuffer;
const char* fragmentShaderBuffer;
int status;
// Load the vertex shader source file into a text buffer.
vertexShaderBuffer = LoadShaderSourceFile(vsFilename);
if(!vertexShaderBuffer)
{
return false;
}
// Load the fragment shader source file into a text buffer.
fragmentShaderBuffer = LoadShaderSourceFile(fsFilename);
if(!fragmentShaderBuffer)
{
return false;
}
// Create a vertex and fragment shader object.
m_vertexShader = m_OpenGLPtr->glCreateShader(GL_VERTEX_SHADER);
m_fragmentShader = m_OpenGLPtr->glCreateShader(GL_FRAGMENT_SHADER);
// Copy the shader source code strings into the vertex and fragment shader objects.
m_OpenGLPtr->glShaderSource(m_vertexShader, 1, &vertexShaderBuffer, NULL);
m_OpenGLPtr->glShaderSource(m_fragmentShader, 1, &fragmentShaderBuffer, NULL);
// Release the vertex and fragment shader buffers.
delete [] vertexShaderBuffer;
vertexShaderBuffer = 0;
delete [] fragmentShaderBuffer;
fragmentShaderBuffer = 0;
// Compile the shaders.
m_OpenGLPtr->glCompileShader(m_vertexShader);
m_OpenGLPtr->glCompileShader(m_fragmentShader);
// Check to see if the vertex shader compiled successfully.
m_OpenGLPtr->glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, &status);
if(status != 1)
{
// If it did not compile then write the syntax error message out to a text file for review.
OutputShaderErrorMessage(m_vertexShader, vsFilename);
return false;
}
// Check to see if the fragment shader compiled successfully.
m_OpenGLPtr->glGetShaderiv(m_fragmentShader, GL_COMPILE_STATUS, &status);
if(status != 1)
{
// If it did not compile then write the syntax error message out to a text file for review.
OutputShaderErrorMessage(m_fragmentShader, fsFilename);
return false;
}
// Create a shader program object.
m_shaderProgram = m_OpenGLPtr->glCreateProgram();
// Attach the vertex and fragment shader to the program object.
m_OpenGLPtr->glAttachShader(m_shaderProgram, m_vertexShader);
m_OpenGLPtr->glAttachShader(m_shaderProgram, m_fragmentShader);
The new inputData1 is bind location is set here.
// Bind the shader input variables.
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 0, "inputPosition");
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 1, "inputTexCoord");
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 2, "inputData1");
// Link the shader program.
m_OpenGLPtr->glLinkProgram(m_shaderProgram);
// Check the status of the link.
m_OpenGLPtr->glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &status);
if(status != 1)
{
// If it did not link then write the syntax error message out to a text file for review.
OutputLinkerErrorMessage(m_shaderProgram);
return false;
}
return true;
}
void ParticleShaderClass::ShutdownShader()
{
// Detach the vertex and fragment shaders from the program.
m_OpenGLPtr->glDetachShader(m_shaderProgram, m_vertexShader);
m_OpenGLPtr->glDetachShader(m_shaderProgram, m_fragmentShader);
// Delete the vertex and fragment shaders.
m_OpenGLPtr->glDeleteShader(m_vertexShader);
m_OpenGLPtr->glDeleteShader(m_fragmentShader);
// Delete the shader program.
m_OpenGLPtr->glDeleteProgram(m_shaderProgram);
return;
}
char* ParticleShaderClass::LoadShaderSourceFile(char* filename)
{
FILE* filePtr;
char* buffer;
long fileSize, count;
int error;
// Open the shader file for reading in text modee.
filePtr = fopen(filename, "r");
if(filePtr == NULL)
{
return 0;
}
// Go to the end of the file and get the size of the file.
fseek(filePtr, 0, SEEK_END);
fileSize = ftell(filePtr);
// Initialize the buffer to read the shader source file into, adding 1 for an extra null terminator.
buffer = new char[fileSize + 1];
// Return the file pointer back to the beginning of the file.
fseek(filePtr, 0, SEEK_SET);
// Read the shader text file into the buffer.
count = fread(buffer, 1, fileSize, filePtr);
if(count != fileSize)
{
return 0;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
return 0;
}
// Null terminate the buffer.
buffer[fileSize] = '\0';
return buffer;
}
void ParticleShaderClass::OutputShaderErrorMessage(unsigned int shaderId, char* shaderFilename)
{
long count;
int logSize, error;
char* infoLog;
FILE* filePtr;
// Get the size of the string containing the information log for the failed shader compilation message.
m_OpenGLPtr->glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);
// Increment the size by one to handle also the null terminator.
logSize++;
// Create a char buffer to hold the info log.
infoLog = new char[logSize];
// Now retrieve the info log.
m_OpenGLPtr->glGetShaderInfoLog(shaderId, logSize, NULL, infoLog);
// Open a text file to write the error message to.
filePtr = fopen("shader-error.txt", "w");
if(filePtr == NULL)
{
cout << "Error opening shader error message output file." << endl;
return;
}
// Write out the error message.
count = fwrite(infoLog, sizeof(char), logSize, filePtr);
if(count != logSize)
{
cout << "Error writing shader error message output file." << endl;
return;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
cout << "Error closing shader error message output file." << endl;
return;
}
// Notify the user to check the text file for compile errors.
cout << "Error compiling shader. Check shader-error.txt for error message. Shader filename: " << shaderFilename << endl;
return;
}
void ParticleShaderClass::OutputLinkerErrorMessage(unsigned int programId)
{
long count;
FILE* filePtr;
int logSize, error;
char* infoLog;
// Get the size of the string containing the information log for the failed shader compilation message.
m_OpenGLPtr->glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logSize);
// Increment the size by one to handle also the null terminator.
logSize++;
// Create a char buffer to hold the info log.
infoLog = new char[logSize];
// Now retrieve the info log.
m_OpenGLPtr->glGetProgramInfoLog(programId, logSize, NULL, infoLog);
// Open a file to write the error message to.
filePtr = fopen("linker-error.txt", "w");
if(filePtr == NULL)
{
cout << "Error opening linker error message output file." << endl;
return;
}
// Write out the error message.
count = fwrite(infoLog, sizeof(char), logSize, filePtr);
if(count != logSize)
{
cout << "Error writing linker error message output file." << endl;
return;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
cout << "Error closing linker error message output file." << endl;
return;
}
// Pop a message up on the screen to notify the user to check the text file for linker errors.
cout << "Error linking shader program. Check linker-error.txt for message." << endl;
return;
}
In SetShaderParameters we are only going to set the matrices and the texture.
Since all particles will be unique, they won't be using any global particle parameters anymore, and will require everything sent in through the inputData1 vec3 in our vertex shader input.
bool ParticleShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix)
{
float tpWorldMatrix[16], tpViewMatrix[16], tpProjectionMatrix[16];
int location;
// Transpose the matrices to prepare them for the shader.
m_OpenGLPtr->MatrixTranspose(tpWorldMatrix, worldMatrix);
m_OpenGLPtr->MatrixTranspose(tpViewMatrix, viewMatrix);
m_OpenGLPtr->MatrixTranspose(tpProjectionMatrix, projectionMatrix);
// Install the shader program as part of the current rendering state.
m_OpenGLPtr->glUseProgram(m_shaderProgram);
// Set the world matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "worldMatrix");
if(location == -1)
{
cout << "World matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpWorldMatrix);
// Set the view matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "viewMatrix");
if(location == -1)
{
cout << "View matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpViewMatrix);
// Set the projection matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "projectionMatrix");
if(location == -1)
{
cout << "Projection matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpProjectionMatrix);
// Set the texture in the pixel shader to use the data from the first texture unit.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "shaderTexture");
if(location == -1)
{
cout << "Shader texture not set." << endl;
}
m_OpenGLPtr->glUniform1i(location, 0);
return true;
}
Openglclass.cpp
In the previous particle tutorial, we were using additive blending to create an interesting particle stream effect.
However, in this tutorial we don't want particles adding their colors together when they are bunched up.
We instead want the RGB value of the particle to come out as the resulting color.
To do so we use what is called premultiplied alpha.
This is what most particle systems use for their blending equation to get the best results when blending with the majority of surfaces.
So, in this tutorial we will modify the OpenGLClass and add an additional enable blending function.
void OpenGLClass::EnableParticleAlphaBlending()
{
// Enable alpha blending.
glEnable(GL_BLEND);
// Premultiplied alpha blend state: result = src.RGB + (dest.RGB * (1 - src.A))
glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ZERO, GL_ONE_MINUS_SRC_ALPHA);
return;
}
Applicationclass.h
////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _APPLICATIONCLASS_H_
#define _APPLICATIONCLASS_H_
/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_NEAR = 0.3f;
const float SCREEN_DEPTH = 1000.0f;
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "openglclass.h"
#include "cameraclass.h"
#include "timerclass.h"
#include "particlesystemclass.h"
#include "particleshaderclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class Name: ApplicationClass
////////////////////////////////////////////////////////////////////////////////
class ApplicationClass
{
public:
ApplicationClass();
ApplicationClass(const ApplicationClass&);
~ApplicationClass();
bool Initialize(Display*, Window, int, int);
void Shutdown();
bool Frame(InputClass*);
private:
bool Render();
private:
OpenGLClass* m_OpenGL;
CameraClass* m_Camera;
TimerClass* m_Timer;
ParticleSystemClass* m_ParticleSystem;
ParticleShaderClass* m_ParticleShader;
};
#endif
Applicationclass.cpp
////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "applicationclass.h"
ApplicationClass::ApplicationClass()
{
m_OpenGL = 0;
m_Camera = 0;
m_Timer = 0;
m_ParticleSystem = 0;
m_ParticleShader = 0;
}
ApplicationClass::ApplicationClass(const ApplicationClass& other)
{
}
ApplicationClass::~ApplicationClass()
{
}
bool ApplicationClass::Initialize(Display* display, Window win, int screenWidth, int screenHeight)
{
char configFilename[128];
bool result;
// Create and initialize the OpenGL object.
m_OpenGL = new OpenGLClass;
result = m_OpenGL->Initialize(display, win, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, VSYNC_ENABLED);
if(!result)
{
cout << "Error: Could not initialize the OpenGL object." << endl;
return false;
}
// Create and initialize the camera object.
m_Camera = new CameraClass;
m_Camera->SetPosition(0.0f, 0.0f, -10.0f);
m_Camera->Render();
m_Camera->RenderBaseViewMatrix();
// Create and initialize the timer object.
m_Timer = new TimerClass;
m_Timer->Initialize();
Our particle system object will now be initialized with the name of the config file for our particle setup.
It will store that file name so that when you reload it knows the file it needs to read.
// Set the file name of the texture for the particle system.
strcpy(configFilename, "../Engine/data/particle_config_01.txt");
// Create and initialize the partcile system object.
m_ParticleSystem = new ParticleSystemClass;
result = m_ParticleSystem->Initialize(m_OpenGL, configFilename);
if(!result)
{
cout << "Error: Could not initialize the particle system object." << endl;
return false;
}
// Create and initialize the particle shader object.
m_ParticleShader = new ParticleShaderClass;
result = m_ParticleShader->Initialize(m_OpenGL);
if(!result)
{
cout << "Error: Could not initialize the particle shader object." << endl;
return false;
}
return true;
}
void ApplicationClass::Shutdown()
{
// Release the particle shader object.
if(m_ParticleShader)
{
m_ParticleShader->Shutdown();
delete m_ParticleShader;
m_ParticleShader = 0;
}
// Release the particle system object.
if(m_ParticleSystem)
{
m_ParticleSystem->Shutdown();
delete m_ParticleSystem;
m_ParticleSystem = 0;
}
// Release the timer object.
if(m_Timer)
{
delete m_Timer;
m_Timer = 0;
}
// Release the camera object.
if(m_Camera)
{
delete m_Camera;
m_Camera = 0;
}
// Release the OpenGL object.
if(m_OpenGL)
{
m_OpenGL->Shutdown();
delete m_OpenGL;
m_OpenGL = 0;
}
return;
}
bool ApplicationClass::Frame(InputClass* Input)
{
bool result;
// Update the system stats.
m_Timer->Frame();
// Check if the escape key has been pressed, if so quit.
if(Input->IsEscapePressed() == true)
{
return false;
}
We add the check for the R key here.
If you press the R key it will reload the particle system and continue to render.
// Check if the user wants to reload the particle system config and restart the particles.
if(Input->IsRPressed() == true)
{
result = m_ParticleSystem->Reload();
if(!result)
{
return false;
}
}
// Run the frame processing for the particle system.
m_ParticleSystem->Frame(m_Timer->GetTime());
// Render the graphics scene.
result = Render();
if(!result)
{
return false;
}
return true;
}
bool ApplicationClass::Render()
{
float worldMatrix[16], viewMatrix[16], projectionMatrix[16];
bool result;
// Clear the buffers to begin the scene.
m_OpenGL->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);
// Get the world, view, and projection matrices from the opengl and camera objects.
m_OpenGL->GetWorldMatrix(worldMatrix);
m_Camera->GetViewMatrix(viewMatrix);
m_OpenGL->GetProjectionMatrix(projectionMatrix);
We will use the new EnableParticleAlphaBlending function to enable the premultiplied alpha blending state.
// Turn on particle alpha blending and disable the Z buffer.
m_OpenGL->EnableParticleAlphaBlending();
m_OpenGL->TurnZBufferOff();
// Set the particle shader as the current shader program and set the matrices that it will use for rendering.
result = m_ParticleShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix);
if(!result)
{
return false;
}
// Render the particles using the particle shader.
m_ParticleSystem->Render();
// Enable the Z buffer and disable alpha blending.
m_OpenGL->TurnZBufferOn();
m_OpenGL->DisableAlphaBlending();
// Present the rendered scene to the screen.
m_OpenGL->EndScene();
return true;
}
Summary
We now have animated particles systems that are easily extensible, and can be used to create millions of different effects.
To Do Exercises
1. Compile and run the program to see the particles animate in a circle pattern. Press escape to quit.
2. Modify the config text file while the program is running, save it, and then press R in the program to see it reload with the new settings.
3. Create your own RGB texture and alpha map to create a new effect.
4. Modify the circle pattern animation to create a different animation, such as a figure eight on its side, or bubbling up animation.
5. Modify the particle pixel shader to take a second texture and alpha. Combine the two RGB textures together, and the two alphas together to create an advanced effect.
6. Add a second set of texture scrolling coordinates so the two alphas are scrolled differently.
7. Add scaling similar to how it was used in the fire shader.
Source Code
Source Code and Data Files: gl4linuxtut55_src.tar.gz