OpenGL ES Particle System Tutorial: Part 1/3

Learn how to develop a particle system using OpenGL ES 2.0 and GLKit! This three-part tutorial covers point sprites, particle effects, and game integration. By Ricardo Rendon Cepeda.

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

Animating Your Polar Rose

Your particle system is looking great, but it needs a little something else. Most particle systems are organic, simulate natural phenomena, and change over time. Okay, enough suspense — your next step is to animate your system!

Animation, in its simplest form, moves an object linearly from point A to point B. In this case, you will animate your rose by expanding and contracting the particles to and from the emitter origin.

Open Emitter.vsh and add the following uniform next to the others:

uniform float uTime;

Still working in Emitter.vsh, replace the first two lines of main with the following code:

float x = uTime * cos(uK*aTheta)*sin(aTheta);
float y = uTime * cos(uK*aTheta)*cos(aTheta);

The code above multiplies the particle position (x,y) by uTime, which will vary the particle's position in relation to time.

As always, you need an Objective-C bridge to get these values to the particles.
Open EmitterShader.h and add the following property:

@property (readwrite) GLint uTime;

This is a handle to the time uniform that you can pass to the shader.

Open EmitterShader.m and add the following line to the end of loadShader:

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

This finds the location of the uniform in the shader and saves a reference to it's name so you can set the value later.

Now, open MainViewController.m and find the line that reads @implementation MainViewController. Declare the following private instance variables by changing MainViewController to look like this:

@implementation MainViewController
{
    // Instance variables
    float   _timeCurrent;
    float   _timeMax;
    int     _timeDirection;
}

Then, add the following code that initializes the above variables to viewDidLoad, just before the call to loadShader:

// Initialize variables
_timeCurrent = 0.0f;
_timeMax = 3.0f;
_timeDirection = 1;

Now, add the following method to the bottom of MainViewController.m:

- (void)update
{
    if(_timeCurrent > _timeMax)
        _timeDirection = -1;
    else if(_timeCurrent < 0.0f)
        _timeDirection = 1;

    _timeCurrent += _timeDirection * self.timeSinceLastUpdate;
}

The above method contains your program’s animation instructions, and performs the following actions:

  1. If the current time variable (_timeCurrent) exceeds the maximum time (3 seconds), then reverse the animation direction from expansion to contraction.
  2. If the current time variable reaches zero, switch the animation direction back to an expansion.
  3. For each frame, the code increments or decrements the current time variable by the amount of time that has passed since the last frame was drawn. This moves the animation at a constant speed, so that if one frame takes longer to redraw than another, it will move farther as well to make up for it.

The net effect of all this is that the rose grows continuously for 3 seconds and then shrinks continuously for 3 seconds — and the cycle then repeats.

Finally, you need to send the normalized current time to your shader.

In MainViewController.m's rendering loop glkView:drawInRect:, add the following line to the bottom of your uniform block, just before the attribute commands:

glUniform1f(self.emitterShader.uTime, (_timeCurrent/_timeMax));

Build and run — your rose should now be fully and continuously animated!

Run4

Using Textures and Point Sprites

Pixel artists may be happy with the current state of the rose, but you can make it look a lot nicer using textures. In this final stage of the tutorial, you will replace the square-shaped particles with point sprites of smaller flowers — cue the soundtrack to Inception! :]

First, download the following image file: texture_32.png. Right-click the link and choose to save the file somewhere on your computer.

Go to File\Add Files to "GLParticles1"... and select the texture_32.png file you downloaded. Be sure to check Copy items into destination group's folder (if needed) and GLParticles1 in the Add to targets section, and click Add, as shown in the following screenshot:

glp_add_to_project

Here's a larger version of the texture so you can have a better look:

glp_texture

As you can see, it's just a white flower-like shape, the rest of which is transparent. The transparent part has been colored gray here so that you can see the white area against the background of a white webpage.

You'll be using this texture as the shape for your particles, but the color will still be calculated using the values in the emitter and particle.

With the texture added to your project, you now need to tell your fragment shader to expect a texture and how to process it.

Open Emitter.fsh and add the following uniform with the others:

uniform sampler2D uTexture;

Then, add the following line to the beginning of main:

highp vec4 texture = texture2D(uTexture, gl_PointCoord);

Here’s how this all works:

  • sampler2D is a special variable exclusively used for texture access.
  • texture2D extracts the value of a texture at a certain texel point. What's a "texel"? Just as pixel = picture element, texel = texture element.
  • gl_PointCoord contains the coordinate of a fragment within each point.

Recall from the introduction to this tutorial that a point sprite is a texture rendered as a single unit. The functions above combine to produce this single unit.

Now texture contains the color value taken directly from the texture file for a given point; for this texture, it's either white or clear.

Change the last line of main from this:

gl_FragColor = color;

to this:

gl_FragColor = texture * color;

Here you multiply the texture color by the color calculated by the emitter and particle. Since the texture color is either white or clear, this will result in either the combination of the emitter and particle colors, or simply clear. This results in the white areas of the texture becoming colored, while the remaining area of the texture remains transparent.

Once again, you must now complete the Objective-C bridge.

Open EmitterShader.h and add the following property:

@property (readwrite) GLint uTexture;

By now, you should recognize a handle when you see it — the above line is just another handle for your bridge.

Open EmitterShader.m and add the following line to the end of loadShader:

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

Now, open MainViewController.m and add the following method just above the @end line:

#pragma mark - Load Texture

- (void)loadTexture:(NSString *)fileName
{
    NSDictionary* options = @{[NSNumber numberWithBool:YES] : GLKTextureLoaderOriginBottomLeft};
    
    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 method uses Apple’s new GLKTextureLoader to load your texture data, which saves you from using the more complex OpenGL ES 2.0 manual loading operations.

You'll now need to call this new loadTexturemethod from within viewDidLoad.

Add the following line to viewDidLoad, just after the call to loadShader:

// Load Texture
[self loadTexture:@"texture_32.png"];

Now you've loaded your texture, but in order to see the texture properly you must enable and set the proper blending function.

Inside glkView:drawInRect: in MainViewController.m, add the following lines just after the calls to glClear:

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

This tutorial doesn't cover all of the possible blending functions in OpenGL, but a quick explanation of the above code is that you are allowing the transparent pixels in your textures to work correctly.

Note: To learn more about blend modes, check out our How to Create Dynamic Textures with CCRenderTexture in Cocos2D 2.X tutorial.

Add the following line to the bottom of your uniform block inside glkView:drawInRect: just before the attribute commands:

glUniform1i(self.emitterShader.uTexture, 0);

This code sends the texture to your shader. You send a value of 0 because you only have one active texture in your program, at the first position of 0.

Build and run! Your flower made of flowers should now be in full bloom, as shown below:

Run5

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.