├── .gitignore ├── include └── Particle.h ├── LICENSE.md ├── Makefile ├── README.md └── src └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | pcisph 3 | .DS_Store 4 | *.pdf -------------------------------------------------------------------------------- /include/Particle.h: -------------------------------------------------------------------------------- 1 | #ifndef __PARTICLE_H__ 2 | #define __PARTICLE_H__ 3 | 4 | #include 5 | using namespace std; 6 | 7 | #include 8 | using namespace Eigen; 9 | 10 | // Lagrangian particle 11 | class Particle 12 | { 13 | public: 14 | Particle(Vector2d _x) : x(_x), v(0.0f, 0.0f), m(PARTICLE_MASS), p(0.0f), pv(0.0f), d(0.0f), dv(0.0f), n(NULL) {} 15 | // position, velocity, and mass 16 | Vector2d x; 17 | Vector2d v; 18 | double m; 19 | // pressure, density, and their variations 20 | double p; 21 | double pv; 22 | double d; 23 | double dv; 24 | // next particle in grid cell linked list 25 | Particle *n; 26 | 27 | constexpr const static double PARTICLE_MASS = 1.0f; 28 | }; 29 | 30 | struct Neighborhood 31 | { 32 | public: 33 | Neighborhood() : particles(MAX_NEIGHBORS), r(MAX_NEIGHBORS), numNeighbors(0) {} 34 | vector particles; 35 | vector r; 36 | unsigned int numNeighbors; 37 | 38 | const static unsigned int MAX_NEIGHBORS = 64; // by grid definition 39 | }; 40 | 41 | #endif -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Lucas V. Schuermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ####################################################################################################### 2 | 3 | # Mac OS X 4 | PCISPH_INCLUDE_PATH = -I/usr/local/include/ 5 | PCISPH_LIBRARY_PATH = -L/usr/local/lib/ 6 | PCISPH_OPENGL_LIBS = -framework OpenGL -framework GLUT 7 | 8 | # # Linux 9 | # PCISPH_INCLUDE_PATH = 10 | # PCISPH_LIBRARY_PATH = 11 | # PCISPH_OPENGL_LIBS = -lglut -lGL -lX11 12 | 13 | # # Windows / Cygwin 14 | # PCISPH_INCLUDE_PATH = -I/usr/include/opengl 15 | # PCISPH_LIBRARY_PATH = -L/usr/lib/w32api 16 | # PCISPH_OPENGL_LIBS = -lglut32 -lopengl32 17 | 18 | ####################################################################################################### 19 | 20 | TARGET = pcisph 21 | CC = g++ 22 | LD = g++ 23 | CFLAGS = -std=c++11 -O3 -Wall -Wno-deprecated -pedantic -Wno-vla-extension $(PCISPH_INCLUDE_PATH) -I./include -I./src -DNDEBUG 24 | LFLAGS = -std=c++11 -O3 -Wall -Wno-deprecated -Werror -pedantic $(PCISPH_LIBRARY_PATH) -DNDEBUG 25 | LIBS = $(PCISPH_OPENGL_LIBS) 26 | 27 | OBJS = obj/main.o 28 | 29 | default: $(TARGET) 30 | 31 | all: clean $(TARGET) 32 | 33 | $(TARGET): $(OBJS) 34 | $(LD) $(LFLAGS) $(OBJS) $(LIBS) -o $(TARGET) 35 | 36 | obj/main.o: src/main.cpp include/Particle.h 37 | mkdir -p obj 38 | $(CC) $(CFLAGS) -c src/main.cpp -o obj/main.o 39 | 40 | clean: 41 | rm -f $(OBJS) 42 | rm -f $(TARGET) 43 | rm -f $(TARGET).exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `pcisph` is a predictive-corrective smoothed particle hydrodynamics (SPH) implementation in 2D for CS4167 final project. See [here](https://github.com/cerrno/pcisph-wasm) for a more recent implementation in Rust including a parallelized solver. 2 | 3 | For further information, please see SPH tutorials on [my website](https://lucasschuermann.com/writing), including an introduction to [SPH math](https://lucasschuermann.com/writing/particle-based-fluid-simulation) and a [simple SPH solver](https://lucasschuermann.com/writing/implementing-sph-in-2d). 4 | 5 | ## Running 6 | ```bash 7 | # install dependencies (debian/ubuntu) 8 | apt install libopengl-dev freeglut3-dev libeigen3-dev 9 | 10 | # uncomment header in `Makefile` depending on platform 11 | make 12 | 13 | # launch demo 14 | ./pcisph 15 | ``` 16 | 17 | ## Demo video 18 | [![Demo video](http://img.youtube.com/vi/_Kxp5dJ7HM8/0.jpg)](http://www.youtube.com/watch?v=_Kxp5dJ7HM8 "Predictive-Corrective Incompressible SPH") 19 | 20 | ## License 21 | This project is distributed under the [MIT license](LICENSE.md). 22 | 23 | ## Note 24 | This solver is not exactly PCISPH, but can be viewed as 1-iteration of SPH relaxation plus sub-stepping. The “prediction-relaxation” scheme of my implementation actually comes mainly from the (much easier to follow) paper ["Particle-based Viscoelastic Fluid Simulation”](https://dl.acm.org/doi/10.1145/1073368.1073400), as opposed to ["Predictive-Corrective Incompressible SPH”](https://dl.acm.org/doi/10.1145/1576246.1531346). 25 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #define GL_SILENCE_DEPRECATION 2 | #if __APPLE__ 3 | #include 4 | #else 5 | #include 6 | #endif 7 | 8 | #include 9 | #include 10 | using namespace std; 11 | 12 | #include "Particle.h" 13 | typedef Matrix Vector2ui; 14 | 15 | // rendering projection parameters 16 | const static unsigned int WINDOW_WIDTH = 800; 17 | const static unsigned int WINDOW_HEIGHT = 600; 18 | const static double VIEW_WIDTH = 12.5f; 19 | const static double VIEW_HEIGHT = WINDOW_HEIGHT * VIEW_WIDTH / WINDOW_WIDTH; 20 | 21 | // global parameters 22 | const static unsigned int MAX_PARTICLES = 50 * 50; 23 | const static unsigned int fps = 40; 24 | const static Vector2d g(0.0f, -9.81f); 25 | static vector boundaries = vector(); 26 | const static double EPS = 0.0000001f; 27 | const static double EPS2 = EPS * EPS; 28 | 29 | // solver parameters 30 | const static unsigned int SOLVER_STEPS = 10; 31 | const static double REST_DENSITY = 45.0f; // 82.0f; 32 | const static double STIFFNESS = 0.08f; 33 | const static double STIFF_APPROX = 0.1f; 34 | const static double SURFACE_TENSION = 0.0001f; 35 | const static double LINEAR_VISC = 0.25f; 36 | const static double QUAD_VISC = 0.5f; 37 | const static double PARTICLE_RADIUS = 0.03f; 38 | const static double H = 6.0f * PARTICLE_RADIUS; // smoothing radius 39 | const static double DT = ((1.0f / fps) / SOLVER_STEPS); 40 | const static double DT2 = DT * DT; 41 | const static double KERN = 20. / (2. * M_PI * H * H); 42 | const static double KERN_NORM = 30. / (2. * M_PI * H * H); 43 | 44 | // global memory allocation 45 | static unsigned int numParticles = MAX_PARTICLES; 46 | static vector particles = vector(); 47 | static vector nh(numParticles); 48 | static vector xlast(numParticles); 49 | static vector xprojected(numParticles); 50 | 51 | // gridding parameters 52 | static const double CELL_SIZE = H; // set to smoothing radius 53 | static const unsigned int GRID_WIDTH = (unsigned int)(VIEW_WIDTH / CELL_SIZE); 54 | static const unsigned int GRID_HEIGHT = (unsigned int)(VIEW_HEIGHT / CELL_SIZE); 55 | static const unsigned int NUM_CELLS = GRID_WIDTH * GRID_HEIGHT; 56 | 57 | // gridding memory allocation 58 | static vector grid(NUM_CELLS); 59 | static vector gridIndices(MAX_PARTICLES); 60 | 61 | void GridInsert(void); 62 | 63 | void InitSPH(void) 64 | { 65 | boundaries.push_back(Vector3d(1, 0, 0)); 66 | boundaries.push_back(Vector3d(0, 1, 0)); 67 | boundaries.push_back(Vector3d(-1, 0, -VIEW_WIDTH)); 68 | boundaries.push_back(Vector3d(0, -1, -VIEW_HEIGHT)); 69 | 70 | cout << "grid width: " << GRID_WIDTH << endl; 71 | cout << "grid height: " << GRID_HEIGHT << endl; 72 | cout << "cell size: " << CELL_SIZE << endl; 73 | cout << "num cells: " << NUM_CELLS << endl; 74 | 75 | Vector2d start(0.25f * VIEW_WIDTH, 0.95f * VIEW_HEIGHT); 76 | double x0 = start(0); 77 | unsigned int num = sqrt(numParticles); 78 | double spacing = PARTICLE_RADIUS; 79 | cout << "initializing with " << num << " particles per row for " << num * num << " overall" << endl; 80 | for (unsigned int i = 0; i < num; i++) 81 | { 82 | for (unsigned int j = 0; j < num; j++) 83 | { 84 | particles.push_back(Particle(start)); 85 | start(0) += 2.0f * PARTICLE_RADIUS + spacing; 86 | } 87 | start(0) = x0; 88 | start(1) -= 2.0f * PARTICLE_RADIUS + spacing; 89 | } 90 | cout << "inserted " << particles.size() << " particles" << endl; 91 | 92 | GridInsert(); 93 | } 94 | 95 | void GridInsert(void) 96 | { 97 | for (auto &elem : grid) 98 | elem = NULL; 99 | for (auto &p : particles) 100 | { 101 | auto i = &p - &particles[0]; 102 | unsigned int xind = p.x(0) / CELL_SIZE; 103 | unsigned int yind = p.x(1) / CELL_SIZE; 104 | xind = std::max(1U, std::min(GRID_WIDTH - 2, xind)); 105 | yind = std::max(1U, std::min(GRID_HEIGHT - 2, yind)); 106 | p.n = grid[xind + yind * GRID_WIDTH]; 107 | grid[xind + yind * GRID_WIDTH] = &p; 108 | gridIndices[i] = Vector2i(xind, yind); 109 | } 110 | } 111 | 112 | void ApplyExternalForces(void) 113 | { 114 | for (auto &p : particles) 115 | p.v += g * DT; 116 | } 117 | 118 | void Integrate(void) 119 | { 120 | for (unsigned int i = 0; i < particles.size(); i++) 121 | { 122 | auto &p = particles[i]; 123 | xlast[i] = p.x; 124 | p.x += DT * p.v; 125 | } 126 | } 127 | 128 | void PressureStep(void) 129 | { 130 | for (unsigned int i = 0; i < particles.size(); i++) 131 | { 132 | auto &pi = particles[i]; 133 | 134 | Vector2ui ind = Vector2ui(gridIndices[i](0), gridIndices[i](1) * GRID_WIDTH); 135 | nh[i].numNeighbors = 0; 136 | 137 | double dens = 0.0f; 138 | double dens_proj = 0.0f; 139 | for (unsigned int ii = ind(0) - 1; ii <= ind(0) + 1; ii++) 140 | for (unsigned int jj = ind(1) - GRID_WIDTH; jj <= ind(1) + GRID_WIDTH; jj += GRID_WIDTH) 141 | for (Particle *pgrid = grid[ii + jj]; pgrid != NULL; pgrid = pgrid->n) 142 | { 143 | const Particle &pj = *pgrid; 144 | Vector2d dx = pj.x - pi.x; 145 | double r2 = dx.squaredNorm(); 146 | if (r2 < EPS2 || r2 > H * H) 147 | continue; 148 | double r = sqrt(r2); 149 | double a = 1. - r / H; 150 | dens += pj.m * a * a * a * KERN; 151 | dens_proj += pj.m * a * a * a * a * KERN_NORM; 152 | if (nh[i].numNeighbors < Neighborhood::MAX_NEIGHBORS) 153 | { 154 | nh[i].particles[nh[i].numNeighbors] = &pj; 155 | nh[i].r[nh[i].numNeighbors] = r; 156 | ++nh[i].numNeighbors; 157 | } 158 | } 159 | 160 | pi.d = dens; 161 | pi.dv = dens_proj; 162 | pi.p = STIFFNESS * (dens - pi.m * REST_DENSITY); 163 | pi.pv = STIFF_APPROX * dens_proj; 164 | } 165 | } 166 | 167 | void Project(void) 168 | { 169 | for (unsigned int i = 0; i < particles.size(); i++) 170 | { 171 | auto &pi = particles[i]; 172 | 173 | Vector2d xx = pi.x; 174 | for (unsigned int j = 0; j < nh[i].numNeighbors; j++) 175 | { 176 | const Particle &pj = *nh[i].particles[j]; 177 | double r = nh[i].r[j]; 178 | Vector2d dx = pj.x - pi.x; 179 | 180 | double a = 1. - r / H; 181 | double d = DT2 * ((pi.pv + pj.pv) * a * a * a * KERN_NORM + (pi.p + pj.p) * a * a * KERN) / 2.; 182 | 183 | // relaxation 184 | xx -= d * dx / (r * pi.m); 185 | 186 | // surface tension applies if the particles are of the same material 187 | // this would allow for extensibility of multi-phase 188 | if (pi.m == pj.m) 189 | xx += (SURFACE_TENSION / pi.m) * pj.m * a * a * KERN * dx; 190 | 191 | // linear and quadratic visc 192 | Vector2d dv = pi.v - pj.v; 193 | double u = dv.dot(dx); 194 | if (u > 0) 195 | { 196 | u /= r; 197 | double a = 1 - r / H; 198 | double I = 0.5f * DT * a * (LINEAR_VISC * u + QUAD_VISC * u * u); 199 | xx -= I * dx * DT; 200 | } 201 | } 202 | xprojected[i] = xx; 203 | } 204 | } 205 | 206 | void Correct(void) 207 | { 208 | for (unsigned int i = 0; i < particles.size(); i++) 209 | { 210 | auto &p = particles[i]; 211 | p.x = xprojected[i]; 212 | p.v = (p.x - xlast[i]) / DT; 213 | } 214 | } 215 | 216 | void EnforceBoundary(void) 217 | { 218 | for (auto &p : particles) 219 | for (auto b : boundaries) 220 | { 221 | double d = p.x(0) * b(0) + p.x(1) * b(1) - b(2); 222 | if ((d = std::max(0., d)) < PARTICLE_RADIUS) 223 | p.v += (PARTICLE_RADIUS - d) * b.segment<2>(0) / DT; 224 | } 225 | } 226 | 227 | void InitGL(void) 228 | { 229 | glClearColor(0.9f, 0.9f, 0.9f, 1); 230 | glEnable(GL_POINT_SMOOTH); 231 | glPointSize(2.5f * PARTICLE_RADIUS * WINDOW_WIDTH / VIEW_WIDTH); 232 | glMatrixMode(GL_PROJECTION); 233 | } 234 | 235 | void Render(void) 236 | { 237 | glClear(GL_COLOR_BUFFER_BIT); 238 | 239 | glLoadIdentity(); 240 | glOrtho(0, VIEW_WIDTH, 0, VIEW_HEIGHT, 0, 1); 241 | 242 | glColor4f(0.2f, 0.6f, 1.0f, 1); 243 | glBegin(GL_POINTS); 244 | for (auto &p : particles) 245 | glVertex2f(p.x(0), p.x(1)); 246 | glEnd(); 247 | 248 | glutSwapBuffers(); 249 | } 250 | 251 | void PrintPositions(void) 252 | { 253 | for (const auto &p : particles) 254 | cout << p.x << endl; 255 | } 256 | 257 | void Update(void) 258 | { 259 | for (unsigned int i = 0; i < SOLVER_STEPS; i++) 260 | { 261 | ApplyExternalForces(); 262 | Integrate(); 263 | GridInsert(); 264 | PressureStep(); 265 | Project(); 266 | Correct(); 267 | GridInsert(); 268 | EnforceBoundary(); 269 | } 270 | 271 | glutPostRedisplay(); 272 | } 273 | 274 | int main(int argc, char **argv) 275 | { 276 | glutInitWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT); 277 | glutInit(&argc, argv); 278 | glutCreateWindow("PCISPH"); 279 | glutDisplayFunc(Render); 280 | glutIdleFunc(Update); 281 | 282 | InitGL(); 283 | InitSPH(); 284 | 285 | glutMainLoop(); 286 | return 0; 287 | } --------------------------------------------------------------------------------