A couple of new improvements helped to speed things up a bit and also stabilize the simulation.

In the previous version there was noticeable noise in regions of a lot of pressure/density. Due to the limited flow field texture resolution, the collisions cant be solved well enough to compute a smooth particle motion. As a result particles velocity/acceleration exploded and went off … and the whole system got unstable.

Previously one quite good solution was to subdivide the update step into multiple sub-steps. Two sub-steps were sufficient in most cases. However, rendering to the collision buffer is the most expensive part of the algorithm. So each substep slows things down further.


(Vimeo crippled the video quality)


A little trick helped me to solve this. I am still experimenting with this, but so far results are promising. The idea is, that particles under such pressure are basically surrounded by a lot of other (overlapping) particles … and kind of trapped and very limited in their motion. The collision buffer (a single channel distance field), which is created by rendering particles at the size of their collision (additive blending enabled), can be used during the update step to “normalize” the particles velocity/acceleration. The collision field on the other hand is also used to create a flowfield to accelerate particles in the first place, so it kind of seems to cancle out its effect, and indeed, It feels like some energy is getting lost (something further to fix, yeah).

Thats the new update shader (GLSL):

void main(){

  // particle index, based on the current fragment position
  ivec2 tex_loc  = ivec2(gl_FragCoord.xy);
  int   particle_idx = tex_loc.y * wh_position.x + tex_loc.x;
  vec4  particle_pos = texelFetch(tex_position, tex_loc, 0);
  vec2 pos_cur = particle_pos.xy;
  vec2 pos_old = particle_pos.zw;

  if(particle_idx < spawn_hi){
    // normalization, kind of removes noise, seems to work
    // ... stops particles from going crazy when they have no place to move
    float pressure = texture(tex_collision, pos_cur).r;
    pressure = 1.0 / max(1.0, sqrt(pressure));
    // acceleration
    vec2 acc = texture(tex_velocity, pos_cur).xy;
    // fix length
    limitLength(acc, acc_minmax * pressure);
    // update position
    pos_cur += (acc * acc_mult) * wh_velocity_rcp * pressure;
    // velocity
    vec2 vel = (pos_cur - pos_old) / wh_velocity_rcp;
    // fix length
    limitLength(vel, vel_minmax * pressure);
    // update position, verlet integration
    pos_old = pos_cur;
    pos_cur += (vel * vel_mult) * wh_velocity_rcp;

  out_frag = vec4(pos_cur, pos_old);



As a result, a very basic simulation of 50K particles render and simulate at around 500 fps at 1280x720pixel.

1M particles still are manageable at around 40-50 fps.



Flow Field / Distance Field – Debug Views

The screenshot shows underlying structure used for the simulation step.

The image is a composition of

  • combined Distance Fields (DF): collision, cohesion, obstacles
  • combined Flow Fields (FF): collisions, cohesion, obstacles, external force (gravity, user input … any kind of additional flow input)

The FF is build from the gradiens of the DF and is used for the acceleration part during the update step. The DF is used for “normalizing” acceleration/velocity to avoid breakouts and spikes.

Flow Field / Distance Field