CS184 Final Project: Spring 2022
Nathan Jew, Nicholas Nolte, Cindy Wang, Danny Yu
The primary goal of our project was to create a highly generalizable 3D particle simulator. Many physically accurate particle models of water, fire, and other systems have been made, but often alone in a vacuum. Our aim was to create a system that could handle many simulation types, as well as the interaction between them.
A tangential aim of this project was related to pixel-art animation. Pixel-art is difficult, and animating it in a believable way even more so, but the popular indie video game dead cells found a way around this by using a 3D rendering pipeline to create 2D pixel animations. So in addition to the particle simulation and physics, we attempt to append post-processing steps towards this secondary aim, with the hopes that the overall result will have a very unique, personalized aesthetic.
We designed our program around taking in JSON files to supply the parameters to define our simulation scenes. This allowed us to be flexible with our input conditions to support a wide variety of situations. Some of the parameters that we want to control, for example, are the number of particle clouds, how many particles are in each, the masses and radii, their colors, and unique collision and attraction properties between different particle types. We can also control parameters for our Marching Cubes algorithm (such as the resolution of the mesh) and the locations of primitives such as planes and spheres in the scene.
Once the particle start positions and class information has been loaded, the physics loop begins. A key feature of this simulation is the ParticleProperties class, which stores the forces received by particles of its type from other types. Because we have the potential for many different particle types to be used together, we use this as a way to keep track of the class properties since we cannot hardcode any interactions. This class maintains pointers to the necessary force functions, and also stores the force with which it operates between any 2 given particles.
The reason we have separate Particle and ParticleProperties classes is because there can be hundreds of individual particles, but many of them will share the same base physical properties. The ParticleProperties class abstracts away the static data which is consistent between particles, while the regular Particle class itself tracks properties which are unique to each individual particle (such as position and forces).
Our highlight demonstration for this project was a simulation containing fire, water, and steam. First, fire appears to blaze from the ground, only to be extinguished by water, before rising up as steam. In describing how this simulation works, we can showcase the inner workings of our simulation system.
Fire was modeled using both an external and internal force. In our system external forces are a constant force vector applied to the particle, while internal forces occur between particles. Each piece calculates the forces it receives from other particles, which is dependent on their type. Fire had an external force downward to simulate gravity, and internal upwards force that came from other particles of the same type. This internal force was also localized, meaning that only nearby particles affected it, and not all in the simulation. We added localized forces to reduce computation, as not all forces need to be calculated on every particle. This was done using box hashing similar to the one used in Project 4. Overall, the force on the fire particles would be:
$f = G$ $* <0, -1, 0> +$ $F * \sum_{n} \frac{<0, 1, 0>}{r}$
where $n$ is the set of particles in the same box as the current one and $r$ is the distance between them. Here $F$ and $G$ are strength constants defined by manual trial and error.
The water in the video starts a large distance above the fire and falls under the external force of gravity. Our water is modeled very simply, only being affected by particle to particle collisons and a localized internal attraction to approximate surface tension. One of the main features of water in this simulation is that when it collides with fire, both the fire and water particles turn into steam particles. This feature of particles transforming into other types is one of the key features we used to make interesting simulations.
Another major feature of our particle simulator was that the particles are rendered as a cohesive group with an overall shape, rather than as a bunch of separate points (which is how they exist in code). In order to do this, we opted to use the Marching Cubes algorithm, which is commonly used for this purpose.
The algorithm operates on a scalar field, which essentially means that it takes in a function where there is some scalar value associated with every 3D point in the world. Given that function, it can define a surface which separates high valued areas from low valued areas such that high values are "inside" of the mesh, and low values are "outside". For our purposes, this meant that we needed to first define a scalar field which had high values in areas with lots of points, and zero values elsewhere. For this, we found an article with this density-based scalar field, which first computes the density at each point, and then uses that density to determine the strength of the scalar field.
$W(r, h) = \frac{(h^2 - r^2)^3}{h^9}$
$\rho_i = \sum_j\frac{m_j}{\rho_j}W(x_j - x_i, h_j)$
$\phi(x) = \sum_j\frac{m_j}{\rho_j}W(x - x_j, h_j)$
where $W(r, h)$ is a helper function which outputs a value based on the distance between two points (inspired by this previous CS184 project), $\rho_i$ is the density for particle $i$ based on its neighbors, and $\phi(x)$ is the final scalar field (taking in input coordinate $x$). For $h$, we ended up using the diameter of the particles, which seemed to work pretty well.
Finally, when we applied the Marching Cubes algorithm, we got a mesh which separated our particle clusters from the rest of the space, resulting in a dynamic cloud shape surrounding the particles based on their radii. We adapted this implementation (specifically the non-recursive approach) for our project, which not only generates a mesh, but also computes vertex normals for smoother shading. However, in order to get the results we wanted, we also modified the algorithm so that the scalar field also tracked which particles were most prevalent at each location (in addition to a raw density value for the main algorithm to use). This allowed us to color the triangles based on the particle types present (and even apply different shaders), since there is a single mesh used for the entire particle simulation. In places where there are multiple particle types present, the colors blend slightly for a smoother appearance.
The final stage in our pipeline was shading and post processing. To make convincing pixel art, we believed that a cel-shaded look would pair well with the pixelated aesthetic, as a reduced color space would result in more well-defined coloring after pixelation was applied. We did also use some custom shaders for water and fire, but most of our other particles stuck with this cel-shaded style for this reason.
The pixelation effect was a bit more complicated than we anticipated. Since we were using Project 4 code, we weren't sampling individual pixels, but we still needed some way to capture the screen contents in order to reduce them into the pixelized space. To do this, we created a temporary framebuffer, which took the contents which would normally be displayed to the screen and stored them in a texture. This texture would have the same dimensions as the screen itself, and could then be projected onto a quad directly on the screen. Of course, before projecting it, we sampled each pixel of the render texture floored to the pixel resolution. Unlike downsampling from Project 1, this technique does not require any pixel averaging, since we wanted to maintain sharp edges.
Starting from our completed Project 4 code, we were able to build a multiclass particle simulator, and produce nearly a dozen distinct animations. In staying true to our title and goal, we have the final images here rendered with pixelation, accomplishing the toon-like appearance we were aiming for. One of the most impressive aspects of this project to us was the diversity of what we were able to create. From protons and electrons at the atomic scale, to a campfire being doused with water at the macroscopic, it is able to capture a wide array of possibilities.
The place with the most room for improvement in all of this is the realism of each individual simulation. Many projects work to render water, fire, snow and other materials with greater detail than ours. If we were to continue our projet, our goal would be to combine both the accuracy of those projects and the cross-class interactions. Nevertheless, we are very happy with the system were able to create, and the particle systems we created out of it.
Nathan: Adapted implementation of marching cubes to work with our system, including modifications to support particle coloring and custom shading. Wrote the code to generate the scalar field from the particle positions. Wrote and integrated the pixelation post-processing shader. Added miscellaneous features such as background coloring, spherical bounds, and velocity-based coloring. Edited the showcase videos.
Nick: Wrote physics simulation code, including force laws, particle collisions, particle transformations, primitive collisions, and the overall ParticleProperties infrastructure. Created parameters for several of the final simulations. Conceived the idea to use particle averaging for a smoother mesh. Implemented basic triangle-based rendering to allow for debugging while marching cubes was being made.
Cindy: Wrote the cel-shading code. Researched how to integrate post processing knowledge into the existing project framework using nanogui.
Danny: Wrote system to parse particle starting conditions and properties from JSON files. Researched techniques for how to apply the pixelation effect via post processing. Debugged major issues with marching cubes. Configured the webpage writeup and formatting.