How To Develop Your Own Particle System with OpenGL ES 2.0 and GLKit – Part 1/3

In this tutorial series, you will be guided through the development of a particle system using OpenGL ES 2.0 and GLKit. In Part 2 [TODO: Link to Part 2] of the series you will learn to create a generic particle-emitter paired system, and in Part 3 [TODO: Link to Part 3] you will use your newly developed skills to integrate particle effects into a simple 2D game . For now, you must first make yourself comfortable with particle system terminology and point sprites.

Particle Systems

A basic particle system works on a two-level hierarchy:

  • At the top we have the emitter , the source/generator which controls the overall behaviour of the particles.
  • At the bottom we have the particles themselves, a large collection of small objects which share very similar characteristics but are each intrinsically unique.

A good example is precipitation. From top to bottom, let’s first start with a cloud ( emitter ). Depending on the current weather conditions, this cloud may produce rain, hail or snow and may also be moving or growing. The resulting precipitation is determined by the cloud itself, but each falling drop ( particle ) has a different size, consistency, and starting position. Together, their properties form a particle system.

Particle Systems Explained

Point Sprites

In computer graphics, a sprite is simply a stand-alone 2D image within a scene. For example, a single block in Tetris or a coin in Mario. If you’ve developed graphics applications before, you may be familiar with sprites as textured quads: essentially, a set of two triangles forming a rectangular area for a 2D texture. For example, take a look at this little creature from our tutorial How To Create A Simple 2D iPhone Game with OpenGL ES 2.0 and GLKit :

Textured Quad

This triangle-based implementation requires at least 4 vertices per sprite. When developing a particle system with hundreds of units, you are now dealing with A LOT of vertices! Thankfully, OpenGL ES 2.0 can actually make sprite rendering a lot easier with GL_POINTS . This command tells the GPU to draw every vertex as a square point, reducing your 4-vertices-per-sprite problem to just 1!

If you are familiar with OpenGL ES 2.0, you will most likely – perhaps exclusively – have used GL_TRIANGLES before, rendering full objects as a group of triangular faces. Below is a comparison of the two different drawing modes, with GL_LINES added for clarity:

GL Drawing Modes

So, now that you are well-versed in particle system lingo and consider GL_POINTS your friend, let’s get started!

Getting Started

Although Xcode comes with an OpenGL Game template, the provided code mixes GLKBaseEffect with OpenGL ES 2.0 and is generally confusing and overwhelming. So let’s start from scratch! Thanks to GLKit, the process is simple and painless :)

1) Create a new project

Open Xcode and go to File\New\Project . Select iOS\Application\Empty Application . Name your project GLParticles1 and make sure Use Automatic Reference Counting is selected, click Next , choose a folder to save it in, and click Create .

glp_create_project

We want this app to run in portrait orientation only, so click on your GLParticles1 project in the Project Navigator and select GLParticles1 under TARGETS . In the Summary tab, under Supported Interface Orientations , make sure only the Portrait option is selected.

Portrait Orientation

2) Add required frameworks

As the tutorial title indicates, you’ll be using both OpenGL ES 2.0 and GLKit – so you need to add their respective frameworks. In the Project Navigator, click on your GLParticles1 project and select GLParticles1 under TARGETS . In the Build Phases tab, expand the Link Binary With Libraries section, click the + button, find OpenGLES.framework , and click Add . Repeat for GLKit.framework as well.

Frameworks

3) Create a Storyboard and GLKViewController

Go to File\New\File… , choose the iOS\User Interface\Storyboard template and name it MainStoryboard.storyboard . Open MainStoryboard.storyboard and drag a GLKit View Controller onto the storyboard. (You can find GLKit View Controller in the Object Library in the lower left of the screen, as shown in the following image.) This being your first and only view controller, Xcode will automatically set it up as the initial view controller.

Storyboard

This view controller will be governed by your custom code, so let’s create a subclass. Go to File\New\File… , choose the iOS\Cocoa Touch\Objective-C class subclass template. Enter MainViewController for the Class and GLKViewController for the subclass. Make sure both checkboxes are unchecked, click Next , and click Create .

Main View Controller

To make the compiler happy, add the following to the top of MainViewController.h :

#import <GLKit/GLKit.h>

Now open MainStoryboard.storyboard again. Select your GLKit View Controller , and in the Identity Inspector under Custom Class, set the Class to MainViewController .

Storyboard Class

4) Deploy with your new Storyboard

With your storyboard all set up, you now need to tell your project to use it. In the Project Navigator, click on your GLParticles1 project and select GLParticles1 under TARGETS . In the Summary tab, under iPhone / iPod Deployment Info , set the Main Storyboard to MainStoryboard .

Finally, for your main window to be created from your storyboard, open AppDelegate.m and replace application:didFinishLaunchingWithOptions with the following:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    return YES;
}

5) Setup GLKit

GLKit requires minimum boilerplate code to get up and running, so open MainViewController.m and replace the contents with the following:

#import "MainViewController.h"
 
@implementation MainViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Set up context
    EAGLContext* context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:context];
 
    // Set up view
    GLKView* view = (GLKView*)self.view;
    view.context = context;
}
 
#pragma mark - GLKViewDelegate
 
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    // Set the background color (green)
    glClearColor(0.30f, 0.74f, 0.20f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
}
 
@end

In this very simple GLKViewController implementation, an OpenGL ES 2.0 context is created and associated with the view . The code also implements glkView:drawInRect to clear the screen to a green color.

6) Build and run

After all that, you should now have an empty green screen :)

Run1

Designing Your Particle System

Now that your GLKViewController is set up (yours is named MainViewController ), it’s time to design your particle system. Here, we’ll define the properties of your particles and emitter. Firstly though, we need to establish the function of the particle system, i.e., what does it aim to accomplish? Particle systems usually simulate natural phenomena, such as explosions, dust, fire, etc. They are also sometimes used as an optimization when rendering large numbers of identical objects. These uses can lead to highly complex models animated over time, with sophisticated physics/mathematics at play…

Poker Face

BUT, we’re going to leave all that to the film and gaming experts :)

However, it is very important that you get used to some basic maths in order to create realistic and exciting particle systems, so we’ll start with the simple yet beautiful Polar Rose Equations .

