Efficient Particle System Using OpenGL ES2.0

My first blog post, wow it took me a while to force myself to do that. If you are tired of digging in docs and stackoverflow it’a a good place to read.

Particles are the most wanted things in games, every good game need some nice and fast particle system on board. Most of people know that good particle system require tons of particles to be processed. Today i’m gonna show you not how to implement master piece class to manage particles but how to implement simple and most important fast particle system on GPU for your own OpenGL game.

More particles you have, better they look. This could be easy to say on PC but when it comes to Mobile device every instruction counts… So we need few things to get this work:

1) Simple data struct to keep our buffers updated
2) Most of calculations need to be done on GPU so, yeah GLSL
3) Possible to modify size, color, rotation, fade etc.
4) Point spites

if we do it right, effects will be amazing!

So first we need some arrays to keep our GPU buffers up to date:

1
2
3
4
5
const int kMaxParticleSize = 10000; //our particle emmiter limit

float SparcleVertex [ 3 * kMaxParticleSize ] , * pSparcleVertex; // 3 coz xyz
float SparcleColor [ 4 * kMaxParticleSize ] , * pSparcleColor; // 4 coz rgba
float SparcleRotation [ 3 * kMaxParticleSize ] , * pSparcleRotation; // sin(angle) cos(angle) fadevalue

Now it is time to implement our Sparke class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class Sparcle
{
public:

    float Pos[ 3 ]; // position
    float Acc[ 3 ]; // acceleration
    float Wind[ 3 ]; // wind direction

    float Time; // life time
    float MaxTime; // max life time
    float gravity; // gravity 
    float h; // saved impact height (height at sparcle hit floor)

    float color[ 4 ]; //sparce color
    float angle; //current rotation angle 
    float random; //random seed so each particle do not start at same angle
    bool rotated; //do we rotate sparcle at each frame

    Sparcle(bool isRotated = true)
    {
        rotated = isRotated;
        Time = 0.0f;
        gravity = -10;

        angle = 0.0f;
        random = rotated ? rand() % 64 : ((float)(rand() % 360) * M_PI / 180.0); //put some random angle at start
    }

    // simulate, bool indicated whatever remove this sparcle or not
    bool Simulate()
    {
        Time += globalDT; //you may want to pass delta time as parameter

        if( Time > MaxTime ) {
          //our sparcle died remove it from queue
          return true;
        }

        //calcualte alpha 0.0-1.0
        float k = Time / MaxTime;

        //update position
        Pos[ 0 ] += ( Acc[ 0 ] + Wind[ 0 ] ) * globalDT;
        Pos[ 1 ] += ( Acc[ 1 ] + Wind[ 1 ] ) * globalDT;
        Pos[ 2 ] += ( Acc[ 2 ] + Wind[ 2 ] ) * globalDT;

        Acc[ 1 ] += gravity * globalDT;

        if( Acc[ 0 ]*Acc[ 0 ] + Acc[ 2 ]*Acc[ 2 ] > 0.08f ) // update particle height if acceleration is big enought
            h = GetHeight( Pos[ 0 ] , Pos[ 2 ] );

        if( Pos[ 1 ] < h && Acc[ 1 ] < -0.05f ) // if particle is falling and hit floor
        {

          //calculate reflect vector 
            Pos[ 1 ] = h;

            Vector3f in( Acc );

            float length = in . length();

            in /= length;

            Vector3f norm( GetNormal( Pos[ 0 ] , Pos[ 2 ] ) );

            Vector3f out = Reflect( in , norm ); // bounce particle

            out *= length * 0.3; //damping 

            memcpy( Acc , &out , 12 ); //3 * sizeof(float)
        }

        angle = rotated ? (Time + random) :  random ;

        color[ 3 ] = 1.0 - fast_absf( k * 2.0 - 1.0 ); //fade out 

        // im using simple iOS OpenglES 2.0 with premultiplied alpha so this may vary on platform
        float c[4] = { color[0] * color[3], color[1] * color[3], color[2] * color[3], color[3] };

        float r[3] = { sinf( angle ), cosf( angle ), (1.0f-k) }; //3rd value is used to specify particle size at given life time

        //update our buffers 
        memcpy( pSparcleVertex , Pos , 12 ); pSparcleVertex += 3;
        memcpy( pSparcleColor , c , 16 ); pSparcleColor += 4;
        memcpy( pSparcleRotation , r , 12 ); pSparcleRotation += 3;

        return false;

    }

};

