For our CS184 final project, we implemented realistic smoke simulation in a 2D grid based environment. We used OpenGL to render our simulation live on a window. Mouse input was caught using callback functions to allow users to create smoke and affect the path/initial velocity of the smoke. We also integrated the nanogui library into our project, allowing users to adjust various parameters including heat, density, color and so on. OpenMP was added to accelerate the simulation part of the project, and shader programs in OpenGL were added to accelerate the rendering part.
We first retrieve the density and temperature each each grid location from our simulation. We convert those into HSV and then into RGB colors for each grid location, depending on the density and temperature. We create a texture (a 2D buffer array) from those colors, and finally we display the texture on the screen using OpenGL.
For our simulation, we kept track of three quantities: smoke density, temperature, and velocity. The grid, which represented the entire space of the simulation, was split into individual cells. Each cell contained the density, temperature, and velocity of the smoke at that specific location. By making the number of cells relatively large, the simulation became less pixelated and more realistic.
We used http://developer.download.nvidia.com/books/HTML/gpugems/gpugems_ch38.html as our primary resource for our simulation equations. Most of the equations on this website was derived from http://www.dgp.toronto.edu/people/stam/reality/Research/pdf/GDC03.pdf (Stam's paper). We implemented most of the features in the webpage, with the exception of vorticity confinement. In addition, we went above and beyond in implementing more ways users can interact with our simulation.
Advection is movement of certain quantities along the velocity field. Recall that in our simulation, we advect smoke density, temperature, and velocity. It is necessary to advect velocity in order to realistically simulate the flow of the smoke.
We use reverse advection technique to ensure stability. (Stam’s method) For each grid location/cell, we trace its velocity backwards. We linearly interpolate the four smoke quantities closest to the position we end up. We set the quantity of the original cell to the interpolated quantity. The reverse advection technique improves stability in the simulation and makes sure the quantities do not diverge as time goes on.
In the above equation, q is the quantity we want to advect, x is a location in our grid, t is the current time, δt is the duration of a timestep, and u is the velocity field.
Diffusion occurs when smoke moves from areas of higher concentration to areas of lower concentration. Viscosity is the natural resistance of fluids to flow. We model viscous diffusion by updating the velocity field to simulate the effects of smoke diffusing outwards. We solve the below partial differential equation to determine the updates to the velocity field. In the equation, is the velocity and is the viscosity term. In order to actually solve the equation, we use the Jacobi iteration technique.
The Jacobi iteration technique involves iterating several times and continuously updating the values of the grid. In our simulation, the number of iterations was set default at . For viscous diffusion calcualations, the and term is the velocity at location , term is , and term is . For pressure calculations, the term is the pressure at location is the divergence of the velocity field at location , term is , and term is . The higher the number of iterations, the more realistic our simulation becomes, but the longer the computation time of the simulation becomes as well.
Buoyant forces result from smoke traveling from areas of high temperature to lower temperature. Buoyant forces result in the smoke traveling up. It is the equivalent of hot air rising and cold air falling. We also model a gravitational force on the smoke that pulls the particles down. We combine the two forces together in the above equation. In equation, is the density of smoke, is the temperature of smoke, is the ambient temperature, is a vector pointing up, and are adjustable parameters. Notice that the gravitational force counteracts the buoyant force, although the buoyant force is usually much higher than the gravitational force.
We also add any external forces in this step. The external forces come from the user created velocity fields. When the user presses m, they can drag and create their own "wind tunnels". We generate the input force at each cell location from the user mouse drags. In the simulation, all we do is add the input force to the velocities at each grid location, scaled by an external force parameter.
The projection step ensures that the velocity field is mass conserving and the flow is incompressible. An incompressible fluid and mass conserving field ensures that the total density of the smoke in the simulation doesn't increase or decrease on its own. It also ensures that the velocity field does not explode or die down. This improves stability of the simulation and makes the simulation that much more realistic.
We use Helmholtz–Hodge decomposition for our projection step. This decomposition states that our velocity can be decomposed into a divergence free field and a gradient field: . For our projection step, we first calculate the divergence of the velocity field at each grid location. This involves finding the difference in the velocity of the left and right cells, adding that to the difference in velocity in the top and bottom cell, and scaling the result be a factor (0.5). Once we have the divergence, we then calculate the pressure at each grid location using the Jacobi technique as described earlier. Finally, we subtract the gradients of the pressure from the vector field at each grid location. The gradients of the pressure is found by taking the slope/rate of change of the pressure at each grid location.
Below are the three steps for projection.
We generate smoke by tracking the clicks and drags of mouse. Callback funtions are used to catch the cursor movement (static mouse clicks are handled separately) and set density and temperature fields correspondingly. To better simulate the emission of smoke, we update the density and temperature fields of all the grids in a certain distance from the mouse using a falloff equation. We add a quadratic fall-off to the emission fields, similar to the definition of irradiance. This way, smoke further away from the mouse click are less dense and less hot.
Additionally, the dragging of the mouse is tracked for updates to velocity. In the callback function, we update the velocity fields of the cells in the grid that are a certain distance from the mouse, using a weighted average of the previous velocity and the mouse's velocity: . Here, mouse_velocity is proportional to direction of mouse, and is an adjustable parameter in .
Finally, we add HSV color fields to the smoke, which is an perceptually uniform and intuitive color model. We set HUE
as a range of value determined by the temprature, and the color wheel is used to select the center the range of values. We set Satuaration
to , which represents primary colors. We set Value
, which represents the brightness, to the density of the smoke naturally.
The HSV color values are finally transformed to RGB and displayed on screen.
The rendering part can be separated into three stages:
Specifically, for the second step, we create a char array with capacity , where and are width and height of the grid, respectively. We first calculate the HSV value of each grid. We then change the HSV fields to RGB fields, and save the RGB color to the array, which is basically a texture map. After that, we pass the array to fragment shader as a 2D sampler and render the grid.
We added nanogui to our project from scratch. Specifically, we added slider widgets for configuring smoke parameters and a color wheel widget for selecting the color of the smoke. Below is a screenshot of our nanogui elements.
We accelerate the simulation in order to achieve a frequency of more than 60 FPS. To do so, we use the below methods.
#pragma
headers before each time-consuming loops of simulation part. We made sure to not parallelize the time steps by accident. Just by doing this, we got an acceleration of 4-5 times on a 6-core Macbook Pro.From this project, we learned how to realistically simulate fluids and use OpenGL for rendering graphics. As a result, we gained a greater intuition on the different techniques used to solve partial differential equations. In addition, we gained a lot of experience working with and organizing a relatively large C++ project. In the future, it probably would have been better to think about the overall structure and organization of the project before writing any of the actual code. Finally, we learned how to use and integrate different libraries such as nanogui for GUI and OpenMP for acceleration.
Video demo:
Chapter 38. Fast Fluid Dynamics Simulation on the GPU
Real-Time Fluid Dynamics for Games
Eric Ying: Worked primarily on the simulation. Created the grid model and implemented the various equations for fluid simulation such as: advection, diffusion, buoyant force, external force, and projection. Also worked on mouse input for smoke generation and creation of wind tunnels.
Yuanhao Zhai: Worked on the rendering aspect of the project, especially writing OpenGL code. Worked on implementing and organizing the different callback functions in our code. Also worked on integrating nanogui elements into the project, allowing for user input.
Fangzhou Lan: Worked on accelerating the performance of the simulation using OpenMP. Also worked on aspects of the project related to smoke color, such as implementing HSV colors, determining the color of the smoke, and creating the color picker widget. Also worked extensively on reorganizing and refactoring code, giving advice on using C++.