CS 184: Computer Graphics and Imaging, Spring 2023

Final Project: Particle-based Fluid and Collision Simulation with Realistic Rendering

Justin Shao, Jierui Xu, Tinna Liu, Yiwei Zhou


Abstract

Fluids, in particular liquids such as water, are responsible for many visually rich phenomena, and simulating them has been an area of long-standing interest and challenge in computer graphics. This final project focuses on the implementation of a particle-based fluid simulation and rendering system. The simulation involves computing and updating particles' positions, velocity, and forces by tracking pressure and viscosity. We utilized kernel functions, such as Poly6 and Spiky kernels, and the SPH algorithm to estimate each particle' state at each time frame. We also re-index the particle's memory position to increase cache efficiency. The rendering aspect offers four modes: particle rendering, wireframe/mesh rendering, Phong rendering, and MirrorPhong rendering. The fluid surface rendering is achieved through the marching cubes algorithm, which outputs a polygonal 3D surface mesh. This project is especially significant in providing insights into fast particle simulation and rendering techniques. Realistic water rendering has potential applications in gaming, engineering, and graphics research.

Project Slide Link Describe Video

Technical Approach

We used particle-based fluid simulation, and we split the problem into two main parts: simulation and rendering. Simulation refers to the physical simulation of each fluid particle, which includes computing and updating particles' positions, velocity, and forces by tracking pressure, viscosity, and vorticity. Rendering refers to the visual rendering in GUI, which supports multiple modes including particle rendering, wireframe/mesh rendering, and Phong rendering. Thus, we will separate the discussion of our technical approach into two subsections, one for simulation and the other for rendering.

Simulation:

For our implementation, the cloth simulator (project 4) codebase served as a convenient starting point since we are implementing a particle-based Lagragian fluid simulation. We implemented the Fluid class to define fluids, which defines a fluid's behavior under the forces of gravity, pressure, and viscosity. Similar to how Cloth objects save a list of PointMasses, our Fluid objects holds a list of Particles that each represent a blob of liquid, recording information of their density, pressure, location, velocity, and forces.

Finding Neighbors:

At the beginning of each timestep, we calculate the neighbors of each particle. To do this, we divide the space into cells of dimensions (R * R * R), and perform spatial hashing. We first build the spatial map using the hash value of the coordinates of the cell that each particle is contained in. Then for each particle, we find its neighbors (defined by having distance < R and not being the particle itself) by checking the neighboring 27 cells (including the cell it resides in). These neighboring particles are recorded within a vector that is used for later calculations within this timestep.

Using Kernel functions:

For the following value updates, we will use kernel functions W to estimation. These kernel functions use the distance of particles to provide a weight for how influential a particle should be towards another particle. In our simulation, we followed Muller et al. (2003) and used three different kernel functions: Poly6 kernel for density estimation, Spiky kernel for gradient, and another kernel for laplacien estimations.

\[\begin{aligned} W_{poly6}(\mathbf{x}_i - \mathbf{x}_j, R) &= \begin{cases} \frac{315}{64 \pi R^9} (R^2 - \|\mathbf{x}_i - \mathbf{x}_j\|^2)^3 & \text{if } \|\mathbf{x}_i - \mathbf{x}_j\| \leq R \\ 0 & \text{otherwise} \end{cases} \end{aligned}\]

\[\begin{aligned} \nabla W_{spiky}(\mathbf{x}_i - \mathbf{x}_j, R) &= \begin{cases} -\frac{45 (R - \|\mathbf{x}_i - \mathbf{x}_j\|)^2}{\pi R^6 \|\mathbf{x}_i - \mathbf{x}_j\|} * (\mathbf{x}_i - \mathbf{x}_j) & \text{if } \|\mathbf{x}_i - \mathbf{x}_j\| \leq R \\ 0 & \text{otherwise} \end{cases} \end{aligned}\]

\[\begin{aligned} \nabla^2 W(\mathbf{x}_i - \mathbf{x}_j, R) &= \begin{cases} \frac{45* (R - \|\mathbf{x}_i - \mathbf{x}_j\|)}{\pi R^6} & \text{if } \|\mathbf{x}_i - \mathbf{x}_j\| \leq R \\ 0 & \text{otherwise} \end{cases} \end{aligned}\]

Updating density and calculating pressure:

Using the neighborhood arrays, we update i, the density of each particle. Here we use the standard SPH density estimator. Note that we are explicitly including the first term since the particle itself should contribute towards the density:
\[\rho_i=m_iW(x_i-x_i,R)+\sum_{j} m_j*W(x_i-x_j,R)\]
We can then calculate the pressure at each particle using this method suggested by Muller et al (2003):
\[p_i = k(\rho_i-\rho_o)\]

Calculating forces:

With the pressure and density calculated for each particle, we can now calculate the pressure forces and viscosity forces acting on each particle. For reference, we use the pressure force estimator suggested in the SPH STAR report (2014) and the viscosity estimator suggested in Muller et al (2003). Note that for both these forces, we do not include the particle itself since the particle should not contribute to forces on itself.

The total force would be: \[f_{total} = f_{pressure} + f_{viscosity} + f_{ext}\]

This force provides us with an acceleration, which allows us to perform forward integration. Note that the instability issue of forward integration is addressed by viscosity acting as a damping force:

Lastly, we check for particles that cross barriers. If a particle goes through a plane, we move it to the correct side and adjust its velocity by adding a component to the direction of the plane’s normal vector with a magnitude proportional to its original velocity in that direction.

Adjustments:

Rendering

Particle Rendering:

Wireframe, Phong, and MirrorPhong Rendering Using Marching Cubes:

All ways to Intersect a Cube

Problems Encountered:

Since we build our project on top of project 4, we need to understand the workflow of the clothSimulator. At first we were not sure where to build the mesh for rendering, and how to pass the mesh to the shaders. By reading over the original main.cpp and clothSimulator.cpp, we understood where our fluid object should be initiated, how the scene was drawn to the GUI, and how different shaders are invoked. We also had problems setting the point size when rendering in particle mode, since we did not know that we needed to add and upload an additional uniform variable for point size. Another big issue we encountered was computing vertex normals. The mesh we built does not follow a halfedge data structure, and it is also very hard to find neighboring triangles given what we have since the mesh is a 3D polygon. We solved the problem by using the vertex normal formula we found in [Fleck 2008], which produces the desired result. We also had problems with C++ pointers and memory allocation, which we spent a lot of time debug using the Xcode debugger and print statements.

Lessons Learned:

Results

Particle Rendering
Wireframe Rendering
Phong Rendering
MirrorPhong Rendering
Wireframe mesh detail

References

Contributions from each team member