I'm in the process of implementing a simple particle system for a 2D mobile game (using OpenGL ES 2.0). It's working, but it's pretty slow. I start getting frame rate battering after about 400 particles, which I think is pretty low.

Here's a summary of my approach:

I start with point sprites (GL_POINTS) rendered in a batch just using a native float buffer (I'm in Java-land on Android, so that translates as a java.nio.FloatBuffer).

On GL context init, the following are set:

GLES20.glViewport(0, 0, width, height);             
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

Each draw frame sets the following:


And I bind a single texture:

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle);  
GLES20.glUniform1i(mUniformTextureHandle, 0);

Which is just a simple circle with some blur (and hence some transparency)


Then there are a bunch of glVertexAttribPointer calls:

mGlEs20.glVertexAttribPointer(mAttributeRGBHandle, valsPerRGB, GLES20.GL_FLOAT, false, stride, mBuffer);

...4 more of these

Then I'm drawing:

GLES20.glUniformMatrix4fv(mUniformProjectionMatrixHandle, 1, false, Camera.mProjectionMatrix, 0);
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, drawCalls);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

My vertex shader does have some computation in it, but given that they're point sprites (with only 2 coordinate values) I'm not sure this is the problem:

#ifdef GL_ES
// Set the default precision to low.
    precision lowp float;

uniform mat4 u_ProjectionMatrix;

attribute vec4 a_Position;
attribute float a_PointSize;
attribute vec3 a_RGB;
attribute float a_Alpha;
attribute float a_Burn;

varying vec4 v_Color;

void main()
vec3 v_FGC = a_RGB * a_Alpha;
v_Color = vec4(v_FGC.x, v_FGC.y, v_FGC.z, a_Alpha * (1.0 - a_Burn));
    gl_PointSize = a_PointSize;
    gl_Position = u_ProjectionMatrix * a_Position;

My fragment shader couldn't really be simpler:

#ifdef GL_ES
    // Set the default precision to low.
    precision lowp float;

uniform sampler2D u_Texture;
varying vec4 v_Color;

void main() {
    gl_FragColor = texture2D(u_Texture, gl_PointCoord) * v_Color;

That's about it. I had read that transparent pixels in point sprites can cause issues, but surely not at only 400 points?

I'm running on a fairly new device (12 month old Galaxy Nexus).

My question is less about my approach (although I'm open to suggestion) but more about whether there are any specific OpenGL "no no's" that have leaked into my code.

I'm sure there's GL master out there facepalming right now... I'd love to hear any critique.

  • \$\begingroup\$ Did you try to profile? \$\endgroup\$ Mar 1, 2013 at 11:06
  • \$\begingroup\$ Whats the actual frame rate? \$\endgroup\$ Mar 1, 2013 at 16:55
  • \$\begingroup\$ The frame rate dips to about 40 fps from 60 as I approach, and pass 400 particles. Not sure of the exact ratio \$\endgroup\$
    – Jason
    Mar 1, 2013 at 20:51
  • \$\begingroup\$ Haven't profiled yet.. but I will \$\endgroup\$
    – Jason
    Mar 1, 2013 at 20:51
  • \$\begingroup\$ Sigh.. gl profiling on Android is broken for me :/ (code.google.com/p/android/issues/…) \$\endgroup\$
    – Jason
    Mar 2, 2013 at 7:58

2 Answers 2


This may not be a general answer, but in my case the problem was state change in native buffers.

My particles are drawn in a batch using an approach similar to sprite batching but obviously with GL_POINTS rather than quads (or GL_TRIANGLES). Every time a particle is "drawn" an instruction is sent to the batch with the location/color/size information for the particle (point), then once all particles are "drawn" the batch executes a single glDrawArrays call.

This is (was) implemented by writing the location/color/size information to a native buffer for each "draw" call on a particle.

I didn't ever get gl profiling to work, but normal Java profiling revealed that writing to the native buffer (java.nio.FloatBuffer) is incredibly slow (not really surprising) so now rather than writing every particle's information to the native buffer I simply hold them in heap memory until the draw call is ready to execute then send them all down to the native buffer in one hit.

There may be an optimal middle ground here (i.e. a balance between writing large amounts of data to native buffers once vs frequent small writes) but for now doing one single large write seems to be ok.

  • \$\begingroup\$ How are you creating your FloatBuffer? FloatBuffer.allocate(x)? This will create a non-direct version, which is not ideal when dealing with OpenGL. To create a direct version you need to do: ByteBuffer.allocateDirect(4*x).asFloatBuffer() \$\endgroup\$
    – elFarto
    Jul 3, 2013 at 12:17
  • \$\begingroup\$ Sorry, that should have been: ByteBuffer.allocateDirect(4*x).order(ByteOrder.nativeOrder()).asFloatBuffer() \$\endgroup\$
    – elFarto
    Jul 3, 2013 at 12:34

An approach I use to avoid computing particle positions on the CPU and updating buffers on the GPU is to instead procedurally generate everything on the GPU's shaders.

If you create a buffer of random numbers, and another random number for the particular particle effect instance, you can use these numbers in your shader to to decide, for any given time point, where a particle would and the rest of its state.

And although you are re-using this exact same little VBO for each and every particle system, and many may be visible on-screen at the same time, no two particle systems look the same!

I've used it for 2D flame effects and even for particles that follow moving objects in 3D. There are obvious limitations - particles that react with other things in the scene e.g. bounce off walls - cannot be modelled easily this way.

Here's some example code for a flame-effect vertex shader from my barebones.js engine:

attribute float aLifetime;
attribute float aXPos;
attribute float aYSpeed;
attribute vec2 aColor;
uniform mat4 mvp;
uniform float uTime;
uniform float uPointSize;
varying float vLifetime;
varying vec2 color;
uniform float randseed;
float rand(float f) {
    return fract(456.789*sin(789.123*f*randseed)*(1.+f));
void main(void) {
    float lifetime = mix(0.0,2.0,rand(aLifetime))+1.0;
    vLifetime = mod(uTime,lifetime);
    float ti = 1. - vLifetime/lifetime;
    gl_Position = mvp * vec4(mix(-1.0,1.0,rand(aXPos))*ti,
        mix(0.0,0.7,rand(aYSpeed))*vLifetime - 1.,
        0., 1.);
    vLifetime = 4.*ti*(1. - ti);
    color = aColor;
    gl_PointSize = uPointSize;

My blog post about it.

For 3D effects, here's how to scale the point size with distance from camera.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .