Skip to main content.

Introduction

This project is the product of something that I have always wanted to write: a fluid simulator. Modeling a continuous substance has always fascinated me and I have been waiting for my chance to do it myself. That chance came when I decided to combine my Comp236 and Comp259 projects to produce a fluid simulator. I am quite pleased with the result of my work, though I will be the first to admit there are features I wish could have added and bugs that I would like to squash.

For this project I implemented a fluid simulator based on Jos Stam's paper "Stable Fluids" (ref. [2]). This simulator works in two and three dimensions using a grid-based system to model the continuous fluid. The velocity field is defined at the center of each square/cube and can be periodic or non-periodic. Liquids are defined as scalar fields defined in each square/cube and are advected and diffused throughout the grid.

Visualizing such a simulator can be tricky due to the large amount of data and its constant flux. In this application I have created several different ways to view the data as well as convenient ways to interact with it. I also allow for the dynamic changing of parameters such as grid size, dimensions, fluid parameters, and visualizations all while keeping memory usage to a minimum.

Features

Looking first at the user interface side of the application, one can introduce a force to any part of the simulation due to the selection technique. When the user clicks the left mouse button in the display window, I render the simulation grid as solid squares (cubes) with distinct colors. I then read back the color of the pixel under the user's mouse and can then translate that into which cube the user chose.

2D Grid, N=32 3D Grid, N=16
2D Grid, N=32 3D Grid, N=16

This technique allows the user to introduce a force into any square; I implemented it as applying a force in two directions away from the mouse in two dimensions and as applying a force into the grid cube in three dimensions. Also, the user may press the left mouse button to add density to a liquid in that square or cube. As another interface mode for the mouse, I implemented a virtual trackball and slider to visualize the data. The user may also start, pause, and reset the simulation using several buttons to aid in looking at the data at particular time steps.

Turning now to the visualization elements, the most basic way of visualizing the flow is to show the velocity at each grid element, creating a velocity field:

2D Velocity Field, N=32 3D Velocity Field, N=16
2D Velocity Field, N=32 3D Velocity Field, N=16

Velocity fields, however, are not very good at showing how the fluid movement changes a liquid. One of the most common tricks in visualizing two-dimensional fluid flows is to use a texture as the "liquid". More concretely, one places two fluids in the simulation with densities varying between 0.0 and 1.0. This is best illustrated in the following images:

First Texture Dimension Second Texture Dimension Both Texture Dimensions
First Texture Dimension Second Texture Dimension Both Texture Dimensions

One liquid varies linearly from 0.0 to 1.0 horizontally while another varies similarly in the vertical direction. The correspondence between these liquid densities and texture coordinates is quite easy to see. The advection of these fluids results in the similar displacement of the texture map as shown below.

Initial Texture Perturbed Texture
Initial Texture Perturbed Texture

One can continue this idea into three dimensions, as I have done, by putting the texture plane in each grid cell in the z-direction. A more general idea of doing this is to allow it to be planar with any of the XY, YZ, or XZ planes at any given time, and further one could interpolate values over the plane while moving it arbitrarily around the grid with arbitrary orientation. My code easily allows for such ideas though keeping it in one plane conveys the idea and I felt my time was better spent on other features. Below is a screenshot of several stacks of images in a N=16 grid:

3D Textures Texture at K=8
3D Textures Texture at K=8

Another method I used to visualize the liquid flow is by using isolines and isosurfaces. These were computed and displayed using an algorithm known as marching squares (the 2D version) and marching cubes (the 3D version). The idea behind marching squares is to iterate over each square in the grid (hence marching) and determine in a line corresponding to the desired value (the reference value) passes through the square. This is done one square at a time by denoting each of the squares' four vertices as positive if the value at the vertex is greater than or equal to the reference value and negative otherwise. Since there are four vertices and two possible states, this results in 2*2*2*2 = 16 possible ways a line (or lines) can pass through the square. These can be simply enumerated as shown below (image from [1]):

Marching Squares cases
Marching Squares cases

The cases 5 and 10 are termed "ambiguous" because either of the two sets of lines could be considered correct. One set breaks up the surface while the other joins the surface. This ambiguity is not bad at all since the discontinuity produced doesn't affect the overall idea of the surface. However, this is not the case when it comes to three dimensions and marching cubes. There are now eight vertices which means there are 256 different ways a surface can intersect each cube. However if you notice in the above picture, the top row is essentially a dual of the bottom row, i.e. Case 0 is similar to Case 15, Case 1 is like Case 14 and so on. Marching cubes exploits this and can turn the 256 states into only 15 using rotations and mirroring (image from [1]):

Marching Cubes cases
Marching Cubes cases

However implementing only these cases and then performing the required rotations and flips is difficult and tedious to do. As a result most people enumerate all 256 possibilities in a list and index into it to determine which edges to connect. The ambiguous cases throw a wrench into this as there are much more of them and they are relatively complicated to resolve correctly. Currently my marching cubes implementation does not consider the ambiguous cases as I simply did not have the time to implement it. Even though, the results are quite convincing with few obvious artifacts resulting from the ambiguous cases. I have shown below my implementation of both marching squares and marching cubes where the reference density is 0.5 or 50%:

2D Isobar, Texture 2D Isobar, Liquids 2D Isobar in 3D
2D Isobar, Texture 2D Isobar, Liquids 2D Isobar in 3D
Isosurface, 1 Liquid Isosurface, 1 Liquid Isosurface, 2 Liquids
Isosurface, 1 Liquid Isosurface, 1 Liquid Isosurface, 2 Liquids

As a simple extension to visualizing the real-time marching cubes, I added an option to export the current view to a Wavefront OBJ file. I had to negate the normals I used generated from the marching cubes algorithm for a reason I am not quite sure of. If both liquids are exported, they are both considered part of the same object in the file, though one idea for this to use material files to define these differently. However I do not have much experience with these and will leave it for another time.

Known Bugs

References

  1. D. Lingrand et al., "The Marching Cubes," http://www.essi.fr/~lingrand/MarchingCubes/accueil.html
  2. Stam, Jos "Stable Fluids," http://www.dgp.toronto.edu/people/stam/reality/Research/pdf/ns.pdf
  3. Bourke, Paul "Polygonising a Scalar Field" http://astronomy.swin.edu.au/~pbourke/modelling/polygonise/