OpenGL ES Particle System Tutorial: Part 2/3

Ricardo Rendon Cepeda
How To Develop Your Own Particle System with OpenGL ES 2.0 and GLKit

BOOM! Develop a bare bones particle system!

Welcome back to our 3-part OpenGL ES particle system tutorial series! Here you’ll learn how to make a cool and fun particle system from scratch, and integrate it into an iOS app.

Here’s an overview of the series:

  • Part 1: In the first part, you learned all about particle systems and point sprites, and created a small app to help you learn as you go.
  • Part 2: You are here! This time you’ll learn to create a generic particle-emitter paired system. That’s code word for “awesome and reusable”.
  • Part 3: Finally, you’ll use your newly developed skills to integrate particle effects into a simple 2D game.

Without further ado, time to get cracking on your new particle-emitter system!

Getting Started

To get started, you’ll need a simple project that uses OpenGL ES 2.0 and GLKit to render a blue screen that will serve as a starting point.

If you completed Part 1 of this tutorial series like a boss, see if you can complete a similar project setup named GLParticles2 that renders a blue screen (0.53, 0.81, 0.92, 1.00) — without downloading the starter code.

Like A Boss

If you need some help, feel free to have a peek at the basic instructions and requisites hidden below:

Solution Inside: Challenge Hints SelectShow

If you haven’t read Part 1, want to save a bit of time, are itching to get to the explosion part of this tutorial, or just aren’t feeling like a boss today, then feel free to download the starter code for this tutorial to hurry things along.

Close Enough

Whichever option you chose, build and run your project! You should now have an empty blue screen as shown below:

Run1

Designing Your Particle System

In Part 1 you learned that particle systems work in a two-level hierarchy composed of the emitter and the particles themselves. Furthermore, you saw that particle systems are affected by global, external factors such as time.

To put this knowledge to good use, you need to find a good real-world system to model that involves lots of individual particles, moving independently, and affected by external forces. Hmm — how about an explosion? Or more specifically, a simple blast, where your particles will spread outwards from your emitter center over time.

Go to File\New\File… and create a new file with the iOS\Cocoa Touch\Objective-C class subclass template. Enter EmitterObject for the class and NSObject for the subclass. Make sure both checkboxes are unchecked, click Next, and click Create.

Open EmitterObject.h and replace the #import line with the following:

#import <GLKit/GLKit.h>

Still working in EmitterObject.h, replace the contents of EmitterObject.m with:

#import "EmitterObject.h"
 
#define NUM_PARTICLES 180
 
typedef struct Particle
{
    float       pID;
    float       pRadiusOffset;
    float       pVelocityOffset;
    float       pDecayOffset;
    float       pSizeOffset;
    GLKVector3  pColorOffset;
}
Particle;
 
typedef struct Emitter
{
    Particle    eParticles[NUM_PARTICLES];
    float       eRadius;
    float       eVelocity;
    float       eDecay;
    float       eSize;
    GLKVector3  eColor;
}
Emitter;
 
@implementation EmitterObject
{
    // Instance variables
    GLKVector2  _gravity;
    float       _life;
    float       _time;
}
 
@end

You may have noticed that this is set up similar to EmitterTemplate.h in Part 1 of this tutorial. However, since you’re creating a generic particle system, the emitter code has now been abstracted into a class in order to easily create multiple emitters.

Additionally, particle and emitter properties have been prefixed with p and e respectively to ease implementation as your project grows.

One important field in Particle is pID. This serves as a unique identifier for each particle. It will store an angle in radians which you’ll use to calculate each particle’s position in the simulation.

From fireworks to demolitions, explosives have many inherent properties calculated at the emitter level. However, due to the individual nature of fragments, they are offset at the particle level. For example, an explosion may have an average blast radius of 10m, but fragments may travel anywhere between 9m or 11m from the emitter source.

In your system design, every emitter property listed below also has a comparable particle offset:

  • Blast radius
  • Explosion velocity
  • Explosion decay
  • Fragment size
  • Fragment color

These properties define a simulation that is further governed by the following global external factors:

  • World gravity
  • Simulation lifetime
  • Current time

Thus you have the basis for the structures, and their respective variables, defined in EmitterObject.m.

Abstracting Your Rendering Cycle

Since each emitter object is unique, each instance should also manage its own set of rendering cycle instructions.

Add the following method declarations to EmitterObject.h, just before the @end line:

- (id)initEmitterObject;
- (void)renderWithProjection:(GLKMatrix4)projectionMatrix;
- (void)updateLifeCycle:(float)timeElapsed;

Add the following basic implementations for these methods to EmitterObject.m, just before the @end line:

- (id)initEmitterObject
{
    if(self = [super init])
    {
        // Initialize variables
        _gravity = GLKVector2Make(0.0f, 0.0f);
        _life = 0.0f;
        _time = 0.0f;
    }
    return self;
}
 
- (void)renderWithProjection:(GLKMatrix4)projectionMatrix
{
 
}
 
- (void)updateLifeCycle:(float)timeElapsed
{
 
}

Due to their parameters and public declaration, you may have deduced that these methods are meant to be accessed from outside the class. If so, you’re absolutely right!

initEmitterObject initializes the variable of an EmitterObject instance. Your GLKit view controller calls renderWithProjection: to display the rendered scene properly within your GLKView. Finally, updateLifeCycle: is called for each refresh cycle to give your emitter a chance to update itself appropriately.