Polar Rose
[ 8-Petal Rose from http://en.wikipedia.org/wiki/Rose_(mathematics) ]

The general equation for these curves can be expressed as: r = cos(kθ) . This is the polar form (hence the name), but we’ll be using the alternative cartesian form with the equations x = cos(kθ)sin(θ) and y = cos(kθ)cos(θ) . In both forms, k is a constant and θ (called “theta”) is a variable angle. Can you guess which is determined by the emitter and which is assigned to the particles?

Solution Inside: Emitter or Particles? Select Show

With that, you now have enough information to build the foundations of your particle effect.

Go to File\New\File… , choose the iOS\C and C++\Header File template, and click Next . Name the new header file EmitterTemplate.h and click Create . Replace the contents of EmitterTemplate.h with the following:

#define NUM_PARTICLES 360
 
typedef struct Particles
{
    float       theta;
}
Particles;
 
typedef struct Emitter
{
    Particles   particles[NUM_PARTICLES];
    int         k;
}
Emitter;
 
Emitter emitter = {0.0f};

Here, you are simply creating your particle-emitter template, hence the basic C header file implementation. NUM_PARTICLES defines the number of particles generated by the emitter – 360 is an ideal choice because it represents a full cycle of θ for 0-360 degrees. The Particles structure contains the data for theta (θ), while the Emitter structure contains the constant k as well as the particles themselves. In this tutorial, you will only create a single emitter source, therefore you can declare it within the same header file.

While the particle systems you create in this tutorial have only a single emitter, particle systems in general are not limited to one emitter. You may have any number of emitters in your system (taking into account the limitations of your hardware, of course).

For example, imagine a firework display. Perhaps it launches a colored ball from one point (a single particle from a single emitter). But maybe there are sparkles spraying out of the colored ball (which are different particles coming from an emitter that is itself attached to the original particle). And then at a certain moment, an explosion occurs that sprays a different set of colored lights in all directions (generated from yet another emitter attached to that original particle). It can get pretty complex, but can produce some amazing effects.

Now it’s time to load your emitter! Open MainViewController.m and add the following line to the top of the file:

#import "EmitterTemplate.h"

Now that the file is aware of EmitterTemplate.h , add the following methods to MainViewController.m , just above the @end at the bottom of the file:

- (void)loadParticles
{
    for(int i=0; i<NUM_PARTICLES; i++)
    {
        // Assign each particle its theta value (in radians)
        emitter.particles[i].theta = GLKMathDegreesToRadians(i);
    }
}
 
- (void)loadEmitter
{
    emitter.k = 4.0f;   // Constant k
}

The loadParticles method above just sets the theta angles on the particles. Each gets its own value from 0-359. The loadEmitter method just sets the value of the constant k on the emitter. You will add more to each of these methods a bit later.

Build your project to make sure you haven’t made any errors so far (always a good idea as you work). You can run it if you want, but you should still only see a green screen.

Your particle system is now ready to be initialized, good job! Time to let this sit for a moment as we move on to the next section: shaders!

Adding Vertex and Fragment Shaders

Shaders are the essence of programmable graphics, allowing for full control of your final render through GLSL programming. Here’s a quick refresher from our tutorial OpenGL ES 2.0 for iPhone on the necessary Vertex and Fragment shaders:

  • Vertex shaders are programs that get called once per vertex in your scene. So if you are rendering a simple scene with a single square, with one vertex at each corner, this would be called four times. Its job is to perform some calculations such as lighting, geometry transforms, etc., figure out the final position of the vertex, and also pass on some data to the fragment shader.
  • Fragment shaders are programs that get called once per pixel (sort of) in your scene. So if you’re rendering the same simple scene with a single square, it will be called once for each pixel that the square covers. Fragment shaders can also perform lighting calculations, etc., but their most important job is to set the final color for the pixel.

For the sake of completeness, here’s a quick explanation on the difference between fragments and pixels:

  • A pixel is simply the smallest measured unit of an image or screen.
  • The graphics pipeline produces fragments (raw data) which are then converted (or not) to actual pixels, depending on their visibility, depth, stencil, colour, etc.

One last note: fragment shaders are called many times. Imagine that single square again, made up of 4 vertices. If the square was very small, let’s say 32×32 pixels, the vertex shader would be called 4 times. Ok. But the fragment shader, on the other hand, would be called 1024 times! Now imagine rendering the pixels in a 3D view that covers the entire screen: that’s 1136×640 pixels on an iPhone 5. That means 727,040 calls to that fragment shader. And shaders run…Every. Single. Frame. And if there are transparent objects in your view, shaders may need to run more than once per frame.

Another super important thing about shaders: they are not executed by your devices CPU. Instead, they run on its GPU, which is a dedicated processor that is optimized for performing just this sort of work. So on one hand, your application can get a lot more done by using shaders because it’s CPU is free to do other things. But because the shaders run so many times, and because your app cannot put anything on the display until all of this work is complete, writing inefficient shaders is a quick way to a slow program.

Got it? Great, let’s write some shaders then!

Create a new file with the iOS\Other\Empty template, click Next . Name the new file Emitter.vsh , uncheck the box next to your GLParticles1 target, and click Create. Repeat this process for another new file called Emitter.fsh .

New Shader

These files will be read by OpenGL ES 2.0 as strings, so the filename extension doesn’t really matter, but it’s good practice to use .vsh for vertex shaders and .fsh for fragment shaders.

Shaders: GLSL Programs

Copy the following code into Emitter.vsh :

// Vertex Shader
 
static const char* EmitterVS = STRINGIFY
(
 
// Attributes
attribute float aTheta;
 
// Uniforms
uniform mat4 uProjectionMatrix;
uniform float uK;
 
void main(void)
{
    float x = cos(uK*aTheta)*sin(aTheta);
    float y = cos(uK*aTheta)*cos(aTheta);
 
    gl_Position = uProjectionMatrix * vec4(x, y, 0.0, 1.0);
    gl_PointSize = 16.0;
}
 
);

And the following into Emitter.fsh :

// Fragment Shader
 
static const char* EmitterFS = STRINGIFY
(
 
void main(void)
{    
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
 
);

If your code is completely black and your aren’t getting automatic GLSL syntax highlighting, then you need to tell Xcode what type of file you are working with. For both shader files, look to the Utilities bar on the right and in the File Inspector set the File Type to OpenGL Shading Language source . You may have to re-open your project to see the change.

Shader Syntax

As you may have noticed, shaders are very short programs and GLSL is quite similar to C. They also have special variable prefixes that determine the type and source of data the shader will receive from our main program. Attributes typically change per-vertex (variable θ ) while Uniforms typically change per-frame or per-object (constant k ). Attributes, due to their per-vertex nature, are exclusive to the vertex shader. Uniforms, on the other hand, are accessible to both vertex and fragment shaders. In this tutorial, all attributes will pertain to the particles while most uniforms will be determined by the emitter.

Take a look at your vertex shader , Emitter.vsh . The program simply calculates the polar rose equation for each value of θ, resulting in x and y coordinates. This position is then multiplied by a Projection Matrix, resulting in the final XYZW position needed by gl_Position . Finally, a point size of 16 pixels is set. When working with GL_POINTS , shaders must always include a value for gl_PointSize .

Now take a look at your fragment shader , Emitter.fsh . This is a one-line program that simply sets the color of all relevant fragments to red, sent to gl_FragColor as a 4-channel RGBA representation.

And that’s all you need to know about your first shader pair of this tutorial series. However, since shaders run on the GPU, we must now create a “bridge” to feed them the necessary data from the CPU. So let’s switch back to Objective-C!

Shaders: Objective-C Bridge

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 click Create . Replace the contents of EmitterShader.h with the following:

#import <GLKit/GLKit.h>
 
@interface EmitterShader : NSObject
 
// Program Handle
@property (readwrite) GLint program;
 
// Attribute Handles
@property (readwrite) GLint aTheta;
 
// Uniform Handles
@property (readwrite) GLint uProjectionMatrix;
@property (readwrite) GLint uK;
 
// Methods
- (void)loadShader;
 
@end

And replace the contents of EmitterShader.m with:

#import "EmitterShader.h"
 
@implementation EmitterShader
 
- (void)loadShader
{
    // Attributes
    self.aTheta = glGetAttribLocation(self.program, "aTheta");
 
    // Uniforms
    self.uProjectionMatrix = glGetUniformLocation(self.program, "uProjectionMatrix");
    self.uK = glGetUniformLocation(self.program, "uK");
}
 
@end

With these files, you have now created shader “handles” which tell your Objective-C variables where to find their GPU counterparts. Within the glGetAttribLocation and glGetUniformLocation functions, the first parameter specifies the shader program to be queried (vertex-fragment pair) and the second parameter points to the name of the attribute/uniform within the same program. This is why it’s a good idea to give your GPU and CPU variables the same name :)

Ok, so your attributes and uniforms are set, but what about the actual program? Since your shaders run on the GPU, they’re only readable at runtime with OpenGL ES 2.0. This means that the CPU needs to give the GPU special instructions to compile and link your shaders, then create the program handle.

Quick Exercise: Try to add a blatant error to your shaders (e.g. a missing semi-colon) and build your project. Did you get any warnings or errors? Nope, because Xcode hasn’t even checked your shaders! Remember, we are passing them to the GPU as strings for actual compiling and linking.

Our tutorial OpenGL ES 2.0 for iPhone covers shader compilation in more detail, so give that section a read if you need a refresher. Otherwise, the necessary files are available below for a simple copy-paste.

Create a new file with the iOS\Cocoa Touch\Objective-C class subclass template. Enter ShaderProcessor for the Class and NSObject for the subclass. Make sure both checkboxes are unchecked, click Next , and click Create . Replace the contents of ShaderProcessor.h with the following:

#import <GLKit/GLKit.h>
 
@interface ShaderProcessor : NSObject
 
- (GLuint)BuildProgram:(const char*)vertexShaderSource with:(const char*)fragmentShaderSource;
 
@end

Then, rename ShaderProcessor.m to ShaderProcessor.mm (for C++ processing) and replace the file contents with:

#import "ShaderProcessor.h"
#include <iostream>
 
@implementation ShaderProcessor
 
- (GLuint)BuildProgram:(const char*)vertexShaderSource with:(const char*)fragmentShaderSource
{
    // Build shaders
    GLuint vertexShader = [self BuildShader:vertexShaderSource with:GL_VERTEX_SHADER];
    GLuint fragmentShader = [self BuildShader:fragmentShaderSource with:GL_FRAGMENT_SHADER];
 
    // Create program
    GLuint programHandle = glCreateProgram();
 
    // Attach shaders
    glAttachShader(programHandle, vertexShader);
    glAttachShader(programHandle, fragmentShader);
 
    // Link program
    glLinkProgram(programHandle);
 
    // Check for errors
    GLint linkSuccess;
    glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE)
    {
        NSLog(@"GLSL Program Error");
        GLchar messages[1024];
        glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]);
        std::cout << messages;
        exit(1);
    }
 
    // Delete shaders
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
 
    return programHandle;
}
 