Ok so it is almost all, now we need some manager / emitter for our sparkles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class SparclesManager
{

public:

    vector< Sparcle > S;

    float gravity;
    float Wind[ 3 ];
    float p_size_min;
    float p_size_max;
    float p_size_decrase;
    bool rotated;

    Texture * texture; //class pointing to texture this is simple wrapper for GLuint texID 

    SparclesManager(    int t = 255,
                      float g = -10,
                      int s = 3,
                      float w1 = 0, float w2 = 0, float w3 = 0 ) :
                      graviti( g ), p_size_min( s ), p_size_max( s )
    {
        rotated = true;
        p_size_decrase = 0.0f;
        pSparcleVertex = SparcleVertex;
        pSparcleColor = SparcleColor;
        pSparcleRotation = SparcleRotation;
    }

    void Clear(){

        S . clear();
    }

    //add new particles to manager
    void Add(   float x, float y , float z ,
              float vx, float vy, float vz,
              int num ,
              GLubyte r, GLubyte g, GLubyte b ,
              float MaxTime = 4 + rand()%10*0.2f )
    {

        Sparcle s(rotated);

        s . Pos[ 0 ] = x;
        s . Pos[ 1 ] = y;
        s . Pos[ 2 ] = z;
        memcpy( s . Wind , Wind, 12 );

        s . gravity = gravity;
        s . MaxTime = MaxTime;

        for( int i = 0; i < num  ; ++i ) // add multiple particles at once
        {
            // particle color
            s . color[ 0 ] = (float)r / 255.0f;
            s . color[ 1 ] = (float)g / 255.0f;
            s . color[ 2 ] = (float)b / 255.0f;

            //acceleration
            s . Acc[ 0 ] = vx + ( rand()%20 - 10 ) * 0.1;
            s . Acc[ 1 ] = ( vy + ( rand()%20 - 10 ) * 0.1 ) * ( rand()%3 * 0.3f + 0.3f );
            s . Acc[ 2 ] = vz + ( rand()%20 - 10 ) * 0.1;

            S . push_back( s );
        }

    }

    // render all particles
    void Render()
    {
        //bind our particle texture 
        glActiveTexture(GL_TEXTURE0);
        glUniform1i(MainMatrix3D->ParticleShader->uniforms[UNI_TEX0],0);
        texture -> Bind();

        //reset buffer position
        pSparcleVertex = SparcleVertex;
        pSparcleColor = SparcleColor;
        pSparcleRotation = SparcleRotation;

        glEnable( GL_BLEND );

        //bind required data (modelview and projection matrix)
        glUniformMatrix4fv(MainMatrix3D->ParticleShader->uniforms[UNI_PROJECTION_MAT], 1 ,false , MainMatrix3D->Projection.m);
        glUniformMatrix4fv(MainMatrix3D->ParticleShader->uniforms[UNI_MODELVIEW_WORLD_MAT], 1 ,false , MainMatrix3D->ModelView.m);

        //this fill our buffers with data calculated for each particle
        for( int i = 0; i < S . size(); )
            if( S[ i ] . Simulate() )
            {
                S .erase( S . begin() + i );
            }
            else
                ++i;

        //send to shader minimum particle size (at start) it maximum size and how much it should decrase size over time
        glUniform3f(MainMatrix3D->ParticleShader->uniforms[UNI_TEX2], p_size_min, p_size_max, p_size_decrase);

        //bind buffer to GPU
        glVertexAttribPointer(ATTRIB_VERTEX, 3, GL_FLOAT, 0, 0, SparcleVertex);
        glEnableVertexAttribArray(ATTRIB_VERTEX);
        glVertexAttribPointer(ATTRIB_COLOR, 4, GL_FLOAT, 0, 0, SparcleColor);
        glEnableVertexAttribArray(ATTRIB_COLOR);
        glVertexAttribPointer(ATTRIB_NORMAL, 3, GL_FLOAT, 0, 0, SparcleRotation);
        glEnableVertexAttribArray(ATTRIB_NORMAL);

        //and draw them as simple POINTS!
        glDrawArrays( GL_POINTS , 0 , S . size() );

        glDisable( GL_BLEND );
    }

};

I will not put here any source code releated to loading textures and shaders in OpenGL as there are tons of other tutorials which explain how to do that. The most importand thing is that as you probably noticed we are not drawing any geometry here! That’s the true power of point sprites. They allow you to draw textured POINTS! the tricky part is to give them proper size and rotation which may not be so easy. Thanks god, most modern phones uses gl_PointCoord and gl_PointSize to manipulate vertex data on GPU.