Open up MainViewController.m and add the following code to the top of the file, after the #import statement:

#import "EmitterObject.h"
 
@interface MainViewController ()
 
// Properties
@property (strong) EmitterObject* emitter;
 
@end

This gives MainViewController access to an EmitterObject.

Add the following line to MainViewController.m at the end of viewDidLoad:

// Set up Emitter
self.emitter = [[EmitterObject alloc] initEmitterObject];

This creates a new EmitterObject instance and stores it in your new emitter property.

Add the following code to MainViewController.m at the end of glkView:drawInRect::

// Create Projection Matrix
float aspectRatio = view.frame.size.width / view.frame.size.height;
GLKMatrix4 projectionMatrix = GLKMatrix4MakeScale(1.0f, aspectRatio, 1.0f);
 
// Render Emitter
[self.emitter renderWithProjection:projectionMatrix];

This code defines the position of the scene that is visible in the GLKView. Recall from Part 1 of this tutorial series that OpenGL ES 2.0 screen coordinates range from -1 to +1 on the x and y axes. Therefore, this matrix converts between the device’s coordinate system and the GLKView‘s coordinate system (width and height in points).

You then pass this projection matrix to your emitter object’s renderWithProjection: for every GLKView draw cycle.

Finally, add the following method to MainViewController.m just before the @end line at the bottom of the file:

- (void)update
{
    // Update Emitter
    [self.emitter updateLifeCycle:self.timeSinceLastUpdate];
}

In the code above, update is called automatically by GLKit at the appropriate refresh rate. Inside update you call emitter‘s updateLifeCycle: to give your emitter a chance to update itself. You’ll flesh out updateLifeCycle: a bit later.

Now you can handle all of your OpenGL ES 2.0 calls within your EmitterObject class. Pretty slick!

Adding Vertex and Fragment Shaders

Now that your self-contained emitter is sketched out, you’re ready to create some shaders for your system.

Go to File\New\File…, choose the iOS\Other\Empty template, then click Next. Name the new file Emitter.vsh, uncheck the box next to your GLParticles2 target, and click Create.

Repeat the process above for another new file, but this time name it Emitter.fsh. These files will be used to write your vertex and fragment shaders.

Copy the following code into Emitter.vsh:

// Vertex Shader
 
static const char* EmitterVS = STRINGIFY
(
 
// Attributes
attribute float     a_pID;
attribute float     a_pRadiusOffset;
attribute float     a_pVelocityOffset;
attribute float     a_pDecayOffset;
attribute float     a_pSizeOffset;
attribute vec3      a_pColorOffset;
 
// Uniforms
uniform mat4        u_ProjectionMatrix;
uniform vec2        u_Gravity;
uniform float       u_Time;
uniform float       u_eRadius;
uniform float       u_eVelocity;
uniform float       u_eDecay;
uniform float       u_eSize;
 
// Varying
varying vec3        v_pColorOffset;
 
void main(void)
{
}
 
);

The above code should look familiar from Part 1 of this tutorial. Because there are so many variables in this shader, all attributes are prefaced with a_, all uniforms are prefaced with u_, and all varyings are prefaced with v_ in order to help tell them apart.

Add the following code to Emitter.vsh, inside main:

// 1
// Convert polar angle to cartesian coordinates and calculate radius
float x = cos(a_pID);
float y = sin(a_pID);
float r = u_eRadius * a_pRadiusOffset;
 
// 2
// Lifetime
float growth = r / (u_eVelocity + a_pVelocityOffset);
float decay = u_eDecay + a_pDecayOffset;
 
// 3
// If blast is growing
if(u_Time < growth)
{
    float time = u_Time / growth;
    x = x * r * time;
    y = y * r * time;
}
 
// 4
// Else if blast is decaying
else
{
    float time = (u_Time - growth) / decay;
    x = (x * r) + (u_Gravity.x * time);
    y = (y * r) + (u_Gravity.y * time);
}
 
// 5
// Required OpenGLES 2.0 outputs
gl_Position = u_ProjectionMatrix * vec4(x, y, 0.0, 1.0);
gl_PointSize = max(0.0, (u_eSize + a_pSizeOffset));
 
// Fragment Shader outputs
v_pColorOffset = a_pColorOffset;

There’s plenty of new code here with a lot of equations, so take a moment to review the code comment by comment:

  1. Each particle has a unique ID in radians which you must convert to cartesian coordinates. By calculating the radius, you have enough information to animate a particle’s trajectory all the way to its final position.
  2. The lifetime of a particle is defined in terms of:
    • Growth: The time taken for a particle to reach its final position when traveling at a certain speed, which is found by way of total radius / total velocity.
    • Decay: The total decay time of the emitter and particle.
  3. If the blast is growing, the particle is traveling from its source towards its final position. In this case, time becomes relative to the particle’s growth lifespan.
  4. If the blast is decaying, the particle is traveling in the direction of gravity from its final position. In this case, time becomes relative to the particle’s decay lifespan.
  5. Both required outputs (the point location and size) and optional outputs (the particle color offset) are passed along the graphics pipeline.

Add the following code to Emitter.fsh:

// Fragment Shader
 