- (GLuint)BuildShader:(const char*)source with:(GLenum)shaderType
{
    // Create the shader object
    GLuint shaderHandle = glCreateShader(shaderType);
 
    // Load the shader source
    glShaderSource(shaderHandle, 1, &source, 0);
 
    // Compile the shader
    glCompileShader(shaderHandle);
 
    // Check for errors
    GLint compileSuccess;
    glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
    if (compileSuccess == GL_FALSE)
    {
        NSLog(@"GLSL Shader Error");
        GLchar messages[1024];
        glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
        std::cout << messages;
        exit(1);
    }
 
    return shaderHandle;
}
 
@end

That’s it, no need to ever touch these files again! They form a straightforward class that carries out a generic and necessary process for all shaders, hence the name ShaderProcessor. Now, let’s use it to complete our shader bridge. Open up EmitterShader.m and add the following lines at the very top, just after the first #import statement:

#import "ShaderProcessor.h"
 
// Shaders
#define STRINGIFY(A) #A
#include "Emitter.vsh"
#include "Emitter.fsh"

Within the loadShader method, right before the attribute and uniform calls, add the following lines:

// Program
ShaderProcessor* shaderProcessor = [[ShaderProcessor alloc] init];
self.program = [shaderProcessor BuildProgram:EmitterVS with:EmitterFS];

