So, I got it working. Finally.
I managed to get so stuck with the fluid simulation that I searched online for GPGPU examples, and found this. It was a really well-written example on how to use the RGB channels of a “velocity” texture for the XYZ co-ordinates of objects on the screen, and have it actually work in three.js.
So I had to painstakingly go through each line of code and try and figure out a way to implement it into my own fluid simulation program, and without having to delete too much of my existing code.
The major differences I found with this method compared to my original method were firstly, the mesh used to render the texture from each shader pass was the same mesh, and instead of adding this mesh to a scene per render pass, it just created one single scene for rendering the fluid on this 1 mesh and then just SWAPPING the texture used in the mesh with the pre-defined textures.
After a few bug fixes and frustration the result was:
Looks better than a single frame! Although there’s no accumulation of textures again, as it is simply replacing the velocity from the previous iteration with the initial velocity, and then not using the divergence shader as it should be.
From further investigation (and another entire day of banging my head from lack of debug ability with such a complex system), I managed to figure out that each texture over-written after it enters the next shader. This meant that once the velocity texture from the first iteration entered the “Add velocity” shader, it was being over-written with the new velocity and then the wrong values are sent to the divergence shader. To fix this, I tried adding a new texture to the system to use for adding velocity onto the velocity texture from the first pass…
Success!
Here you can see the velocity X (red) the velocity Y (green) and the pressure (blue).
I then created a texture that I could use to sample the colour based on the velocity of the fluid:
And then rendered these colours based on the pressure and some trial-and-error and fixing a weird bug where the colours would go completely white when past the usual 1.0 value of the texture (caused by the texture looping back to the start of the image), the results were interesting:
I sent it to someone and they said that it reminded them of a pool of lava. (A pity I didn’t record a video of it because it looked really smooth and mesmerizing). But I wanted it to look like an explosion.
After more tweaking, I got the fluid to fade out to 0 when there was little-to-no pressure:
I then did some polishing and included more colours in the “heat”
texture to get more fun colours of the flames:
There was still a few problems (minus an issue to do with the velocity texture I originally had and trying to figure out why taking 0.5 from it, it wouldn’t halve shift the velocity, but I won’t get into that):
- The render can reach the edge of the screen, getting cut off which is an undesired result for a sprite.
- The edge of the screen was prone to looping back and causing the fluid to exponentially fling out of the edge.
- The effect lasted too long for a sprite-sheet (about 100 frames!).
- Velocity that is too highly concentrated in one area creates a noisy area, when it should look completely white (caused by the shaders only being able to calculate pixels either side of one another, not regarding the rest of the effect)
The first problem I solved by introducing a terxture I could multiply the blending level of the heat texture:
With this being sampled per pixel, I was able to adjust the flames so that if they trail too far from the centre, it would gradually shove the colour being sampled from the heat texture down the right-side, causing the effect to seem to naturally burn out the further away from the center it got:
To prevent chaotic bleeding of velocity, I added another sample texture, this time to the “add velocity” shader, which basically has a region around the outside of the image that can be multiplied by the velocity to 0 off the velocity around the edges:
The “add velocity” shader now looks like this (notice the edges are darker):
This method was sort of like a cheap hack, but because I’m not too worried about the frames-per-second, and the fact that I have the circle texture limiting the drawing of the fluid anyway, it works well for the situation.
For the effect lasting too long, I introduced a “time” variable where it would start at 1.0 and decrease by a small amount each frame. Once it reaches 0.0, the effect has been faded out completely. I multiply this number by the effect, much like the circle from the first issue above.
I’ll have to figure out the other issues at another time as the project has already lasted too long and the effect is starting to look really good. Here’s a video:
Some ideas for the remaining solution is with the noisy look, I could apply a lens blur effect:
Although this removes some of the fine details of the more orange flames, so I’d have to try and apply it to only brighter areas somehow.
I also managed to accidentally create a “brightness” effect to the final render while trying to figure out the “life” variable (the time that it simulates before it fades out), which I left in and added a uniform for it, this is because I planned to include that effect in the original design anyway.
For the time-being, the final shader code for actually rendering the effect looks like this:
uniform vec2 pixelUnit; uniform float deltaTime; uniform float life; uniform sampler2D heatMap; uniform sampler2D pressureSample; uniform sampler2D velocitySample; uniform sampler2D roundMap; uniform float heatMapRow; uniform float brightness; varying vec2 vuv; // represents which colour gradient our effect will use const float numberOfHeatRows = 8.0; // main program void main() { // get velocity as a unit per pixel float speed = sqrt( texture2D(velocitySample, vuv).x * texture2D(velocitySample, vuv).x + texture2D(velocitySample, vuv).y * texture2D(velocitySample, vuv).y ); // calculate the spot on the UV map of the heat texture we want to use float heatV = 1.0 - (heatMapRow / numberOfHeatRows); // calculate heat based on circular center around center of image float round = texture2D(roundMap, vuv).x; // float heatAmount = (2.0 - speed - round) * clamp(life, 1.0, 10.0); // position on the heat map "amount of heat" vec2 heatPos = vec2(heatAmount, heatV); // get colour from heat map vec4 color = texture2D(heatMap, heatPos) * brightness; // final colour gl_FragColor = vec4( color.r * color.a, color.g * color.a, color.b * color.a, color.a ); }
That’s all, folks.