Incompressible Navier-Stokes via Multigrid
CS 493
Lecture, Dr. Lawlor
Oftentimes we don't want to deal with watching pressure waves slowly
expand and bounce around--when we push on the fluid, we want it to flow,
not to just bounce around! If the pressure is defined to be
uniform, we better make sure that the fluid velocities don't ever
try
to change the pressure. Recall that pressure change is driven
from velocity divergence,
often written as "div", and defined with the del operator as "-dp /
dt
= ∇ . v" (the "." here stands for dot like dot product).
Divergence, ∇ . v, takes a vector field like velocity, and returns a
scalar
field like pressure difference.
From the definition of ∇,
-dp / dt = div v = ∇ . v =
dot(vec3(∂ _ / ∂x, ∂ _ / ∂y, ∂ _ / ∂z),v) = ∂v/∂x + ∂v/∂y +
∂v/dz
Recall that in our little simple wave simulator,
C.z+=dt*(L.x - R.x + B.y - T.y); /*
velocity difference -> height */
Again, this is just a centered-difference approximation for:
dp/dt = -(∂v/∂x + ∂v/∂y) = -∇ . v =
-div v
Of course, our goal with incompressible fluids is to make dp/dt
equal
zero; to do this, we've got to ensure the divergence of the velocity
always equals zero. One frustrating problem: by changing the
velocity at one pixel we can easily force divergence to be zero
there, but
this will create divergence at the neighboring pixels. This
means
divergence cannot be eliminated locally, but must act across wide
areas
of the mesh.
The problem of extracting a divergence-free version of a vector
field is called the Helmholtz
problem.
Mathematically, the cleanest way to exactly solve the Helmholtz
problem
is to project the vector field to frequency space with the FFT,
drop
the component of the resulting vectors that points toward the
origin, and then inverse-FFT. This works nicely (at least on a
rectangular 2D grid with power of two dimensions) but it's a lot of
code to import, not terribly fast, and I haven't found a reliable
fast WebGL FFT implementation.
We can more easily eliminate divergence by just computing it, and
gently penalizing it. For example, our standard shallow-water
velocity update dv / dt = - ∇ p pushes against pressure differences,
so
we can reuse this to eliminate divergence. Actually... that's
what happens in the real world--at places where velocities are
converging, pressure increases, pushing back on the velocities,
until
equilibrium is restored. The only trouble is this process
takes
thousands of steps to redistribute velocities across a big mesh.
We can speed up pressure's normal divergence correction process by
just
computing the divergence at several different spatial scales (or
pixel
offsets). For example, we can easily just use the texture2D bias
factor to grab a
velocity sample from a higher mipmap level:
float div=0.0; /* magnitude of divergence of our vector field */
for (float bias=maxbias;bias>=0.0;bias--) {
float far=pow(2.0,bias);
R = texture2D(srcTex,tc+far*srcDirs[0],bias); /* 4 directions */
T = texture2D(srcTex,tc+far*srcDirs[1],bias);
L = texture2D(srcTex,tc+far*srcDirs[2],bias);
B = texture2D(srcTex,tc+far*srcDirs[3],bias);
div += L.x - R.x + B.y - T.y; /* velocity difference -> divergence */
}
We can then change our velocities per-pixel to try to eliminate this
divergence, just like it was pressure (it's often called
"pseudopressure" in simulations). This goes a little bit
faster
if we correct the divergence as we go, like so:
vec2 corr=vec2(0.0);
float div=0.0;
for (float bias=maxbias;bias>=0.0;bias--) {
float far=pow(2.0,bias);
R = texture2D(srcTex,tc+far*srcDirs[0],bias); /* 4 directions */
T = texture2D(srcTex,tc+far*srcDirs[1],bias);
L = texture2D(srcTex,tc+far*srcDirs[2],bias);
B = texture2D(srcTex,tc+far*srcDirs[3],bias);
/* figure out how to fix velocity divergence */
corr+=vec2(R.z-L.z,T.z-B.z);
div+=R.x-L.x + T.y-B.y;
}
C.xy+=divcorr*corr; /* repair divergence using our velocity */
C.z=divscale*div; /* write out new divergence estimate */
Note that we *don't* do a C.z += operation, like a partial
differential equation, since this results in pressure waves carried
by z. Instead, if there's no divergence, z has done its job
and should be zero.
This seems to work quite well, despite being an Euler-style update.
Try
the multigrid WebGL demo.
Adding boundary conditions to these simulations is usually fairly
easy. For example, we can just force the fluid velocity to be
zero over land, and the fluid will flow around it.
Try
fluid flowing around disk island demo.
But there's a problem with multigrid and boundary conditions.
If we ignore the boundary conditions when sampling the coarser
grids, it's easy to get nonphysical behavior where effects like our
divergence correction leap across what should be solid
boundaries. In this demo, the grid is divided into square
zones, so it should act like many disconnected simulations, but
unless we modify the multigrid sampling, information propagates
between the zones.
Try
multigrid cells demo.
In the general case, with arbitrary inlets surrounded by elongated
peninsulas, multigrid can actually be pretty hard to do correctly.