You have now completed your CPU-GPU shader bridge, great job! You can definitely build your program and check for errors now, but don’t run it just yet.

Can I Run My Program Now?

Almost! Time to send your shaders some meaningful data from your rendering loop. Open up MainViewController.m and replace its contents with the following:

#import "MainViewController.h"
#import "EmitterTemplate.h"
#import "EmitterShader.h"
 
@interface MainViewController ()
 
// Properties
@property (strong) EmitterShader* emitterShader;
 
@end
 
@implementation MainViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Set up context
    EAGLContext* context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:context];
 
    // Set up view
    GLKView* view = (GLKView*)self.view;
    view.context = context;
 
    // Load Shader
    [self loadShader];
 
    // Load Particle System
    [self loadParticles];
    [self loadEmitter];
}
 
#pragma mark - Load Shader
 
- (void)loadShader
{
    // 1
    self.emitterShader = [[EmitterShader alloc] init];
    [self.emitterShader loadShader];
    glUseProgram(self.emitterShader.program);
}
 
#pragma mark - Load Particle System
 
- (void)loadParticles
{
    for(int i=0; i<NUM_PARTICLES; i++)
    {
        // Assign each particle its theta value (in radians)
        emitter.particles[i].theta = GLKMathDegreesToRadians(i);
    }
 
    // 2
    // Create Vertex Buffer Object (VBO)
    GLuint particleBuffer = 0;
    glGenBuffers(1, &particleBuffer);                   // Generate particle buffer
    glBindBuffer(GL_ARRAY_BUFFER, particleBuffer);      // Bind particle buffer
    glBufferData(                                       // Fill bound buffer with particles
                 GL_ARRAY_BUFFER,                       // Buffer type (vertices/particles)
                 sizeof(emitter.particles),             // Buffer data size
                 emitter.particles,                     // Buffer data pointer
                 GL_STATIC_DRAW);                       // Data never changes
}
 
- (void)loadEmitter
{
    emitter.k = 4.0f;   // Constant k
}
 