static const char* EmitterFS = STRINGIFY
(
 
// Varying
varying highp vec3      v_pColorOffset;
 
// Uniforms
uniform highp float     u_Time;
uniform highp vec3      u_eColor;
 
void main(void)
{
    // Color
    highp vec4 color = vec4(1.0);
    color.rgb = u_eColor + v_pColorOffset;
    color.rgb = clamp(color.rgb, vec3(0.0), vec3(1.0));
 
    // Required OpenGL ES 2.0 outputs
    gl_FragColor = color;
}
 
);

The fragment shader simply calculates the final color of each particle, based on the overall emitter color and particle offset. The result then uses the clamp function to stay within the bounds of 0.0 for black and 1.0 for white.

If your code is completely black and you aren’t getting automatic GLSL syntax highlighting, then you need to tell Xcode what type of file you are working with.

Part 1 showed you how to turn on syntax highlighting for GLSL files, but there’s another quick way to accomplish the same thing. Look to the top bar of Xcode, go to Editor\Syntax Coloring, and select GLSL from the list as shown in the screenshot below:

Shader Syntax

Building an Objective-C Bridge

Your shaders are ready to run on the GPU, but just as in Part 1, you must create a “bridge” to feed them the necessary data from the CPU. Time to switch back to Objective-C!

First, you’ll need to download the resources for this tutorial. Unzip GLParticles2-resources.zip, which creates a folder named Resources. Right-click on the GLParticles folder in XCode’s Project Navigator and select Add Files to “GLParticles2″…, as shown below:

glp_add_resources_a

Select the Resources folder you unzipped and click Add. Make sure Copy items into destination’s group folder is checked, Create groups for any added folders is selected, and GLParticles2 is checked in the “Add to targets” section, as shown below:

Add Resources Dialog

ShaderProcessor.h and ShaderProcessor.mm are relatively simple shader processors that will be used by your emitter. They are included in the resources file, and were explained back in Part 1 of this tutorial. For that reason they won’t be covered in detail in this part of the tutorial.

Time to build your emitter shaders.

Go to File\New\File… and create a new file with the iOS\Cocoa Touch\Objective-C class subclass template. Enter EmitterShader for the Class and NSObject for the subclass. Make sure the checkbox for the GLParticles2 target is checked, click Next, and click Create.

Replace the contents of EmitterShader.h with the following:

#import <GLKit/GLKit.h>
 
@interface EmitterShader : NSObject
 
// Program Handle
@property (readwrite) GLuint    program;
 
// Attribute Handles
@property (readwrite) GLint     a_pID;
@property (readwrite) GLint     a_pRadiusOffset;
@property (readwrite) GLint     a_pVelocityOffset;
@property (readwrite) GLint     a_pDecayOffset;
@property (readwrite) GLint     a_pSizeOffset;
@property (readwrite) GLint     a_pColorOffset;
 
// Uniform Handles
@property (readwrite) GLuint    u_ProjectionMatrix;
@property (readwrite) GLint     u_Gravity;
@property (readwrite) GLint     u_Time;
@property (readwrite) GLint     u_eRadius;
@property (readwrite) GLint     u_eVelocity;
@property (readwrite) GLint     u_eDecay;
@property (readwrite) GLint     u_eSize;
@property (readwrite) GLint     u_eColor;
 
// Methods
- (void)loadShader;
 
@end

Now, replace the contents of EmitterShader.m with the following:

#import "EmitterShader.h"
#import "ShaderProcessor.h"
 
// Shaders
#define STRINGIFY(A) #A
#include "Emitter.vsh"
#include "Emitter.fsh"
 
@implementation EmitterShader
 
- (void)loadShader
{
    // Program
    ShaderProcessor* shaderProcessor = [[ShaderProcessor alloc] init];
    self.program = [shaderProcessor BuildProgram:EmitterVS with:EmitterFS];
 
    // Attributes
    self.a_pID = glGetAttribLocation(self.program, "a_pID");
    self.a_pRadiusOffset = glGetAttribLocation(self.program, "a_pRadiusOffset");
    self.a_pVelocityOffset = glGetAttribLocation(self.program, "a_pVelocityOffset");
    self.a_pDecayOffset = glGetAttribLocation(self.program, "a_pDecayOffset");
    self.a_pSizeOffset = glGetAttribLocation(self.program, "a_pSizeOffset");
    self.a_pColorOffset = glGetAttribLocation(self.program, "a_pColorOffset");
 
    // Uniforms
    self.u_ProjectionMatrix = glGetUniformLocation(self.program, "u_ProjectionMatrix");
    self.u_Gravity = glGetUniformLocation(self.program, "u_Gravity");
    self.u_Time = glGetUniformLocation(self.program, "u_Time");
    self.u_eRadius = glGetUniformLocation(self.program, "u_eRadius");
    self.u_eVelocity = glGetUniformLocation(self.program, "u_eVelocity");
    self.u_eDecay = glGetUniformLocation(self.program, "u_eDecay");
    self.u_eSize = glGetUniformLocation(self.program, "u_eSize");
    self.u_eColor = glGetUniformLocation(self.program, "u_eColor");
}
 
@end

These two files comprise the requisite shader handles which tell your Objective-C variables where to find their GLSL counterparts. These shader handles are very similar to the EmitterShader you wrote in Part 1, but with quite a few more variables.

Note: You need to be aware of the number of variables you use in your shaders. It turns out that different graphics hardware has different limitations on the number of variables that can be used. In iOS, your shader will fail to compile at runtime if these limits are exceeded. Refer to the Best Practices for Shaders section of Apple’s documentation for more information.

Now that your emitter is complete, you can send some meaningful data to your shaders.

