I have a pretty good glow shader going now thanks to some tips from people here.

However there are a few things that bother me that could maybe be better.

#1 Definition of the particles

A lot of times if the particles are moving a lot it just looks like a single sheet of color. The image below is zoomed in so you can see a bit more definition, but zoomed out and real time it just looks like a sheet of sorts enter image description here

#2 The particles don't look like they are neon

I mean I don't want the effect to be huge but when they are grouped together I would hope for them to have some white highlighting in the glow. It looks like this: enter image description here When it should look more like this (this was just a mockup by blending in an additive copy of it with more contrast) when particles are grouped together [However singular particles shouldn't have white highlights]: enter image description here

Here is my current rendering process:

I have a couple FBO's with texture attachments:

  • Drawing Frame: Never cleared, up-scaled for the purposes of anti-aliasing
  • Smaller Frame: Cleared each frame, exact resolution of screen, Draws the drawing frame on it with the darkest parts cut out
  • Horizontal Blur: Cleared each frame, does a horizontal blur (lower resolution than screen)
  • Vertical Blur: Cleared each frame, blurs the horizontal blur vertically (lower resolution then screen)
  • Front buffer: Draws the Smaller frame, and Vertical Buffers on top of each-other to the screen.

So A typical render might look like this (I apologize for the misalignment and cropping, the Xcode frame debugger allows you to peek at FBO's but not export the texture inside of them I had to use screenshots) enter image description here

To combine the two textures I am using this

gl_FragColor = vec4(clamp(texture2D(s_texture, v_texcoord).xyz, 0.,
1.) + (clamp(texture2D(s_texture2, v_texcoord).xyz, 0., 1.) * 1.5),

And for the two blurs I am using code like this, a lot of this is pre-computed but to make it easier to read I put how I computed it.

float Gaussian(float x, float deviation)
    return (1.0 / sqrt(2.0 * 3.141592 * deviation)) * exp(-((x * x) / (2.0 * deviation)));
vec3 blurS15()

    vec3 blr = vec3(0.0);

    float scalart =  (1 / screenResolution.x) * (1.0 / 7.0) * 14.0;
    float deviationScale = 1.2;
    float finalScalar = 1.1;

    blr += texture2D(s_texture, (v_texcoord + vec2(7.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(7.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(6.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(6.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(5.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(5.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(4.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(4.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(3.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(3.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(2.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(2.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(1.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(1.0, 7.0 * deviationScale) ;

    blr += texture2D(s_texture, (v_texcoord + vec2(0.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(0.0, 7.0 * deviationScale) ;

    blr += texture2D(s_texture, (v_texcoord + vec2(-1.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(-1.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(-2.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(-2.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(-3.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(-3.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(-4.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(-4.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(-5.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(-5.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(-6.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(-6.0, 7.0 * deviationScale) ;
    blr += texture2D(s_texture, (v_texcoord + vec2(-7.0 * scalart, 0.0))).xyz *  finalScalar * Gaussian(-7.0, 7.0 * deviationScale) ;

    return blr; //This gets put into gl_FragColor

Now while the issue could be in how I calculate the blur, and combine it I think it is more likely that the issue is in the way I render the particles. Basically each particle is rendered with a circle and a rectangle each frame. The dot marks where it is, and the rectangle connects it the the previous frame. Each new frame the previous frame is faded and then drawn over.

Here is a diagram of the process, each new hue group represents a new frame, sorry for the childish look of the colors.

enter image description here

I do this using two draw calls, one that draws the rectangles (GL_TRIANGLES with indices), and one that draws the circles (GL_POINTS with outsides alpha set to 0).

While this allows me to quickly simulate an accumulation buffer it comes with some drawbacks, if these can be dealt with without compromising speed then it would be nice to change them.

  1. No actual depth sorting, although the particles could easily have z values it is futile because opengles 2.0 will go black if you don't clear the depth buffer each frame, so it cant really work with an accumulation buffer.
  2. No additive drawing
    • Because there are two separate draw calls, even with that setting on nothing seems to draw additively inside of the same draw call
    • Even if I did the area where the circle and rectangle meet would be extra bright which is undesirable

The additive mode I tried was

glBlendFunc(GLenum(GL_ONE), GLenum(GL_ONE))

To be specific when I said it didn't work I tried disabling it before fade, and then enabling it afterwards. It caused everything to look weird almost as if there were no trails. And there didn't seem to be highlights where particles intersect. However if I had it enabled the whole time then it did look like additive blending. Only the previous frame never faded because you can't draw black in add blend mode.

If you can think of any way I can get more highlighting especially when particles collide, or otherwise make the glow effect look better please let me know.

I am very curious if there is a way to still do depth sorting with an accumulation buffer/a way to not have to clear the depth buffer.


1 Answer 1


In order to achieve the "white" glow effect for amassed particles, you should probably switch to HDR rendering and have your tone mapping operator desaturate the "overbright" pixels. So you'll need a higher-precision, floating point render target (e.g. RGBA1010102, or RGBA16F) and a post-process step to bring that down to the LDR system back-buffer. Then just make sure that your colliding particles add up to a sufficiently high colour value (i.e. over 1.0) and the tone mapper should do the trick.

You may easily simulate an accumulation buffer by using the ping-pong pattern for your render targets:

  1. Render frame #0 to FBO A.
  2. Render frame #1 to FBO B, taking FBO A as history (i.e. hitherto accumulation) input.
  3. Render frame #2 to FBO A, taking FBO B as history input.
  4. Repeat from point 2.

In particular, you could have a moving average in order for the particles to leave nice trails like that. Check out Brian Karis's presentation on Unreal Engine 4's temporal supersampling - slides 15-17 describe the moving average-based blend weighting.

  • $\begingroup$ For your last comment, do you mean that moving average should be used for the simulated accumulation buffer? Aren't you missing a step where you draw a fullscreen black quad to "erase" the buffer lest it blow out to white? $\endgroup$
    – user3412
    Commented Jul 15, 2016 at 6:16
  • 1
    $\begingroup$ Actually, that should be taken care of by the moving average. If new pixel's value is: gl_FragColor = weight * current + (1.0 - weight) * history Then note that the contribution to the final image of a particular frame will simply approach zero over time by principle of diminishing weight, thus fading away. There is no need to clear the RT, then, just overwrite it with each ping-pong. $\endgroup$
    – IneQuation
    Commented Jul 15, 2016 at 8:03
  • 1
    $\begingroup$ Isn't this the same as Alpha blending? $\endgroup$
    – PaulHK
    Commented Jul 22, 2016 at 6:38
  • $\begingroup$ As long as you don't need to accumulate alpha, it is so indeed, since you can put weight in the alpha channel. $\endgroup$
    – IneQuation
    Commented Jul 22, 2016 at 12:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.