Finally some shaders that will do all the hard work for us:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//VERTEX SHADER

precision mediump float; //mobile device so we need to specify precison (mediup will be good enought)

attribute vec4 position;
attribute vec4 color;
attribute vec3 rotvec;

uniform    mat4 projection;
uniform mat4 modelView;
uniform vec3 pSize; // min, max, decrase

varying vec4 pcolor;
varying mat2 particleRot;

void main()
{
    //we need to build our 2x2 matrix so we can rotate texture coordinates that comes from GPU
    particleRot[0] = vec2( rotvec.y , -rotvec.x );
    particleRot[1] = rotvec.xy;
    /*
    [cos(angle),-sin(angle),
     sin(angle), cos(angle)]
    */

    pcolor = color;
  
    //to explain this:
    //let say min size is 2 and maximum is 5 and reduction is also 2 so the size will move
    //from 2 -> 5 -> 3 (2->5->(5-2)) so it is close to easyOutBack function

    gl_PointSize = mix( pSize.z * ( rotvec.z - 1.0 ) + pSize.y , pSize.x, rotvec.z );
    gl_Position = projection * (modelView * position);
}

//FRAGMENT SHADER

precision mediump float;

varying vec4 pcolor;
uniform sampler2D pTex;

varying mat2 particleRot;

void main()
{
    vec2 texCoord = particleRot * ( gl_PointCoord - vec2( 0.5 ) ); //use our rotation matrix to modyfi texture coordinates
    gl_FragColor = texture2D( pTex , texCoord + vec2( 0.5 ) ) * pcolor;
}

One more thing: remember to load your texture with GL_CLAMP_TO_EDGE paramter otherwise you may get some weird artifacts while rendering particles when they rotate.

Usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//init

SparclesManager

LightSparcles,
ParticleSmoke( 64 , 0.0f , 14 , 6, 0 , 3 ),
TankSmokeWater(255, 0.0f, 14, 0, 0, 0);

ParticleSmoke . texture = new Texture( "particle1",true);
ParticleSmoke . p_size_min = 40;
ParticleSmoke . p_size_max = 60;

LightSparcles . texture = new Texture( "particle2",true);
LightSparcles . p_size_min = 12;
LightSparcles . p_size_max = 14;

TankSmokeWater . texture = new Texture( "fx_water-splash-01",true);
TankSmokeWater . p_size_min = 15;
TankSmokeWater . p_size_max = 150;
TankSmokeWater . p_size_decrase = 50;
TankSmokeWater . rotated = false;

//example particle adding

for( int i = 0; i < 40; ++i ) {

    float dx = ( rand()%10 - 5 ) * 0.5f;
    float dz = ( rand()%10 - 5 ) * 0.5f;
    float dy = rand() % 3 * 10;

    LightSparcles . Add( Pos[ 0 ]  , Pos[ 1 ] + 1 , Pos[ 2 ] ,  dx*2 , dy , dz*2 , 12 , 244, 142, 74 , 2 + rand()%2 );
    ParticleSmoke . Add( Pos[ 0 ]  , Pos[ 1 ] + 1 , Pos[ 2 ] ,  dx , 0.0f , dz , 1 , 32, 32, 32 );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//drawing

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{

    glViewport(0, 0, rect.size.width*RETINA_SCALE, rect.size.height*RETINA_SCALE);
    glClearColor( 1.0 , 1.0 , 1.0 , 1.0 );
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer

    glUseProgram(MainMatrix3D->ParticleShader->ShaderProgram);
    glDepthMask( 0 );

    Sparcles . Render();
    ParticleSmoke . Render();
    TankSmoke . Render();
    TankSmokeWater . Render();
    TankSmokeGrass . Render();
    TreeParticles . Render();
    HouseParticles . Render();

    glBlendFunc( GL_ONE, GL_ONE ); //additive blending
    LightSparcles . Render();
    glBlendFunc( GL_ONE, GL_ONE_MINUS_SRC_ALPHA );

    glDepthMask( 1 );
}

And that’s it! now we have some really simple and small (less than 300lines of code) particle emitter :) most of code is done in C++ so you can easily port / use this in other mobile platform. I’m using same implementation in my iOS Game SagaTanks and it works great. You can see it in action in the video below:

That’s all for today, next time i will talk about iOS8 weirdness.