Loading Your Shader Program and Particle System

Open up EmitterObject.m and add the following line just below the existing #import statement:

#import "EmitterShader.h"

Still working in EmitterObject.m, add the following properties just above the @implementation statement:

@interface EmitterObject ()
 
@property (assign) Emitter emitter;
@property (strong) EmitterShader* shader;
 
@end

Stay with EmitterObject.m and add the following method that loads your shader, just above the @end statement:

- (void)loadShader
{
    self.shader = [[EmitterShader alloc] init];
    [self.shader loadShader];
    glUseProgram(self.shader.program);
}

At this point, you’re ready to load up your particle system. Add the following methods to EmitterObject.m, after loadShader:

// 1
- (float)randomFloatBetween:(float)min and:(float)max
{
    float range = max - min;
    return (((float) (arc4random() % ((unsigned)RAND_MAX + 1)) / RAND_MAX) * range) + min;
}
 
- (void)loadParticleSystem
{
    // 2
    Emitter newEmitter = {0.0f};
 
    // 3
    // Offset bounds
    float oRadius = 0.10f;      // 0.0 = circle; 1.0 = ring
    float oVelocity = 0.50f;    // Speed
    float oDecay = 0.25f;       // Time
    float oSize = 8.00f;        // Pixels
    float oColor = 0.25f;       // 0.5 = 50% shade offset
 
    // 4
    // Load Particles
    for(int i=0; i<NUM_PARTICLES; i++)
    {
        // Assign a unique ID to each particle, between 0 and 360 (in radians)
        newEmitter.eParticles[i].pID = GLKMathDegreesToRadians(((float)i/(float)NUM_PARTICLES)*360.0f);
 
        // Assign random offsets within bounds
        newEmitter.eParticles[i].pRadiusOffset = [self randomFloatBetween:oRadius and:1.00f];
        newEmitter.eParticles[i].pVelocityOffset = [self randomFloatBetween:-oVelocity and:oVelocity];
        newEmitter.eParticles[i].pDecayOffset = [self randomFloatBetween:-oDecay and:oDecay];
        newEmitter.eParticles[i].pSizeOffset = [self randomFloatBetween:-oSize and:oSize];
        float r = [self randomFloatBetween:-oColor and:oColor];
        float g = [self randomFloatBetween:-oColor and:oColor];
        float b = [self randomFloatBetween:-oColor and:oColor];
        newEmitter.eParticles[i].pColorOffset = GLKVector3Make(r, g, b);
    }
 
    // 5
    // Load Properties
    newEmitter.eRadius = 0.75f;                                     // Blast radius
    newEmitter.eVelocity = 3.00f;                                   // Explosion velocity
    newEmitter.eDecay = 2.00f;                                      // Explosion decay
    newEmitter.eSize = 32.00f;                                      // Fragment size
    newEmitter.eColor = GLKVector3Make(1.00f, 0.50f, 0.00f);        // Fragment color
 
    // 6
    // Set global factors
    float growth = newEmitter.eRadius / newEmitter.eVelocity;       // Growth time
    _life = growth + newEmitter.eDecay + oDecay;                    // Simulation lifetime
 
    float drag = 10.00f;                                            // Drag (air resistance)
    _gravity = GLKVector2Make(0.00f, -9.81f*(1.0f/drag));           // World gravity
 
    // 7
    // Set Emitter & VBO
    self.emitter = newEmitter;
    GLuint particleBuffer = 0;
    glGenBuffers(1, &particleBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, particleBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(self.emitter.eParticles), self.emitter.eParticles, GL_STATIC_DRAW);
}

Okay — that’s a ton of code. Take a minute to walk through the code, comment by comment:

  1. This function is taken straight from Part 1 and is used to produce a random float value between two bounds.
  2. The member variables of your Emitter structure can’t be assigned values through your emitter property. Therefore, you must create a new emitter variable for this initialization stage, which is then assigned in full to your emitter property.
  3. Here, you define the bounds used to calculate each particle’s offset according to the particle system structures.
  4. All particles are initialized with a unique ID and random offsets.
  5. Here, you define your emitter properties, creating a custom explosion based on a generic template.
  6. Next, you set the global factors that affect the simulation of your scene.
  7. Your new emitter is assigned to your emitter property and a Vertex Buffer Object (VBO) is created for storing its particles.

To finish off your emitter, scroll all the way back up EmitterObject.m and add the following code to initEmitterObject inside the if statement immediately after your variable initialization statements:

// Load Shader
[self loadShader];
 
// Load Particle System
[self loadParticleSystem];

These two lines of code initialize your shader and your particle system.

Rendering Your Explosion

All of the emitter and particle frameworks are in place. All that’s left to do is add the code that will take care of rendering your system on-screen.

Open EmitterObject.m and add the following code to renderWithProjection::

// Uniforms
glUniformMatrix4fv(self.shader.u_ProjectionMatrix, 1, 0, projectionMatrix.m);
glUniform2f(self.shader.u_Gravity, _gravity.x, _gravity.y);
glUniform1f(self.shader.u_Time, _time);
glUniform1f(self.shader.u_eRadius, self.emitter.eRadius);
glUniform1f(self.shader.u_eVelocity, self.emitter.eVelocity);
glUniform1f(self.shader.u_eDecay, self.emitter.eDecay);
glUniform1f(self.shader.u_eSize, self.emitter.eSize);
glUniform3f(self.shader.u_eColor, self.emitter.eColor.r, self.emitter.eColor.g, self.emitter.eColor.b);
 
