Boundary Conditions - sahilshahpatel/fluid-sim GitHub Wiki

If you've played around enough with your diffusion-enabled simulator, you may have noticed that the dye seems to leak through the edges.

Boundary leaking

Like the blue look? Appendix A shows you how to make a more beautiful render.

This happens because the velocity at our boundaries is allowed to point outwards, so the dye can move out of bounds. But since our texture has a limited size, that dye is then lost forever.

For a proper fluid simulation we have two boundary conditions:

  1. The no-slip condition -- Velocity on the boundaries should be opposite that of the next inner-most cell. Conceptually you can think of this as the dye bouncing off the wall, reversing its velocity.
  2. The pure Neumann condition -- We will address this once we get to the projection step.

In this chapter we will focus on the first. In doing so we will set up the ground work to make the second easy to add later.

Section 1: Revisiting geometry

Recall that most of our shaders are run on all pixels in the texture. This boundary condition shader will be different in that we want it to only run on the boundary pixels. Each boundary pixel will also need to know its inner neighbor in order to enforce these conditions, but the offset from the boundary pixel to its neighbor is different for each edge of the boundary.

In total, this means that we will actually have to run this shader four seperate times, once for each edge of the boundary.

In all of our JavaScript wrapper functions so far we have been using the this.quad object for its VAO and vertex buffer. In this chapter we will use this.boundary. This array buffer uses lines rather than triangles as its primitive. The full buffer describes all four edges at once, but we'll want to run one at a time.

Keep this in mind for later, once we get to the JavaScript wrapper. But first, let's write the shader.

Section 2: The shader

Create a new shader at glsl/boundary.glsl. We already know of two inputs the shader needs (the data texture and the offset vector). In order to make it extendable, we'll also add a float scale input. In the case of the no-slip condition, the scale will be -1, indicating that the boundary's value should be the opposite of the neighbor's.

All in all we have

uniform sampler2D data; // The data to apply the boundary condition to
uniform vec2 res;       // The texture resolution
uniform vec2 offset;    // The offset in XY units to the inner neighbor
uniform float scale;    // The scaling to apply to the neighbor value

At this point you should be familiar with how to use offset and res to calculate the UV position of the neighbor. Then all you need to do is call the texture function, apply the scaling, and set the output variable! If you're not sure how to do this, review the coordinate system overview in chapter 4.

Section 3: The wrapper

Now it's time for our JavaScript wrapper. The first lines are the same as always except that this time we must use this.boundary instead of this.quad. You should also be able to write your own code to set the uniforms.

Where it gets interesting is the very last line. Typically we call gl.drawArrays just once at the end. This time we'll want to call it in a loop. In each iteration of this loop we'll need to send correct the offset uniform and tell the GPU to only draw one edge of the boundary.

Luckily, gl.drawArrays has parameters to allow us to specify where in the vertex buffer to start and how many verticies to look at. You'll need to know a few things in order to do this yourself:

  1. The buffer from this.boundary gives the line vertices starting from the left side and moving clockwise.
  2. this.boundary is also specified as if using gl.LINE_STRIP, but you will want to use individual calls with gl.LINES. (See here for the differences).

Doing this by yourself is certainly tricky, so there are some hints below.

Hint 1

Create an offsets list to know what value to send for the offset uniform.

let offsets = [[1, 0], [0, -1], [-1, 0], [0, 1]];
Hint 2

We will use gl.LINES as our primitive type. This means that we should always use two vertices per call to gl.drawArrays

Hint 3

The data is specified so that it represents the whole boundary as a gl.LINE_STRIP (Note: not a gl.LINE_LOOP). This means that our first line's data starts at index 0, our second line's data starts at index 1, and so on.

Review the data format of gl.LINE_STRIP (link above) to see why this makes sense.

Section 4: Calling the wrapper

Now it's time to call the wrapper in update. Don't forget to create the shader program and uniforms object in init!

We want to assert the boundary condition as our very last step in update, so go ahead and do that now. Opening up our page we see:

Bugged-out boundary

Depending on your exact order of update you may actually see something different. Either way, it will be wrong. In the above footage you can see that there is never any y-velocity drawn (blue) and that the x-velocity is always positive. So what's going on here?

We encourage you to think on it a bit, but this bug took us quite a while to figure out ourselves. It's possible to have noticed the flaw in our logic in the steps above, but we'll show you know how you might figure it out from this debug output.

You might notice that this output looks as if the dye was being drawn yellow rather than red. At the most there seems to be a one-frame delay where you can see pure red before it becomes yellow. If you continue to play around, you'll notice that if you draw on the boundaries they stay red instead of turning yellow.

At this point, if you're very clever, you might think that the dye texure's data is being copied to the velocity texture, but only for non-boundary pixels. This is basically exactly what's going on, and it all comes down to our use of this.outputTexture.

The rest of our shaders draw onto the full canvas quad, so any previous data in this.outputTexture is completely irrelevant. In fact, the only reason we set this.outputTexture during our swap operation is because we don't want to have to allocate new textures every time.

But our boundary texture is only ever overwriting values on the boundary. So all the inner data comes from whatever was left from before! In our case, this.outputTexture was some old dye data (from just before diffusion), so that's why we appeared to be copying data.

The solution to this is very simple. We want the output to be the same as the input for all non-boundary pixels. We can use the this.copyTexture function to copy the data texture into the output texture before our operation.

Great! Now our boundary condition should work as expected. To be honest, the boundary condition alone doesn't do much to stop our leaking:

Boundary still leaks

To fully stop the leaking we'll need to implement projection, which is the focus of our next chapter.

⚠️ **GitHub.com Fallback** ⚠️