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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
OpenGL ES Particle System Tutorial: Part 3/3
40 mins
- Particle System and 2D Game Series Review
- Getting Started
- Deciphering the Game Hierarchy
- Creating the Emitter
- Rendering Your Emitter Object
- Creating the GPU-CPU Bridge
- Generating Data for Your Particle System
- Sending Shader Data to the GPU
- Initializing Your Emitter
- Creating Your Particle Effect
- Deleting Your Emitter Objects
- Gratuitous Sound Effects
- Where To Go From Here?
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.
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.