// Attributes
glEnableVertexAttribArray(self.shader.a_pID);
glEnableVertexAttribArray(self.shader.a_pRadiusOffset);
glEnableVertexAttribArray(self.shader.a_pVelocityOffset);
glEnableVertexAttribArray(self.shader.a_pDecayOffset);
glEnableVertexAttribArray(self.shader.a_pSizeOffset);
glEnableVertexAttribArray(self.shader.a_pColorOffset);
 
glVertexAttribPointer(self.shader.a_pID, 1, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)(offsetof(Particle, pID)));
glVertexAttribPointer(self.shader.a_pRadiusOffset, 1, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)(offsetof(Particle, pRadiusOffset)));
glVertexAttribPointer(self.shader.a_pVelocityOffset, 1, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)(offsetof(Particle, pVelocityOffset)));
glVertexAttribPointer(self.shader.a_pDecayOffset, 1, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)(offsetof(Particle, pDecayOffset)));
glVertexAttribPointer(self.shader.a_pSizeOffset, 1, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)(offsetof(Particle, pSizeOffset)));
glVertexAttribPointer(self.shader.a_pColorOffset, 3, GL_FLOAT, GL_FALSE, sizeof(Particle), (void*)(offsetof(Particle, pColorOffset)));
 
// Draw particles
glDrawArrays(GL_POINTS, 0, NUM_PARTICLES);
glDisableVertexAttribArray(self.shader.a_pID);
glDisableVertexAttribArray(self.shader.a_pRadiusOffset);
glDisableVertexAttribArray(self.shader.a_pVelocityOffset);
glDisableVertexAttribArray(self.shader.a_pDecayOffset);
glDisableVertexAttribArray(self.shader.a_pSizeOffset);
glDisableVertexAttribArray(self.shader.a_pColorOffset);

In the code above, you are simply sending all your uniform and attribute data to your shader program and drawing all your particles.

Still working with EmitterObject.m, add the following code to updateLifeCycle:

_time += timeElapsed;
 
if(_time > _life)
    _time = 0.0f;

This small bit of code will continuously repeat your explosion from birth to death.

Build and run your app — your explosion should look just as if you blew up Stone Man’s stage in Mega Man 5, as shown in the image below:

Run2

There’s something incredibly satisfying to blowing things up! :] Note how each particle is completely unique, differing in position, speed, size, and color.

Enhancing Your Particle System

So far you’ve built a pretty good foundation for a generic particle system. However, as the particles decay, nothing really exciting happens — they simply fall to the ground under the force of gravity. A real explosion changes over time, such as fire turning into smoke, or embers shrinking away.

You’re going to implement a similar effect in your particle system.

Open up EmitterObject.m and modify the Emitter structure by replacing the following variables:

float       eSize;
GLKVector3  eColor;

with:

float       eSizeStart;
float       eSizeEnd;
GLKVector3  eColorStart;
GLKVector3  eColorEnd;

Instead of having a fixed size and color, you now have a start and end target color and size.

Whoops — Xcode is noting some compile errors. To get rid of these compile errors, remove the following lines from renderWithProjection: in EmitterObject.m:

glUniform1f(self.shader.u_eSize, self.emitter.eSize);
glUniform3f(self.shader.u_eColor, self.emitter.eColor.r, self.emitter.eColor.g, self.emitter.eColor.b);

Still working in EmitterObject.m, replace the following lines in loadParticleSystem:

newEmitter.eSize = 32.00f;                                      // Fragment size
newEmitter.eColor = GLKVector3Make(1.00f, 0.50f, 0.00f);        // Fragment color

with the following:

newEmitter.eSizeStart = 32.00f;                                 // Fragment start size
newEmitter.eSizeEnd = 8.00f;                                    // Fragment end size
newEmitter.eColorStart = GLKVector3Make(1.00f, 0.50f, 0.00f);   // Fragment start color
newEmitter.eColorEnd = GLKVector3Make(0.25f, 0.00f, 0.00f);     // Fragment end color

This will make your explosion will fade from bright orange to dark red as your particles decrease in size from 32 pixels to 8. Now you just need to communicate this to your shaders.

Open up Emitter.vsh and replace the following uniform:

uniform float       u_eSize;

with:

uniform float       u_eSizeStart;
uniform float       u_eSizeEnd;

Next, add the following varying variables to Emitter.vsh just below v_pColorOffset:

varying float       v_Growth;
varying float       v_Decay;

These varying variables will be used in the fragment shader.

There’s a few changes to make to main in Emitter.vsh, but rather than try to explain where to make your changes, just replace the entire main method with the following:

void main(void)
{
    // Convert polar angle to cartesian coordinates and calculate radius
    float x = cos(a_pID);
    float y = sin(a_pID);
    float r = u_eRadius * a_pRadiusOffset;
 
    // Lifetime
    float growth = r / (u_eVelocity + a_pVelocityOffset);
    float decay = u_eDecay + a_pDecayOffset;
 
    // Size
    float s = 1.0;
 
    // If blast is growing
    if(u_Time < growth)
    {
        float time = u_Time / growth;
        x = x * r * time;
        y = y * r * time;
 
        // 1
        s = u_eSizeStart;
    }
 
    // Else if blast is decaying
    else
    {
        float time = (u_Time - growth) / decay;
        x = (x * r) + (u_Gravity.x * time);
        y = (y * r) + (u_Gravity.y * time);
 
        // 2
        s = mix(u_eSizeStart, u_eSizeEnd, time);
    }
 
    // Required OpenGL ES 2.0 outputs
    gl_Position = u_ProjectionMatrix * vec4(x, y, 0.0, 1.0);
 
    // 3
    gl_PointSize = max(0.0, (s + a_pSizeOffset));
 
    // Fragment Shader outputs
    v_pColorOffset = a_pColorOffset;
    v_Growth = growth;
    v_Decay = decay;
}

