Message Boards Message Boards

WLJS real-time fluid simulation part 2: adding tracer particles to the velocity field

enter image description here

In this notebook, we will continue to explore a simple technique for simulating 2D incompressible fluids for visual effects. This work is mostly based on Jos Stam. Stable Fluids SIGGRAPH 1999 as well as a tutorial by Karl Sims

Advection

From mathematica point of view it describes a general process of moving something on a scalar or vector field in the direction given by another vector field.

For simplicity's sake, we will first start with scalar fields.

grid = Table[{0, 0}, {5}, {5}]; grid[[3, 3]] = {0, 1};
u = Table[1., {5}, {5}];(*our scalar field we will move*)

One can think about u as if it were a mass of something, which will be carried by the stream grid.

Naive approach

Looking at the borders enter image description here
Warning: this is wrong

advect[v_, u_, \[Delta]t_ : 0.1] := 
 With[{max = Length[v]}, 
  With[{take = 
     Function[{array, x, y}, 
      If[x >= 1 && x <= max && y >= 1 && y <= max, array[[x, y]], 
       array[[1, 1]]  0.]]}, 
   Table[With[{v2 = ((take[v, i, j + 1] + 
             take[v, i, j])/(2.0)) . {0, -1}, 
       v4 = (((take[v, i, j - 1] + take[v, i, j])/(2.0)) . {0, 1}, 
         org = u[[i, j]]}, 
      org + (v4  ((take[u, i, j - 1] + org)/(2.0)) + 
          v2   ((take[u, i, j + 1] + org)/(2.0)))  \[Delta]t], {i, 
      max}, {j, max}] // Chop]]

Now if we test it on our u field

u = Table[1., {5}, {5}];

Table[MatrixPlot[Nest[advect[grid, #] &, u, n], 
   ImageSize -> 200], {n, {0, 1, 100}}];
Riffle[%, 
  HTMLView["<span style=\"font-size:2.5rem\">&rarr;</span>"]] // Row

enter image description hereenter image description here
We can check it numerically as well

Nest[advect[grid, #] &, u, 100] // Chop // MatrixForm

enter image description here
or in a more creative way

Map[Function[cell, RGBColor[Tanh[cell], 0,-Tanh[cell]]], Nest[advect[grid, #]&, u, 100], {2}];
% // MatrixForm

enter image description here
Now we are ready to apply this knowledge to our fluid simulation. But how?

Advection differential equation

I would like to draw your attention to the differential equation of advection

enter image description here
If one tries to descritize it on 1D grid enter image description here
After substituting it back, one finds, that it matches our previous equation we found for a problem of moving "mass" on a grid. ==We solved this exact differnetial advection equation while implementing advect function.==

Fluid Momentum

Newton's second law in the Navier-Stokes equation is defined by the following term.
enter image description here
Does it remind you anything?.. The conservation of momentum of our fluid is actual self advection.

To simulate fluid momentum, the flow field can simply flow itself. Flow vectors are updated by reading interpolated values from upstream grid cells in the same way the tracer image is advected above. New vector values are pulled from the negative direction of flow.

To test it, one can construct the same sandbox used in Part 1, but change the update loop slightly.

vgrid = Table[{0., 0.}, {i, 15}, {j, 15}];
     vcolors = Table[1.0, {Length[vgrid]}, {Length[vgrid]}];

     start = {1, 1};
     drawing = False;
     dest = {0, 0};
     vfps = 0;

     With[{win = 
        CurrentWindow[],(*save the current window to append graphics*)
       currentCell = ResultCell[]}, 
      EventHandler[
       win, {"Closed" -> 
         Function[Null, 
          Delete[currentCell] (*remove output cell if a notebook has \
been closed*)(*this will prevent the animation running uncontrollably \
on the next start*)]}];
      Graphics[{Table[
          With[{i = i, 
            j = j},(*now we have dynamic Hue and dynamic Arrow*)
           Offload[{Hue[vcolors[[i]][[j]]], 
             Arrow[{{i, j}, {i, j} + vgrid[[i]][[j]]}]}]], {i, 
           15}, {j, 
           15}](*attach listeners to a user's mouse to manipulate the \
grid*) EventHandler[
          Graphics`Canvas[], {"mouseup" -> 
            Function[xy, 
             With[{v = -Normalize[start - xy]}, 
              Do[(*draw a line of velocities*)
                With[{p = Round /@ ((xy - start)  a + start)}, 
                  If[p[[1]] <= 15 && p[[1]] >= 1 && p[[2]] <= 15 && 
                    p[[2]] >= 1, 
                    vgrid[[p[[1]], p[[2]]]] = {v[[1]], 
                    v[[2]]};];];, {a, 0, 1, 0.1}];];
             Delete[drawing];(*delete temporal arrow*)
             drawing = False;], 
           "mousemove" -> Function[xy, dest = xy;], 
           "mousedown" -> Function[xy, start = xy;
             dest = xy;
             If[drawing =!= False, Delete[drawing]];
             (*BB[*)(*append GUI's arrow to existing canvas*)
             drawing = 
              FrontSubmit[{AbsoluteThickness[3], Gray, 
                Arrow[{xy, dest // Offload}]}, MetaMarker["vcanvas"], 
               "Window" -> win, 
               "Tracking" -> 
                True (*enable tracking of created \
object*)];]}],(*sync with browser's repaint cycle and update of fps \
label*)AnimationFrameListener[vfps // Offload, "Event" -> "vframe"],
        (*mark this instance of Graphics with uid to append new \
elements*)
        MetaMarker["vcanvas"], Text[vfps // Offload, {0, 0}]}, 
       Controls -> False, ImageSize -> 500, 
       PlotRange -> {{-0.5, 15.5}, {-0.5, 15.5}}, 
       ImagePadding -> None, TransitionType -> None]]

     (*subscribe to animation event*)

     vtime = AbsoluteTime[];

     EventHandler["vframe", 
      Function[Null (vgrid = advect[vgrid, vgrid] // N);
       vcolors = 
        Map[Function[
          val, ((\[Pi] + 2.0  ToPolarCoordinates[val] // 
              Last) (/(3.0  \[Pi]))], vgrid, {2}];
   vfps = (((vfps + 1/(AbsoluteTime[] - vtime)))/(2.0)) // Round;
   vtime = AbsoluteTime[];]];

In the end it should be similar to what is shown on the animation enter image description here

Test particles

In order to better visualize the fluid, we need something to be moved by our stream. The simplest solution is to spread hundreds of tracer particles. The velocity field can push them in the direction interpolated bilinearly from the grid.

Bilinear interpolation

This is the most basic thing to implement. This probably is not the fastest version, but good enough for educational purposes.

bilinearInterpolation[array_, {x0_, y0_}] := 
 Module[{rows, cols, x = y0, y = x0, x1, x2, y1, y2, fQ11, fQ12, fQ21,
    fQ22},(*Get the dimensions of the array*){rows, cols} = 
   Take[Dimensions[array], 2];
  (*Clip points to the boundaries*)x = Clip[x, {1, cols}];
  y = Clip[y, {1, rows}];
  (*Find the bounding indices*)x1 = Floor[x];
  x2 = Ceiling[x];
  y1 = Floor[y];
  y2 = Ceiling[y];
  (*Get the values at the four corners*)fQ11 = array[[y1, x1]];
  fQ12 = array[[y2, x1]];
  fQ21 = array[[y1, x2]];
  fQ22 = array[[y2, x2]];
  (*Perform bilinear interpolation*)
  If[x2 == x1, 
   If[y2 == y1, fQ11, 
    1/(2  (y2 - y1))*(fQ11  (y2 - y) + fQ21  (y2 - y) + 
       fQ12  (y - y1) + fQ22  (y - y1))], 
   If[y2 == y1, 
    1/(2  (x2 - x1))*(fQ11  (x2 - x) + fQ21  (x - x1) + 
       fQ12  (x2 - x) + fQ22  (x - x1)), 
    1/((x2 - x1)  (y2 - y1))*(fQ11  (x2 - x)  (y2 - y) + 
       fQ21  (x - x1)  (y2 - y) + fQ12  (x2 - x)  (y - y1) + 
       fQ22  (x - x1)  (y - y1))]]]

One can check if it works correctly by scaling up some raster images

bmp = Table[{Power[Cos[y], 2], Power[Sin[x], 2], 0.5} // N, {x, 0, Pi,
     Pi/5}, {y, 0, Pi, Pi/5}];
ibmp = Table[
   bilinearInterpolation[bmp, {x, y}], {x, 1, 6 - 0.1, 0.1}, {y, 1, 
    6 - 0.1, 0.1}];

{Image[bmp, "Real32", Magnification -> 50, Antialiasing -> False], 
  Image[ibmp, "Real32", Magnification -> 6]} // Row

enter image description here enter image description here
On the right, there is our interpolated array, while on the left, the original one

Uniform distribution

There is another short problem to solve: how to distribute particles evenly over a defined region. However, there is a solution out of the box in the Wolfram Language standard library.

RandomPointConfiguration[HardcorePointProcess[10000, 0.4, 2], 
    Rectangle[{1, 1}, {5, 5}], Method -> {"LengthOfRun" -> 10000000}][
   "Points"] // Point // Graphics

enter image description here
The last thing is to implement a function, which updates all probe particles

advectParticles[v_, pts_, \[Delta]t_ : 0.02] := 
 Map[Function[p, p + \[Delta]t (bilinearInterpolation[v, p])], pts]

Complete Real-Time fluid simulation

Wrapping everything up together with the divergence solver from the previous part, we can finally assemble the model

removeDivergence[grid_] := 
 With[{(*safety checks,which enforce closed boundaries*)
   take = Function[{array, x, y}, 
     If[x >= 1 && x <= Length[grid] && y >= 1 && y <= Length[grid], 
      array[[x, y]], {0, 0}]]}, 
  MapIndexed[
   Function[{val, i}, 
    val + ((1)/(8.0)) (((take[grid, i[[1]] - 1, i[[2]] - 1] + 
             take[grid, i[[1]] + 1, i[[2]] + 1]) . {1, 1}) {1, 
          1} + ((take[grid, i[[1]] - 1, i[[2]] + 1] + 
             take[grid, i[[1]] + 1, 
              i[[2]] - 1]) . {1, -1}) {1, -1} + (take[grid, 
            i[[1]] - 1, i[[2]]] + take[grid, i[[1]] + 1, i[[2]]] - 
           take[grid, i[[1]], i[[2]] - 1] - 
           take[grid, i[[1]], i[[2]] + 1]) {2, -2} + 
        take[grid, i[[1]], i[[2]]]  (-4))], grid, {2}]]

TL;DR Here is the final code

fgrid = Table[{0., 0.}, {i, 15}, {j, 15}];
fngrid = 
  fgrid // 
   NumericArray; (*introduce a copy of fgrid wrapped as NumericArray*)

(*NumericArray is always faster than a normal List*)

fcolors = Table[1.0, {Length[fgrid]}, {Length[fgrid]}];

start = {1, 1};
drawing = False;
dest = {0, 0};
ffps = 0;

particles = 
  RandomPointConfiguration[HardcorePointProcess[10000, 0.4, 2], 
    Rectangle[{1 + 4, 1 + 4}, {15 - 4, 15 - 4}], 
    Method -> {"LengthOfRun" -> 10000000}]["Points"];

With[{win = 
   CurrentWindow[],(*save the current window to append graphics*)
  currentCell = ResultCell[]}, 
 EventHandler[
  win, {"Closed" -> 
    Function[Null, 
     Delete[currentCell] (*remove output cell if a notebook has been \
closed*)(*this will prevent the animation running uncontrollably on \
the next start*)]}];
 Graphics[{Arrowheads[Medium/2], 
   Table[With[{i = i, 
      j = j},(*BB[*)(*now we have dynamic Hue and dynamic Arrow*)
     Offload[{Hue[fcolors[[i]][[j]]], 
       Arrow[{{i, j}, {i, j} + fngrid[[i]][[j]]}]}]], {i, 15}, {j, 
     15}],(*attach listeners to a user's mouse to manipulate the grid*)
   EventHandler[
    Graphics`Canvas[], {"mouseup" -> 
      Function[xy, 
       With[{v = -Normalize[start - xy]}, 
        Do[(*draw a line of velocities*)
          With[{p = Round /@ ((xy - start)  a + start)}, 
            If[p[[1]] <= 15 && p[[1]] >= 1 && p[[2]] <= 15 && 
               p[[2]] >= 1, 
              fgrid[[p[[1]], p[[2]]]] = {v[[1]], v[[2]]};];];, {a, 0, 
           1, 0.1}];];
       Delete[drawing];(*delete temporal arrow*)drawing = False;], 
     "mousemove" -> Function[xy, dest = xy;], 
     "mousedown" -> Function[xy, start = xy;
       dest = xy;
       If[drawing =!= False, Delete[drawing]];
       (*append GUI's arrow to existing canvas*)
       drawing = 
        FrontSubmit[{AbsoluteThickness[3], Gray, 
          Arrow[{xy, dest // Offload}]}, MetaMarker["fcanvas"], 
         "Window" -> win, 
         "Tracking" -> 
          True (*enable tracking of created object*)];]}],(*sync with \
browser's repaint cycle and update of fps label*)
   AnimationFrameListener[ffps // Offload, "Event" -> "fframe"], 
   r["fcanvas"], PointSize[0.02], Point[particles // Offload], 
   Text[ffps // Offload, {0, 0}]}, Controls -> False, 
  ImageSize -> 500, PlotRange -> {{-0.5, 15.5}, {-0.5, 15.5}}, 
  ImagePadding -> None, 
  TransitionDuration -> 35 (*since the simulation is slow,
  we have to interpolate*)]]

(*subscribe to animation event*)

ftime = AbsoluteTime[];

(*1 advection per 2 removeDivergence*)
fpipeline = 
  Composition[removeDivergence, removeDivergence, advect[#, #, 0.2] &];

EventHandler["fframe", 
  Function[Null,(*apply the whole pipline as a single function*)
   fgrid = fpipeline[fgrid];
   fngrid = fgrid // NumericArray;(*speed up data-
   transfer by packaging array*)
   fcolors = 
    Map[Function[
       val, ((\[Pi] + 2.0  ToPolarCoordinates[val] // 
           Last)/(3.0  \[Pi]))], fgrid, {2}] // NumericArray;
   (*2 times advection*)
   particles = 
    With[{p = advectParticles[fgrid, particles // Normal, 0.3]}, 
     advectParticles[fgrid, p, 0.3] // NumericArray];
   ffps = (((ffps + 1/(AbsoluteTime[] - ftime)))/(2.0)) // Round;
   ftime = AbsoluteTime[];]];

Use your mouse to draw velocity vectors like on the video enter image description here
There is still a lot that can be improved further. Especially advection and the code for removing divergence can be optimized and compiled as well.

Optimizations

Since all our main functions used in the animation loop are dealing only with numerics, one can apply Compile to them. With some minor changes made (which unfortunately make the code a bit ugly, because not all functions are compilable) we have

advect = 
  Compile[{{v, _Real, 3}, {u, _Real, 3}, {\[Delta]t, _Real, 0}}, 
   With[{max = Length[v]}, 
    With[{}, 
     Table[With[{v1 = ((If[i - 1 >= 1, v[[i - 1, j]], {0., 0.}] + 
               v[[i, j]])/(2.0)) . {1, 0}, 
         v2 = ((If[j + 1 <= max, v[[i, j + 1]], {0., 0.}] + 
               v[[i, j]])/(2.0)) . {0, -1}, 
         v3 = ((If[i + 1 <= max, v[[i + 1, j]], {0., 0.}] + 
               v[[i, j]])/(2.0)) . {-1, 0}, 
         v4 = ((If[j - 1 >= 1, v[[i, j - 1]], {0., 0.}] + 
               v[[i, j]])/(2.0)) . {0, 1}, org = u[[i, j]]}, 
        org + (v1  If[v1 > 0, If[i - 1 >= 1, u[[i - 1, j]], {0., 0.}],
               org] + v3  If[v3 > 0, 
              If[i + 1 <= max, u[[i + 1, j]], {0., 0.}], org] + 
            v4  If[v4 > 0, If[j - 1 >= 1, u[[i, j - 1]], {0., 0.}], 
              org] + v2  If[v2 > 0, 
              If[j + 1 <= max, u[[i, j + 1]], {0., 0.}], 
              org])  \[Delta]t], {i, max}, {j, max}] // Chop]]];
removeDivergence = 
  Compile[{{grid, _Real, 3}}, 
   With[{max = grid // Length}, 
    MapIndexed[
     Function[{val, i}, 
      val + ((1)/(8.0))  (((If[
                i[[1]] - 1 >= 1 && i[[1]] - 1 <= max && 
                 i[[2]] - 1 >= 1 && i[[2]] - 1 <= max, 
                grid[[i[[1]] - 1, i[[2]] - 1]], {0., 0.}] + 
               If[i[[1]] + 1 >= 1 && i[[1]] + 1 <= max && 
                 i[[2]] + 1 >= 1 && i[[2]] + 1 <= max, 
                grid[[i[[1]] + 1, i[[2]] + 1]], {0., 0.}]) . {1, 
              1}) {1, 1} + ((If[
                i[[1]] - 1 >= 1 && i[[1]] - 1 <= max && 
                 i[[2]] + 1 >= 1 && i[[2]] + 1 <= max, 
                grid[[i[[1]] - 1, i[[2]] + 1]], {0., 0.}] + 
               If[i[[1]] + 1 >= 1 && i[[1]] + 1 <= max && 
                 i[[2]] - 1 >= 1 && i[[2]] - 1 <= max, 
                grid[[i[[1]] + 1, i[[2]] - 1]], {0., 
                 0.}]) . {1, -1}) {1, -1} + (If[
              i[[1]] - 1 >= 1 && i[[1]] - 1 <= max, 
              grid[[i[[1]] - 1, i[[2]]]], {0., 0.}] + 
             If[i[[1]] + 1 >= 1 && i[[1]] + 1 <= max, 
              grid[[i[[1]] + 1, i[[2]]]], {0., 0.}] - 
             If[i[[2]] - 1 >= 1 && i[[2]] - 1 <= max, 
              grid[[i[[1]], i[[2]] - 1]], {0., 0.}] - 
             If[i[[2]] + 1 >= 1 && i[[2]] + 1 <= max, 
              grid[[i[[1]], i[[2]] + 1]], {0., 0.}]) {2, -2} + 
          grid[[i[[1]], i[[2]]]]  (-4))], grid, {2}]]];

and

bilinearInterpolation = 
  Compile[{{array, _Real, 3}, {v, _Real, 1}}, 
   Module[{rows, cols, x = v[[2]], y = v[[1]], x1, x2, y1, y2, fQ11, 
     fQ12, fQ21, 
     fQ22},(*Get the dimensions of the array*){rows, 
      cols} = {Length[array], Length[array]};
    (*Clip points to the boundaries*)x = Clip[x, {1, cols}];
    y = Clip[y, {1, rows}];
    (*Find the bounding indices*)x1 = Floor[x];
    x2 = Ceiling[x];
    y1 = Floor[y];
    y2 = Ceiling[y];
    (*Get the values at the four corners*)fQ11 = array[[y1, x1]];
    fQ12 = array[[y2, x1]];
    fQ21 = array[[y1, x2]];
    fQ22 = array[[y2, x2]];
    (*Perform bilinear interpolation*)
    If[x2 == x1, 
     If[y2 == y1, fQ11, 
      1/(2  (y2 - y1))*(fQ11  (y2 - y) + fQ21  (y2 - y) + 
         fQ12  (y - y1) + fQ22  (y - y1))], 
     If[y2 == y1, 
      1/(2  (x2 - x1))*(fQ11  (x2 - x) + fQ21  (x - x1) + 
         fQ12  (x2 - x) + fQ22  (x - x1)), 
      1/((x2 - x1)  (y2 - y1))*(fQ11  (x2 - x)  (y2 - y) + 
         fQ21  (x - x1)  (y2 - y) + fQ12  (x2 - x)  (y - y1) + 
         fQ22  (x - x1)  (y - y1))]]]];

After that the main bottleneck is still the way how we draw and update SVG graphics (as well as WebSocket connection latency). However, these changes improves average FPS from up to on my Mac Air M1.17-2430-35

POSTED BY: Kirill Vasin

enter image description here -- you have earned Featured Contributor Badge enter image description here Your exceptional post has been selected for our editorial column Staff Picks http://wolfr.am/StaffPicks and Your Profile is now distinguished by a Featured Contributor Badge and is displayed on the Featured Contributor Board. Thank you!

POSTED BY: EDITORIAL BOARD
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract