OpenGL ES Particle System Tutorial: Part 3/3

In this third part of our OpenGL ES particle system tutorial series, learn how to add your particle system into a simple 2D game! By Ricardo Rendon Cepeda.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Rendering Your Emitter Object

The console output is interesting, but it certainly isn’t terribly exciting. Your task is to add some graphics to the collision event using textures.

Open up SGGEmitter.h and add the following method declaration:

- (id)initWithFile:(NSString *)fileName projectionMatrix:(GLKMatrix4)projectionMatrix position:(GLKVector2)position;

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

- (id)initWithFile:(NSString *)fileName projectionMatrix:(GLKMatrix4)projectionMatrix position:(GLKVector2)position
{
   self = [super init];
   return self;
}

The compiler won’t give you any warnings or errors, but this method and class is incomplete – right now there’s no emitter code yet!

You’ll return to this method shortly to add the remaining code — but first, you need to create the OpenGL ES 2.0 elements.

Go to File\New\File…, create a new file with the iOS\Other\Empty template, and click Next. Name the new file Emitter.vsh, uncheck the box next to your GLParticles3 target, and click Create.

Repeat the above process for another new file, but this time name it Emitter.fsh.

Copy the following code into Emitter.vsh:

// Vertex Shader

static const char* EmitterVS = STRINGIFY
(

// Attributes
attribute float     a_pID;

// Uniforms
uniform mat4        u_ProjectionMatrix;
uniform mat4        u_ModelViewMatrix;
uniform vec2        u_ePosition;

void main(void)
{        
    float dummy = a_pID;
    vec4 position = vec4(u_ePosition, 0.0, 1.0);
    gl_Position = u_ProjectionMatrix * u_ModelViewMatrix * position;
    gl_PointSize = 16.0;
}

);

Next, add the following code to Emitter.fsh:

// Fragment Shader

static const char* EmitterFS = STRINGIFY
(

// Uniforms
uniform sampler2D       u_Texture;

void main(void)
{
    highp vec4 texture = texture2D(u_Texture, gl_PointCoord);
    gl_FragColor = texture;
}

);

After working through Parts 1 and 2 of this tutorial, this shader pair should seem relatively straightforward:

  • Your vertex shader simply determines a position for your 16-pixel point sprite, which is adjusted by a projection and model-view matrix.
  • Your fragment shader then renders a texture for the above point sprite.

You may have noticed there’s a variable called dummy in your vertex shader that doesn’t actually do anything. This is simply a placeholder as OpenGL ES 2.0 will crash if you send unprocessed attribute data to your vertex shader.

Note: If your shader code is completely black, simply turn on GLSL syntax highlighting under the Editor\Syntax Coloring\GLSL menu item.

Creating the GPU-CPU Bridge

It’s time to head back to Objective-C to create the GPU-CPU bridge.

Choose 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 both checkboxes are unchecked, click Next, and finally click Create.

Open up EmitterShader.h and replace the contents of the file with the following:

#import <GLKit/GLKit.h>

@interface EmitterShader : NSObject

// Program Handle
@property (readwrite) GLuint    program;

// Attribute Handles
@property (readwrite) GLint     a_pID;

// Uniform Handles
@property (readwrite) GLint     u_ProjectionMatrix;
@property (readwrite) GLint     u_ModelViewMatrix;
@property (readwrite) GLint     u_Texture;
@property (readwrite) GLint     u_ePosition;

// Methods
- (void)loadShader;

@end

Now, open up EmitterShader.m replace the entire contents of that file 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");
    
    // Uniforms
    self.u_ProjectionMatrix = glGetUniformLocation(self.program, "u_ProjectionMatrix");
    self.u_ModelViewMatrix = glGetUniformLocation(self.program, "u_ModelViewMatrix");
    self.u_Texture = glGetUniformLocation(self.program, "u_Texture");
    self.u_ePosition = glGetUniformLocation(self.program, "u_ePosition");
}

@end

This setup should look pretty familiar by now. You are simply creating Objective-C variables as counterparts to your GLSL variables. Along with the shader program, this class forms your CPU-GPU bridge.

Generating Data for Your Particle System

Now that your shaders are setup, you can start to work on your particle system.

Open up SGGEmitter.m and add the following line along with the other #import statements at the top of your file:

#import "EmitterShader.h"

Next, add the following structures just below the #import statements in SGGEmitter.m:

#define NUM_PARTICLES 1

typedef struct Particle
{
    float       pID;
}
Particle;

typedef struct Emitter
{
    Particle    eParticles[NUM_PARTICLES];
    GLKVector2  ePosition;
}
Emitter;

At this stage your particle system will simply consist of a single point with a position. Yes, a single point is not very spectacular, but it will show you that you have the positioning element of your particle system coded properly before moving on.

Directly following the code you added above in SGGEmitter.m, replace the line that reads @implementation SGGEmitter with the following bit of code:

@interface SGGEmitter ()

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

@end

@implementation SGGEmitter
{
    // Instance variables
    GLuint      _particleBuffer;
    GLuint      _texture;
    GLKMatrix4  _projectionMatrix;
    GLKVector2  _position;
}

The emitter property stores your emitter-specific data, while the shader property and the various GL instance variables will be used for communicating with the GPU.

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

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

- (void)loadEmitter
{
    Emitter newEmitter = {0.0f};
    
    // Load Particles
    for(int i=0; i<NUM_PARTICLES; i++)
    {
        newEmitter.eParticles[i].pID = 0.0f;
    }
    
    // Load Properties
    newEmitter.ePosition = _position;   // Source position
    
    // Set Emitter & VBO
    self.emitter = newEmitter;
    glGenBuffers(1, &_particleBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, _particleBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(self.emitter.eParticles), self.emitter.eParticles, GL_STATIC_DRAW);
}

The above methods contain code that you've seen in the previous two parts of this tutorial. loadShader creates an EmitterShader, compiles the vertex and fragment shaders and loads the resulting shader program. loadEmitter creates an Emitter, sets the emitter's variables and finally sets up the Vertex Buffer Object (VBO) to pass the data to the GPU.

Still working in SGGEmitter.m, add the following method for loading textures, 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]);
    }
    
    _texture = texture.name;
    glBindTexture(GL_TEXTURE_2D, _texture);
}

This is basically the same loadTexture: method you've used in the past two parts of this tutorial. The one difference is that it stores a handle to the texture in _texture. You'll need that later.

Stay with SGGEmitter.m and replacing the contents of initWithFile:projectionMatrix:position: with the following code:

if((self = [super init]))
{
    _particleBuffer = 0;
    _texture = 0;
    _projectionMatrix = projectionMatrix;
    _position = position;
        
    [self loadShader];
    [self loadEmitter];
    [self loadTexture:fileName];
}
return self;

Once again, this is the standard emitter initialization that should be familiar to you from Part 1 and Part 2 of this tutorial series.

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.