The changes are subtle, but they’re explained below:

  1. If the blast is growing, maintain the particle starting size.
  2. If the blast is decaying, gradually decrease the size of the particle from the starting size to the ending size according to the relative decay time. You use the mix function to interpolate between the two.
  3. Finally, output the resulting size after adding or subtracting the offset.

The fragment shader follows a very similar implementation.

Open Emitter.fsh and add the following lines just below v_pColorOffset:

varying highp float     v_Growth;
varying highp float     v_Decay;

Then, replace the following uniform:

uniform highp vec3      u_eColor;

with:

uniform highp vec3      u_eColorStart;
uniform highp vec3      u_eColorEnd;

Finally, replace all of main with:

void main(void)
{
    // Color
    highp vec4 color = vec4(1.0);
 
    // If blast is growing
    if(u_Time < v_Growth)
    {
        // 1
        color.rgb = u_eColorStart;
    }
 
    // Else if blast is decaying
    else
    {
        highp float time = (u_Time - v_Growth) / v_Decay;
 
        // 2
        color.rgb = mix(u_eColorStart, u_eColorEnd, time);
    }
 
    // 3
    color.rgb += v_pColorOffset;
    color.rgb = clamp(color.rgb, vec3(0.0), vec3(1.0));
 
    // Required OpenGL ES 2.0 outputs
    gl_FragColor = color;
}

The color implementation is almost identical to the size implementation:

  1. If the blast is growing, maintain the particle starting color.
  2. If the blast is decaying, gradually change the color of the particle from the starting color to the ending color, according to the relative decay time. Again, you’re using the mix function to interpolate between the two.
  3. Finally, output the resulting color after adding or subtracting the offset, using the clamp function to stay within the bounds of 0.0 (black) and 1.0 (white).

With your shaders in place it’s time to complete the obligatory EmitterShader bridge. See if you can complete this step all on your own! If you’re stuck, you can check out the solution below:

Solution Inside: CPU-GPU Bridge SelectShow

If you think you know what the next step is, and are still in the mood for a challenge, then go ahead and try it yourself! Otherwise, you can find the solution in the spoiler section below:

Solution Inside: Sending Data to OpenGL ES 2.0 SelectShow

Build and run your app — your particles should now decay in color and size, as shown below:

Run3

It’s a really nice visual effect, especially considering how little code you wrote to achieve this result.

Adding Textures

Your explosion is looking good, but you’re not going to settle for “good” — you want it to look “great”. A simple texture is all you need to turn your chunky squares into tiny little sparks. Point sprites FTW!

You already added some texture files when you added the resources to your project. Now, you just need to tell your fragment shader to expect a texture and how to process it.

Open Emitter.fsh and add the following uniform:

uniform sampler2D       u_Texture;

Still working in Emitter.fsh, add the following line to the beginning of main:

highp vec4 texture = texture2D(u_Texture, gl_PointCoord);

Again in Emitter.fsh, change the gl_FragColor output line as shown below:

gl_FragColor = texture * color;

As before, you need an Objective-C bridge to finish this task off. Open EmitterShader.h and add the following property:

@property (readwrite) GLint u_Texture;

Finalize the implementation by opening EmitterShader.m and adding the following line to the bottom of loadShader:

self.u_Texture = glGetUniformLocation(self.program, "u_Texture");

Now, open EmitterObject.m and add the following method just above the @end statement at the bottom of the file:

- (void)loadTexture:(NSString *)fileName
{
    NSDictionary* options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES],
                             GLKTextureLoaderOriginBottomLeft,
                             nil];
 
    NSError* error;
    NSString* path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
    GLKTextureInfo* texture = [GLKTextureLoader textureWithContentsOfFile:path options:options error:&error];
    if(texture == nil)
    {
        NSLog(@"Error loading file: %@", [error localizedDescription]);
    }
 
    glBindTexture(GL_TEXTURE_2D, texture.name);
}

This is the same method that you used in Part 1, which leverages Apple’s new GLKTextureLoader to load your texture data. But before you can call this new method, you need to do just a tiny bit of refactoring.

Open EmitterObject.h and replace the following line:

- (id)initEmitterObject;

with:

- (id)initWithTexture:(NSString *)fileName;

Likewise, inside EmitterObject.m, replace the following line:

- (id)initEmitterObject

with:

- (id)initWithTexture:(NSString*)fileName

This small change lets you pass a texture filename into the initializer.

Also in EmitterObject.m, add the following code just after you load your shader but before you load your particle system:

// Load Texture
[self loadTexture:fileName];

This simply calls the newly created loadTexture method from within initWithTexture:. At this stage you haven’t actually sent a texture file to this method – you’ll do that in the very next step.

Open up MainViewController.m and replace the following line in viewDidLoad:


self.emitter = [[EmitterObject alloc] initEmitterObject];


