Requires ES6 & WebGL 2.0 ★ Runs Best In Firefox (and Chrome) ★ Does Not Run On Mobile
Overview
This is a particle shader where the particles exhibit a force on a field, which is rendered to a texture. It’s kind of a hello world for molecular dynamics. In this simulation, the field does not change the movement of particles. That involves gradient descent, which I will focus on next. In this animation, the motion of particles is similar to the Brownian Motion shader I just wrote. Here it’s like a polar version of Brownian Motion, where the random numbers that are generated alter the forward motion of the particles and the rotation of each. In this way, the particles have a rotational orientation, which is important later on and in particular for the implementation of gradient descent that I will use. This kind of gradient descent is quite a bit more difficult than normal.
The long term goal of this series on graphics simulations is a social physics simulation, where the particles exhibit a non-directional repulsive force and a directional attractive force. This force is calculated on two shared fields and balanced to produce equilibrium, so that the particles form molecule-like “conversations.” Then, “programs” can be written “within” the simulation, which are essentially probabilistic state machines. These allow the exchange of information and the difference in behavior of various particles types to be observed. Utilizing Delauney Triangulation and Quad Trees, the ways in which particles are able to exchange can be greatly expanded.
Challenges
Avoiding Use of Blending
The correct way to render the field textures is by using blending, which I tried to use on the brownian motion shader. Configuring blending with transparent particles is a bit tricky in 2D, since in 3D the particle can be sorted and drawn back to front. I ran into problems doing this and decided to implement it in a different way, which is much less performant, but still creative.
Rendering ParticleId and Joining Textures
To avoid blending, I employed a bit of relational algebra and took a
page from SQL. I rendered the particles first, writing the
particleId
to just one pixel each. This is rendered to a texture
after the particles have their position updated. Then, in the
fsFields
shader, I join the particles
and particleIds
texture.
There is a drawback, which is that occasionally two particles can overlap, leading to an inaccurately generated field. However, for the purpose of calculating a shared field used to update the particle positions, this is fine: with properly tuned parameters, equilibria is produced, the particles repel and no two particles should ever occupy the same pixel. If they do, then the particles are still update as though they are in slightly separate positions, so no two particles would ever get “stuck” together in the same pixel.
Improving the Speed
The problem with this implementation is that it’s slow. This is
because of the for loop in the fsFields
shader. I expected there to
be large performance hits. To avoid these, I need to properly
configure blending to minimize the number of pixels rastered. This
will be much better and much simpler in the end. With a ballSize
of
21, there is a loop of 445 iterations on every pixel drawn in
fsFields
.
Checking Rasterization of ParticleId
To get the particleId
joining solution working, this required
forcing rasterization of only one pixel in vsParticleId & fsParticleId
shaders. This the motivation behind the following lines:
Ensuring that each particle only resulting in one pixel draw is
absolutely essential for an accurate field being drawn. I wanted to
minimize debugging math errors in runtime, so to check the output of
the fsParticleId
shaders, I debugged it with the following lines of
code:
With this code, I was able to check that there were slightly less than
1024 counts, where the sum in the hash particleIdCounts
was exactly
one for every pixel. There would be slightly less than 1024 counts for
1024 pixels because of the chance that two particles could be rendered
to the same pixel and overwritten. This is fine, but I needed to
ensure that no single particle would result in the rasterization of
more than one pixel. This is a creative approach to solving the
problem, but ended up being less performant than necessary.
Fragment Shader: fsParticle
Vertex Shader: vsParticleId
Fragment Shader: fsParticleId
Fragment Shader: fsFields
Fragment Shader: fsRenderFields