#pragma mark - GLKViewDelegate
 
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    // Set the background color (green)
    glClearColor(0.30f, 0.74f, 0.20f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
 
    // 3
    // Create Projection Matrix
    float aspectRatio = view.frame.size.width / view.frame.size.height;
    GLKMatrix4 projectionMatrix = GLKMatrix4MakeScale(1.0f, aspectRatio, 1.0f);
 
    // 4
    // Uniforms
    glUniformMatrix4fv(self.emitterShader.uProjectionMatrix, 1, 0, projectionMatrix.m);
    glUniform1f(self.emitterShader.uK, emitter.k);
 
    // 5
    // Attributes
    glEnableVertexAttribArray(self.emitterShader.aTheta);
    glVertexAttribPointer(self.emitterShader.aTheta,                // Set pointer
                          1,                                        // One component per particle
                          GL_FLOAT,                                 // Data is floating point type
                          GL_FALSE,                                 // No fixed point scaling
                          sizeof(Particles),                        // No gaps in data
                          (void*)(offsetof(Particles, theta)));     // Start from "theta" offset within bound buffer
 
    // 6
    // Draw particles
    glDrawArrays(GL_POINTS, 0, NUM_PARTICLES);
    glDisableVertexAttribArray(self.emitterShader.aTheta);
}
 
- (void)update
{
 
}
 
@end

Ok, a lot to cover, so let’s go over how this works:

  1. First of all, you need to load your newly created shaders from your bridge class, then you tell the GPU to use the resulting program for future rendering.
  2. The most effective way to send data to the GPU is through the use of a Vertex Buffer Object (VBO) – data storage units – which you create for your particles. For a more detailed overview of VBOs please refer to the section “Creating Vertex Buffer Objects” of our OpenGL ES 2.0 for iPhone tutorial. If you just need a quick refresher, the accompanying comments should help.
  3. By default, your OpenGL ES 2.0 screen coordinates range from -1 to +1 in x and y. The iPhone screen is not square, so a Projection Matrix calculated from the GLKView aspect ratio is used to scale the view to the right proportions.
  4. Here, you send your Uniform data to the shader program. For all the glUniform calls, the first parameter tells OpenGL ES 2.0 where to find the shader handle to your data, and the last parameter sends the actual data.
  5. Similarly, you also send your Attribute data. This is a slightly more complicated process since you are pointing to a larger batch of data, so let’s go over the parameters of glVertexAttribPointer :
    1. index : pointer to the shader variable (using your bridge object)
    2. size : 1 component per particle (θ is a single float)
    3. type : floating point
    4. normalized : false
    5. stride : no gaps in your Particles structure (single block of data)
    6. pointer : start from the theta offset within the bound particle buffer (useful once you expand your Particles structure)
  6. Finally, you tell the GPU how many points to draw: all of your particles!

Build and run! You should now see an 8-petal polar rose made up of small red squares – congratulations! :D

Run2

Enhancing Your Polar Rose

The last render is all well and good, but perhaps a bit too plain. Particle systems are meant to be exciting organisms, so let’s add some more life to our rose – starting with color.

Open up EmitterTemplate.h and add the following field to your Particles structure:

float shade[3];

Similarly, add the following property to your Emitter structure:

float color[3];

Thinking back to our particle-emitter hierarchy, color will determine the overall RGB color of the rose while shade will determine the RGB color shade of each individual particle. Think of a tree in Autumn; you could say it’s overall color is orange, but in fact it’s a mix of leaves with tones ranging from yellow to red.

Now you will complete the shader-side implementation of these new properties. Open Emitter.vsh and add the following attribute:

attribute vec3 aShade;

In the same file, just below your uniforms and before main , add the following lines:

// Output to Fragment Shader
varying vec3 vShade;

This is a new type of variable called a varying . As you now know, all coloring is carried out by the fragment shader but attributes are not accessible to it. Therefore, a varying acts as an output from the vertex shader to the fragment shader, thus creating an outlet for attributes to be passed along the OpenGL ES 2.0 pipeline. To complete this process, add the following line inside main , at the very end:

vShade = aShade;

Each particle’s shade is now be passed straight through to the fragment shader, where it will be processed appropriately.

Open Emitter.fsh and replace its contents with the following:

// Fragment Shader
 
static const char* EmitterFS = STRINGIFY
(
 
// Input from Vertex Shader
varying highp vec3 vShade;
 
// Uniforms
uniform highp vec3 uColor;
 
void main(void)
{    
    highp vec4 color = vec4((uColor+vShade), 1.0);
    color.rgb = clamp(color.rgb, vec3(0.0), vec3(1.0));
    gl_FragColor = color;
}
 
);

Variables in the fragment shader require precision qualifiers because they process a lot more data than the vertex shaders. They are very important when optimizing large programs but since this tutorial series is rather light, we’ll be using highp all the way.

Anyway, the algorithm above simply adds/subtracts the particle’s shade from the emitter color. The result then uses the clamp function to stay within the bounds of 0.0 (black) and 1.0 (white).

With your shaders all set, it’s time to complete the obligatory bridge. Open up EmitterShader.h and add the following properties to your list of attributes and uniforms:

@property (readwrite) GLint aShade;
 
@property (readwrite) GLint uColor;

Then, open up EmitterShader.m and complete their implementation within loadShader , after the lines creating the program:

self.aShade = glGetAttribLocation(self.program, "aShade");
 
self.uColor = glGetUniformLocation(self.program, "uColor");

Finally, let’s create the actual data for the shaders. Open MainViewController.m and add the following method, just before loadParticles :

- (float)randomFloatBetween:(float)min and:(float)max
{
    float range = max - min;
    return (((float) (arc4random() % ((unsigned)RAND_MAX + 1)) / RAND_MAX) * range) + min;
}

This is a random float generator which you’ll use to create a unique shade for each particle. Now, within loadParticles , add the following lines inside the for loop:

// Assign a random shade offset to each particle, for each RGB channel
emitter.particles[i].shade[0] = [self randomFloatBetween:-0.25f and:0.25f];
emitter.particles[i].shade[1] = [self randomFloatBetween:-0.25f and:0.25f];
emitter.particles[i].shade[2] = [self randomFloatBetween:-0.25f and:0.25f];

As you can see, each particle will have a shade offset between -0.25 and +0.25 for each channel. With that, your particles are set – onto the emitter! Add the following lines inside loadEmitter :

emitter.color[0] = 0.76f;   // Color: R
emitter.color[1] = 0.12f;   // Color: G
emitter.color[2] = 0.34f;   // Color: B

Finally, replace your rendering loop ( glkView:drawInRect ) with the following:

