note: please view this project at rachel.pt/cloth-simulator !
In this project I implemented a real-time cloth simulator that mimics cloth behavior using a system of discrete masses and springs. I built the data structure that represents these masses and springs, and added support for collision with other objects and self-collision using a hashmap of weights. I also wrote some GLSL shaders that model lambertian diffuse materials, blinn-phong materials, bump mapping and displacement mapping.
Creating the Masses & Springs
To start, I had to create an evenly-spaced grid of points, each with a mass value. (I called them point masses). I then created springs between each point mass, so that each point mass is connected to others around it. Springs had different types: "structural" springs connect a point mass to the masses to its left and above it, "shearing" springs connect a point mass to the masses to the diagonal upper right, and diagonal upper left, and "bending" springs connect a point mass to the masses two to the left and two above.
Below are some screenshots of the wireframe of the cloth, made up of the spring constraints I created. It forms a 8-directional grid between every point mass.
Below are some screenshots showing the difference between constraints. Notice the orientation!
Simulation via Numerical Integration
Now I had to actually update the positions of the point masses at each timestep, based on the forced applied to them by gravity and the springs connecting them. First, I iterated through each point mass and updated its force vector to the sum of all of our external accelerations (in our case, it was simply gravity). I then iterated through each enabled spring in the cloth and computed the force applied to the two point masses at its ends using Hooke's law. This used a spring constant ks to modify the overall strength of the spring force. I then iterated through each point mass again, and used verlet integration to use the sum of the spring forces and external forces to update their positions in 3D space. Finally, I made sure each position update didn't make the spring length exceed its 10% greater than its resting length.
Below is a video of me playing around with the spring constant ks. As you can see, increasing ks increases the overall robustness of the springs, making them more stiff. A very low ks makes the cloth behave as a thinner fabric would, like rayon or silk. A higher ks makes the cloth behave almost like leather or rubber. The default ks of 5000 is somewhere in between.
Here is a video of me playing around with the density of the cloth. This directly affects the weight of the fabric, so a higher density makes the cloth fall faster. A higher density would equate to something like chain mail, while a lower density would equate to something light and flowy like silk.
Here is a video of me playing around with the damping of the simulation. You can see that a higher slider value equates to a slower and less noisy simulation, "calming down" the cloth. A lower value equates to a faster, more jumpy and noisy simulation.
Below is a screenshot of the cloth pinned by all 4 corner vertices, in its final resting state.
Handling Collisions with Other Objects
First, I added support for colliding the cloth with spheres. I made a function in a sphere class that took in a point mass and determined whether or not the point mass was inside the sphere, based on whether the distance between the point mass and the sphere's center is less than the sphere's radius.
If the point mass was inside the sphere, I created a correction vector to bump the point mass up to the surface of the sphere, plus a little offset. I then added the correction vector to the point mass's position and updated it.
Below are some screenshots of the cloth resting on a sphere, with different spring constants. Like above, you can see that a higher ks equates to a thicker and stronger cloth, and a lower ks equates to a thinner and weaker cloth.
Next, I added support for collisions with stationary planes. I created a function in a plane class that determined if a given point mass had intersected the plane. I did this by taking the dot product of the vector from a point on the plane to the point mass's position, and the plane's normal. If this value was less than 0, the plane was on the "wrong" side of the plane. Like before, I made a correction vector to bump the point mass up to a little bit above the surface of the plane, and updated the point mass's position using this vector.
Below is a screenshot of the cloth in its resting state above the plane.
To avoid the cloth intersecting with itself, I created a hashmap of point masses. I basically divided up the 3D space into a series of buckets - and if two point masses were placed in the same bucket, then they were close enough together to possibly intersect. This is much faster than the (n^2) time it would take to test if every point mass collided with every other point mass at each time step - now, we're only testing collisions if two point masses have a high probability of colliding. At each time step, I divided up the point masses into their given buckets, and then iterated through each point mass. For each neighboring point mass in the same bucket as the current point mass, I checked whether a collision had taken place by testing the distance between them against a minimum distance. For two colliding point masses, I calculated a correction vector to push them both apart, and applied the vector to their positions, effectively keeping them from colliding.
Below are some screenshots of my algorithm in action - the vertical cloth falls onto the horizontal ground plane, and avoids colliding with itself by crumpling up and then spreading out.
Below, you can see that modifying the density of the cloth can create different results in the resting position of the cloth. A lower density created a calmer and less wiggly cloth, probably because the effects of gravity were less pronounced.
And here, it's clear that a higher spring constant creates a smoother and less jumpy cloth. This is probably because the stronger springs respond less to small variations in the force applied to them.
Making some GLSL shaders
Now it's time to create some shaders to show off the cloth! (woohoo)
In essence, a shader is a piece of software that tells a rendering program how to display a 2D or 3D object on the screen. So it determines how the object appears to the viewer. Shaders can do a whole number of things - determine the color, texture and material of the object or simulate cool effects like lava or rippling water.
I used GLSL to write some basic vertex and fragment shaders to model some different materials for my cloth. A vertex shader can manipulate the attributes of the vertices of the object, and pass these values to the fragment shader, which actually handles rendering the colors of the pixels on the screen.
To begin, I created a blinn-phong shader. The blinn-phong model combines four components together to create a final result: an ambient light component, a diffuse component, (which is simply lambertian shading), and a specular component.
The ambient component is simply a flat shader, the same color for the entire object regardless of the lighting conditions. The diffuse component is simply the maximum of 1) the dot product between the direction towards the light source and the surface normal, and 2) 0. This provides a nice matte surface on which to layer the specular shading. The specular component uses a half-vector, which is the normalized addition of the incident light angle and the angle of view. The result of the specular component is then the max of 1) the dot product of the surface normal and the half-vector, and 2) 0. This value is then raised to a power, which is inversely proportional to the shininess of the specularity. All components are also multiplied by scalars that can control their contribution to the final output, and the illuminance of the surface.
In my implementation, I also multiplied everything by an input color, so as to allow the user to choose their own color for the shader. Below you can see a breakdown of the shader, with each component isolated, and the final result at the end.
Next, I created a fragment shader that used the texture() function to load in a custom texture and use the object's UV coordinates to display an image texture on the object. Here you can see the cloth with a lovely shingle texture:
Next I implemented a bump-mapping fragment shader. This used the B&W "height" values in a bump map texture to perturb the surface normals of my mesh, and fed these perturbed normals into my blinn-phong shader to get the final result, which adds some fake surface details. Below is a screenshot of the cloth with a patterned fabric bump map on it:
Next, I created a displacement vertex shader to go along with the bump map fragment shader. Instead of modifying the surface normals, this actually moved the positions of the vertices on the mesh along their normal vectors according to the B&W "height" values of the input height map. Both were also modified by scalars input in the editor. Below are some screenshots of the shader in action on the sphere:
As you can see, while the bump mapping fakes surface detail by modifying the normals and thus the light/dark values of the surface of the object, the displacement mapping actually modifies the surface (the actual vertex positions). This results in a changed silhouette of the object.
Increasing the resolution of the sphere makes the sphere smoother and the displacement effect more clear. With more resolution comes more vertices to move around according to the texture:
Finally, I created a fragment shader that modeled a mirror-like effect with a spherical HDRi texture. I calculated the outgoing ray from the camera, reflected it across the surface normal, and used a texture sampler to sample the environment map at that angle for the resulting color value. Below are the cloth and the sphere with this shader.