with:

self.emitter = [[EmitterObject alloc] initWithTexture:@"texture_64.png"];

Now that your EmitterObject has a new initializer, you call it here with the name of the texture to use.

Open up EmitterObject.m and add the following line to the end of the uniform block in renderWithProjection::

glUniform1i(self.shader.u_Texture, 0);

This actually sends the texture to your shader.

Finally, you need to enable blending and set the appropriate function in your rendering loop.

Open up MainViewController.m and add the following lines to glkView:drawInRect, just after glClearColor and glClear:


// Set the blending function (normal w/ premultiplied alpha)
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

This sets the same blend modes as in part 1, so the textures render properly with transparency.


Build and run your app — you should see the nicely textured explosion as illustrated below:

Run4

Your particle-emitter system just keeps looking better and better. But what if you could create an explosion wherever you wanted on the screen? Read on to find out how!

Adding Multiple Emitters

Recall that you constructed your emitter class to facilitate multiple emitters. This next stage will create a new explosion wherever you tap the screen.

First, you’ll need to enable a position field for your emitter objects. Open EmitterObject.m and add the following line to your Emitter structure:

GLKVector2  ePosition;

Now open Emitter.vsh and add the following uniform to the rest of the uniforms:

uniform vec2        u_ePosition;

Still working in Emitter.vsh, replace the following line that sets gl_Position in main:

gl_Position = u_ProjectionMatrix * vec4(x, y, 0.0, 1.0);

with these two lines:

vec2 position = vec2(x,y) + u_ePosition;
gl_Position = u_ProjectionMatrix * vec4(position, 0.0, 1.0);

These lines translate the points you already calculated to emit from the position you will provide in u_ePosition.

Now, complete the shader bridge by adding the following property to EmitterShader.h:


@property (readwrite) GLint     u_ePosition;


Now, add the following line to EmitterShader.m, at the very end of loadShader:

self.u_ePosition = glGetUniformLocation(self.program, "u_ePosition");

This finds the location of the u_ePosition uniform in the shader and stores its id/name in a property.

Now you need to do a little refactoring to prepare for the new position input. Open up EmitterObject.h and replace the following line:

- (id)initWithTexture:(NSString *)fileName;

with:

- (id)initWithTexture:(NSString *)fileName at:(GLKVector2)position;

Likewise, inside EmitterObject.m, replace the following line:

- (id)initWithTexture:(NSString*)fileName

with:

- (id)initWithTexture:(NSString *)fileName at:(GLKVector2)position

Also in EmitterObject.m, replace the method signature:

- (void)loadParticleSystem

with:

- (void)loadParticleSystem:(GLKVector2)position

Now loadParticleSystem: takes a position as an argument.

Still working in EmitterObject.m, add the following code to loadParticleSystem:, just below the line that starts with newEmitter.eColorEnd = ...:


newEmitter.ePosition = position;                                // Source position

This simply sets the position of the emitter to the position that is passed in.

Now you need to send this data to your shader. Again in EmitterObject.m add the following line to renderWithProjection: immediately after the other calls to glUniform...:

glUniform2f(self.shader.u_ePosition, self.emitter.ePosition.x, self.emitter.ePosition.y);

Don’t go anywhere — you’re still working in EmitterObject.m. Return to initWithTexture:at and replace the following line:

[self loadParticleSystem];

with:

[self loadParticleSystem:position];

Replace the following line in EmitterObject.h:

- (void)updateLifeCycle:(float)timeElapsed;

with:

- (BOOL)updateLifeCycle:(float)timeElapsed;

This halts the rendering of an explosion once it has ceased.

Now return to EmitterObject.m and replace updateLifeCycle: with the following:

- (BOOL)updateLifeCycle:(float)timeElapsed
{
    _time += timeElapsed;
 
    if(_time < _life)
        return YES;
    else
        return NO;
}

This new version of updateLifeCycle: doesn’t reset _time to 0 when the emitter reaches the end of its life. Instead, it returns NO, which will cause the emitter to stop. You’ve effectively turned the looping animation into a one-shot explosion.

There’s still two unanswered questions, though — where does the position come from, and where will you check the boolean condition above? The answer to both is: your GLKit View Controller.

Once again, time for some refactoring.

Open MainViewController.m and replace the following single emitter property:

@property (strong) EmitterObject* emitter;

with the following property that stores an array of emitters:

@property (strong) NSMutableArray* emitters;

Also in MainViewController.m, change the lines that set the emitter in viewDidLoad from this:

// Set up Emitter
self.emitter = [[EmitterObject alloc] initWithTexture:@"texture_64.png"];

to this:

// Set up Emitters
self.emitters = [NSMutableArray array];

In this new version, you aren’t creating any actual EmitterObjects yet — you’re just creating an NSMutableArray in which to store them later. You’re using a mutable array because you’ll be adding an EmitterObject for each tap and removing the object later on when the emitter is done.

Now that you have an array of EmitterObjects instead of just one, you need to change your rendering loop.

In MainViewController.m, replace the following line in glkView:drawInRect::

// Render Emitter
[self.emitter renderWithProjection:projectionMatrix];

with:

// Render Emitters
if([self.emitters count] != 0)
{
  for(EmitterObject* emitter in self.emitters)
  {
      [emitter renderWithProjection:projectionMatrix];
  }
}

This loops through all the EmitterObjects that currently exist and calls each emitter’s renderWithProjection: to draw them on screen.

Now replace your implementation of update in MainViewController.m with the following code:

- (void)update
{
    // Update Emitters
    if([self.emitters count] != 0)
    {
        NSMutableArray* deadEmitters = [NSMutableArray array];
 
        for(EmitterObject* emitter in self.emitters)
        {
            BOOL alive = [emitter updateLifeCycle:self.timeSinceLastUpdate];
 
            if(!alive)
                [deadEmitters addObject:emitter];
        }
 
        for(EmitterObject* emitter in deadEmitters)
            [self.emitters removeObject:emitter];
    }
}

This new version of update checks each existing explosion to see if it’s alive or dead. If the explosion has reached the end of its life cycle, as indicated by returning NO from updateLifeCycle:, it is removed from the emitter array.

You are now managing multiple emitter objects, but you need a way to create them when you tap on the screen!

Add the following method to MainViewController.m, just above the @end statement at the bottom of the file:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 1
    // Get touch point and screen information
    CGPoint touchPoint = [touches.anyObject locationInView:self.view];
    CGPoint glPoint = CGPointMake(touchPoint.x/self.view.frame.size.width, touchPoint.y/self.view.frame.size.height);
 
    // 2
    // Convert touch point to GL position
    float aspectRatio = self.view.frame.size.width / self.view.frame.size.height;
    float x = (glPoint.x * 2.0f) - 1.0f;
    float y = ((glPoint.y * 2.0f) - 1.0f) * (-1.0f/aspectRatio);
 
    // 3
    // Create a new emitter object
    EmitterObject* emitter = [[EmitterObject alloc] initWithTexture:@"texture_64.png" at:GLKVector2Make(x, y)];
    [self.emitters addObject:emitter];
}

This logic in the above code is as follows:

  1. An explosion will occur on every tap, so you need some information about this event — specifically, the touch point and screen aspect ratio.
  2. In this tutorial, your OpenGL ES 2.0 coordinates range from -1 to +1, so you need to convert the UI touch point to an OpenGL ES 2.0 position.
  3. With this information, you can now create a new emitter object and add it to your emitter array.

Build and run your app, and tap on the screen to release your destructive powers! You should see multiple explosions on the screen, as shown below:

Run5

After you’ve tired yourself out blowing up everything in sight, give yourself a big pat on the back — you’ve nearly completed your very own generic and interactive particle system!

Note: If you run your app in the simulator, you may see an unnerving pause in the rendering each time you click the simulator’s screen. Running on an actual device will give you the smooth performance you’d expect in your app.

Correcting the Rendering Cycle

Wait – “nearly completed”? Although it feels like you’re all done, there’s one small bug in your app. Did you spot it?

It’s a subtle bug, but try the following exercise to see if you can detect what’s going on:

  • Build and run your app again.
  • Tap on multiple points on the screen.
  • Look closely at the resulting explosions. Notice anything strange?

Every time you tap the screen and cause a new explosion, the particles of the previous explosions switch position. In fact, all of the attributes of the old explosions will change to the attributes of the latest explosion. The difference in elapsed lifetimes is what makes this bug hard to notice, because the size of the explosions change over time, but it’s definitely a noticeable effect.

In Part 1 of this tutorial series, you took some OpenGL ES 2.0 shortcuts because you were only rendering a single object. Now that you have multiple emitters, you need to pay closer attention to the rendering cycle.

Open EmitterObject.m and locate your particle VBO. As you can see, the buffer only gets bound during the emitter initialization. This means that whenever a new emitter object is created, all particles will be rendered from the last VBO to be created.

Inside EmitterObject.m, add a new instance variable to your implementation just below the declaration for _time:

GLuint      _particleBuffer;

Stay with EmitterObject.m, locate the spot where you initialize your other instance variables in initWithTexture:at, and add the following line:

_particleBuffer = 0;

Still in EmitterObject.m, replace the following lines in loadParticleSystem:

GLuint particleBuffer = 0;
glGenBuffers(1, &particleBuffer);
glBindBuffer(GL_ARRAY_BUFFER, particleBuffer);

with:

glGenBuffers(1, &_particleBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _particleBuffer);

Finally, switch buffers on every render by adding the following line to the beginning of renderWithProjection in EmitterObject.m:

// Switch Buffers
glBindBuffer(GL_ARRAY_BUFFER, _particleBuffer);

Build and run your app and tap away! Notice that your new explosions don’t affect the previous ones any more, as shown below:

Run6

You’ve done a great job completing this part of the tutorial. You’ve definitely blown this one out of the water — pun most definitely intended! :]

Where To Go From Here?

You can find the completed project with all of the code and resources from this tutorial on GitHub. Keep an eye on the repository for any updates!

You should now have a good knowledge of the inner workings of generic particle systems, albeit in their most basic form. You should also be able to expand this particular implementation to include more properties and behaviors at both particle and emitter levels.

You have built complex shaders and can pass data to them with ease. You can also abstract rendering cycles and manage multiple graphics objects. All in all, you’ve come really far in the first two parts of this tutorial!

Feel free to play around with the project here to create some of your own cool effects. For example, try changing the look of the particle system or changing the path the particles take.

In Part 3 of this series, you will use your newly developed skills to integrate some particle effects into a simple 2D game.

If you have any questions, comments or suggestions, please feel free to join the conversation below!

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 15 total!

iOS Team

... 29 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 11 total!

Resident Authors Team

... 15 total!