// Set the background color (green)
glClearColor(0.30f, 0.74f, 0.20f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
 
// Create Projection Matrix
float aspectRatio = view.frame.size.width / view.frame.size.height;
GLKMatrix4 projectionMatrix = GLKMatrix4MakeScale(1.0f, aspectRatio, 1.0f);
 
// Uniforms
glUniformMatrix4fv(self.emitterShader.uProjectionMatrix, 1, 0, projectionMatrix.m);
glUniform1f(self.emitterShader.uK, emitter.k);
glUniform3f(self.emitterShader.uColor, emitter.color[0], emitter.color[1], emitter.color[2]);
 
// Attributes
glEnableVertexAttribArray(self.emitterShader.aTheta);
glVertexAttribPointer(self.emitterShader.aTheta,                // Set pointer
                      1,                                        // One component per particle
                      GL_FLOAT,                                 // Data is floating point type
                      GL_FALSE,                                 // No fixed point scaling
                      sizeof(Particles),                        // No gaps in data
                      (void*)(offsetof(Particles, theta)));     // Start from "theta" offset within bound buffer
 
glEnableVertexAttribArray(self.emitterShader.aShade);
glVertexAttribPointer(self.emitterShader.aShade,                // Set pointer
                      3,                                        // Three components per particle
                      GL_FLOAT,                                 // Data is floating point type
                      GL_FALSE,                                 // No fixed point scaling
                      sizeof(Particles),                        // No gaps in data
                      (void*)(offsetof(Particles, shade)));     // Start from "shade" offset within bound buffer
 
// Draw particles
glDrawArrays(GL_POINTS, 0, NUM_PARTICLES);
glDisableVertexAttribArray(self.emitterShader.aTheta);
glDisableVertexAttribArray(self.emitterShader.aShade);

Build and run! Your rose should now be made up of small pink-toned squares, good job.

Run3

Animating Your Polar Rose

For this part of the tutorial, those are all the changes you’ll be making to your particle-emitter pair. However, there’s still some particle system ground to cover. Remember describing them as organisms? Learning they usually simulate natural phenomena? That they change over time? I’ll stop teasing you… animation is the next step!

The simplest form of animation is moving an object linearly from A to B. In this case, you will animate your rose by expanding and contracting the particles to and from the emitter source. Open Emitter.vsh and add the following uniform:

uniform float uTime;

Then, multiply the particle position (x,y) by uTime , resulting in:

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

As always, you must now complete the Objective-C bridge. Open EmitterShader.h and add the following property:

@property (readwrite) GLint uTime;

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

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

Now, open MainViewController.m and add the following instance variables to the implementation, just below the interface/property declarations:

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

Then, initialize them in viewDidLoad , just before you load the shader:

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

Now, turn to the update method at the bottom of MainViewController.m and add the following instructions:

if(_timeCurrent > _timeMax)
    _timeDirection = -1;
else if(_timeCurrent < 0.0f)
    _timeDirection = 1;
 
_timeCurrent += _timeDirection * self.timeSinceLastUpdate;

These are your program’s animation instructions. In pseudo-code, they do the following:

  1. If the current time exceed the maximum time (3s), then reverse the animation direction to a contraction.
  2. Else, if the current time resets, switch the animation direction to an expansion.
  3. On each frame, depending on the direction, the current time adds or subtracts the time equivalent to 1 frame refresh (at 30 FPS).

Essentially, the rose continuously grows for 3 seconds and then shrinks for 3 seconds.

Finally, you need to send the normalized current time to your shader. In your rendering loop, glkView:drawInRect , add the following line to the bottom of your uniform block, just before you reach the attribute commands:

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

Build and run! Your rose should now be fully and continuously animated, enjoy!

Run4

Adding Texture

Pixel artists may be happy with the current rose as their final product, but we can make it 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 Inception music). First, you’ll need to download the resources for this tutorial [TODO: GLParticles1-resources.zip here], and add them to your project. Next, you 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 uTexture;

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

highp vec4 texture = texture2D(uTexture, gl_PointCoord);

And change the gl_FragColor output to:

gl_FragColor = texture * color;

Let’s explain how this fits together:

  • sampler2D is a special variable exclusively used for texture access.
  • texture2D extracts the value of a texture at a certain texel point (just as pixel = picture element, texel = texture element).
  • gl_PointCoord contains the coordinate of a fragment within each point.

As we touched upon at the start of the tutorial, a point sprite is a texture rendered as a single unit. The functions above combine to produce this unit :)

Once again, you must now complete the Objective-C bridge. Open EmitterShader.h and add the following property:

@property (readwrite) GLint uTexture;

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

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

Now, open MainViewController.m and add the following method, just below loadShader :

#pragma mark - Load Texture
 
- (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);
}

And call it from viewDidLoad with your texture file, just after loading the shader:

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

This method uses Apple’s new GLKTextureLoader , which saves us from the more complex OpenGL ES 2.0 manual loading operations. However, in order to see the texture properly, you must enable and set the proper blending function. In your rendering loop, just after glClearColor and glClear , insert the following code:

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

Finally, send the texture to your shader by adding the following line to the bottom of your uniform block, just before you reach the attribute commands:

glUniform1i(self.emitterShader.uTexture, 0);

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, excellent work!

Run5

Where To Go From Here?

Here is the sample project [TODO: GLParticles1-project.zip here] with all of the code and resources from this tutorial.

You should now be comfortable with the particle-emitter hierarchy as well as point sprites in OpenGL ES 2.0. We’ve barely touched upon shaders, but you should have a good idea of what they are and how to communicate with them.

Check out Part 2 [TODO: Link to Part 2] in the series, where you will learn to make a generic particle system for explosions!

If you have any questions, comments or suggestions, please feel free to leave a message below :)


iPad , iPhone Category:

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章