Stable Fluids Smoke

Jessie Liu, Luca Herden, Nada Hameed, Sarthak Agrawal

Link to webpage: cal-cs184-student.github.io/hw-webpages-nadahameed/final
Link to GitHub repository: github.com/jessieewoo/184-final-project
Video: Demo (Google Drive)
Presentation: Google Slides

Abstract

Our project is a 2D and 3D smoke simulator using stable fluids through Navier-Stokes. The smoke can emit from a certain shape, via hardcoded cells, typed text, or an imported image. Via the GUI, the user can control certain traits of the smoke: the emit rate (how much new smoke you inject each timestep at every emitter cell), the buoyancy (how strongly smoke rises), the decay rate (how fast smoke fades everywhere after each step), the color, and the strength of the cursor pushing on it. There are also a few toggles. Toggling '3' switches from 2D to 3D, and toggling 's' toggles fluid 3D and analytic 3D. The reason for the separate 3D versions is because fluid 3D is a bit more computationally intensive. We include an analytic implementation is included for sake of comparison, to show tradeoff between physical accuracy and cost/computation.


Technical approach

Implementation

Our implementation stemmed from HW4, cloth simulator. This was helpful because that already had a GUI in place we could use, as well as updating frames and rendering infrastructure. We just built on top of that with fluid simulation logic. We replaced the PointMass class with a FluidGrid class, and we replaced clothSimulator.cpp/h with smokeSimulator.cpp/h. There, we implemented our full rendering functionality, where FluidGrid was our solver.

2D

To implement 2D fluid simulation for our smoke, we created a grid of cells. Each cell keeps track of velocity (how fast the air is moving and in what direction), and how much smoke there is. Every frame, we add new smoke and wind, and then adjust and blend it with neighboring cells so that it looks like a real fluid and doesn't falsely compress or expand in each cell, and finally render the smoke density.

We used the fluid simulation implementation from Stam's paper Stable Fluids where we maintain a velocity field and a passive scalar (smoke field) on a cell-based grid. Each timestep, we update velocity via the Navier-Stokes equations and then update the movement of the density by that velocity. We have a class called LetterMask that determines which cells have smoke in them, ie which cells are on. We do this so that we can incorporate shapes into our simulation. This way, the smoke starts out in a certain input shape. We incorporate extra sources/forces based on user inputs in the GUI, like buoyancy. We also incorporate velocity from the mouse wind (which simulates pushing the smoke around).

3D analytic

To implement 3D, we first implemented it analytically. In 2D, we solved for the wind every frame, following Stam's method. In 3D analytic, we do not run that heavy velocity solver. Instead, the wind at any point is solved using sine and cosine of position and time, plus an upward factor/force that scales with the buoyancy slider. So the smoke is designed to look swirly and modified over time, not produced by solving Navier-Stokes on the grid. When we say analytic, we mean that the wind is an explicit field that we can evaluate, not a simulated velocity field that is advanced every time step. The smoke still moves in a believable way, but it is far less computationally heavy. Every step, we carry velocity along that wind, then add new smoke near emitters, lightly blur with neighbors (so it looks smooth), and then fade with decay rate. To render 3D in our simululation, we draw a 3D density texture: the fragment shader does ray marching (taking many samples through the volume) so the screen shows thickness and depth, not a single flat image. For each pixel, the shader iterates through the volume and builds up color and opacity, which makes the smoke look thick and layered.

3D via Stam's

For 3D fluid rendering, we extended the same idea with Stam's stable fluids from 2D simulation, but to a cube of cells. Each cell stores three wind components (x, y, and z) plus smoke density. Every timestep, we update that 3D wind the same way we did in 2D. We blend cells (what does visocity mean in this context), correct the wind so it doesn't falsely bunch up or vanish in each cell, then move the wind/smoke accordingly and correct again. After that, we move the smoke density along the final 3D wind, blur it slightly between neighbors, and fade it with decay. New smoke still comes from emitter cells, which we build from our letter mask like before. We copy the 2D shape into 3D by created a thin layer of depth cells.

This is different from our analytic 3D mode, where the wind is not simulated (it came from a fixed math formula). In fluid 3D, the wind is actually stepped forward on the grid each frame, closer to the physical picture aka a bit more physical accurate. It only changes how density is being used, and the rest of the processes are the same.

Runtime comparison


Problems encountered


Lessons learned

We learned that getting a fluid simulation to look realistic was not only about Navier Stokes math. A lot of the work was in making simulation, rendering, and input all agree on coordinate systems and update timing. We also learned that 2D and 3D versions of the same idea in theory can actually look very different when actually implemented.


Results

Hardcoded images: 4 vertical bars

2D
3D analytic
3D fluid

Text

Text: 2D
Text: 3D analytic
Text: 3D fluid

Shapes

Flower: 2D
Flower: 3D analytic
Flower: 3D fluid

Photo: Owl

Original photo
2D
3D analytic
3D fluid

References