├── .gitignore
├── DFSPH.py
├── IISPH.py
├── LICENSE.txt
├── README.md
├── WCSPH.py
├── config_builder.py
├── data
├── BoxOpenedHole.obj
├── gif
│ ├── armadillo_bath.gif
│ └── dragon_bath_large.gif
├── models
│ ├── Dragon_50k.obj
│ ├── armadillo_small.obj
│ ├── bunny.stl
│ └── bunny_sparse.obj
└── scenes
│ ├── armadillo_bath_dynamic.json
│ ├── armadillo_bath_dynamic_dfsph.json
│ ├── dragon_bath.json
│ ├── dragon_bath_dfsph.json
│ ├── dragon_bath_dynamic_dfsph.json
│ ├── high_fluid_dfsph.json
│ └── high_fluid_wcsph.json
├── demo_high_fluid.py
├── legacy
├── README.md
├── engine
│ ├── __init__.py
│ └── sph_solver.py
├── img
│ ├── DFSPH.gif
│ ├── PCISPH.gif
│ ├── WCSPH.gif
│ ├── sph_hv.gif
│ └── wcsph_alpha030.gif
├── scene.py
└── test_sample.py
├── particle_system.py
├── requirements.txt
├── run_simulation.py
├── scan_single_buffer.py
└── sph_base.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 |
--------------------------------------------------------------------------------
/DFSPH.py:
--------------------------------------------------------------------------------
1 | import taichi as ti
2 | from sph_base import SPHBase
3 |
4 |
5 | class DFSPHSolver(SPHBase):
6 | def __init__(self, particle_system):
7 | super().__init__(particle_system)
8 |
9 | self.surface_tension = 0.01
10 | self.dt[None] = self.ps.cfg.get_cfg("timeStepSize")
11 |
12 | self.enable_divergence_solver = True
13 |
14 | self.m_max_iterations_v = 100
15 | self.m_max_iterations = 100
16 |
17 | self.m_eps = 1e-5
18 |
19 | self.max_error_V = 0.1
20 | self.max_error = 0.05
21 |
22 |
23 | @ti.func
24 | def compute_densities_task(self, p_i, p_j, ret: ti.template()):
25 | x_i = self.ps.x[p_i]
26 | if self.ps.material[p_j] == self.ps.material_fluid:
27 | # Fluid neighbors
28 | x_j = self.ps.x[p_j]
29 | ret += self.ps.m_V[p_j] * self.cubic_kernel((x_i - x_j).norm())
30 | elif self.ps.material[p_j] == self.ps.material_solid:
31 | # Boundary neighbors
32 | ## Akinci2012
33 | x_j = self.ps.x[p_j]
34 | ret += self.ps.m_V[p_j] * self.cubic_kernel((x_i - x_j).norm())
35 |
36 |
37 | @ti.kernel
38 | def compute_densities(self):
39 | # for p_i in range(self.ps.particle_num[None]):
40 | for p_i in ti.grouped(self.ps.x):
41 | if self.ps.material[p_i] != self.ps.material_fluid:
42 | continue
43 | self.ps.density[p_i] = self.ps.m_V[p_i] * self.cubic_kernel(0.0)
44 | den = 0.0
45 | self.ps.for_all_neighbors(p_i, self.compute_densities_task, den)
46 | self.ps.density[p_i] += den
47 | self.ps.density[p_i] *= self.density_0
48 |
49 |
50 | @ti.func
51 | def compute_non_pressure_forces_task(self, p_i, p_j, ret: ti.template()):
52 | x_i = self.ps.x[p_i]
53 |
54 | ############## Surface Tension ###############
55 | if self.ps.material[p_j] == self.ps.material_fluid:
56 | # Fluid neighbors
57 | diameter2 = self.ps.particle_diameter * self.ps.particle_diameter
58 | x_j = self.ps.x[p_j]
59 | r = x_i - x_j
60 | r2 = r.dot(r)
61 | if r2 > diameter2:
62 | ret -= self.surface_tension / self.ps.m[p_i] * self.ps.m[p_j] * r * self.cubic_kernel(r.norm())
63 | else:
64 | ret -= self.surface_tension / self.ps.m[p_i] * self.ps.m[p_j] * r * self.cubic_kernel(ti.Vector([self.ps.particle_diameter, 0.0, 0.0]).norm())
65 |
66 |
67 | ############### Viscosoty Force ###############
68 | d = 2 * (self.ps.dim + 2)
69 | x_j = self.ps.x[p_j]
70 | # Compute the viscosity force contribution
71 | r = x_i - x_j
72 | v_xy = (self.ps.v[p_i] -
73 | self.ps.v[p_j]).dot(r)
74 |
75 | if self.ps.material[p_j] == self.ps.material_fluid:
76 | f_v = d * self.viscosity * (self.ps.m[p_j] / (self.ps.density[p_j])) * v_xy / (
77 | r.norm()**2 + 0.01 * self.ps.support_radius**2) * self.cubic_kernel_derivative(r)
78 | ret += f_v
79 | elif self.ps.material[p_j] == self.ps.material_solid:
80 | boundary_viscosity = 0.0
81 | # Boundary neighbors
82 | ## Akinci2012
83 | f_v = d * boundary_viscosity * (self.density_0 * self.ps.m_V[p_j] / (self.ps.density[p_i])) * v_xy / (
84 | r.norm()**2 + 0.01 * self.ps.support_radius**2) * self.cubic_kernel_derivative(r)
85 | ret += f_v
86 | if self.ps.is_dynamic_rigid_body(p_j):
87 | self.ps.acceleration[p_j] += -f_v * self.ps.density[p_i] / self.ps.density[p_j]
88 |
89 |
90 | @ti.kernel
91 | def compute_non_pressure_forces(self):
92 | for p_i in ti.grouped(self.ps.x):
93 | if self.ps.is_static_rigid_body(p_i):
94 | self.ps.acceleration[p_i].fill(0.0)
95 | continue
96 | ############## Body force ###############
97 | # Add body force
98 | d_v = ti.Vector(self.g)
99 | self.ps.acceleration[p_i] = d_v
100 | if self.ps.material[p_i] == self.ps.material_fluid:
101 | self.ps.for_all_neighbors(p_i, self.compute_non_pressure_forces_task, d_v)
102 | self.ps.acceleration[p_i] = d_v
103 |
104 |
105 | @ti.kernel
106 | def advect(self):
107 | # Update position
108 | for p_i in ti.grouped(self.ps.x):
109 | if self.ps.is_dynamic[p_i]:
110 | if self.ps.is_dynamic_rigid_body(p_i):
111 | self.ps.v[p_i] += self.dt[None] * self.ps.acceleration[p_i]
112 | self.ps.x[p_i] += self.dt[None] * self.ps.v[p_i]
113 |
114 |
115 | @ti.kernel
116 | def compute_DFSPH_factor(self):
117 | for p_i in ti.grouped(self.ps.x):
118 | if self.ps.material[p_i] != self.ps.material_fluid:
119 | continue
120 | sum_grad_p_k = 0.0
121 | grad_p_i = ti.Vector([0.0 for _ in range(self.ps.dim)])
122 |
123 | # `ret` concatenates `grad_p_i` and `sum_grad_p_k`
124 | ret = ti.Vector([0.0 for _ in range(self.ps.dim + 1)])
125 |
126 | self.ps.for_all_neighbors(p_i, self.compute_DFSPH_factor_task, ret)
127 |
128 | sum_grad_p_k = ret[3]
129 | for i in ti.static(range(3)):
130 | grad_p_i[i] = ret[i]
131 | sum_grad_p_k += grad_p_i.norm_sqr()
132 |
133 | # Compute pressure stiffness denominator
134 | factor = 0.0
135 | if sum_grad_p_k > 1e-6:
136 | factor = -1.0 / sum_grad_p_k
137 | else:
138 | factor = 0.0
139 | self.ps.dfsph_factor[p_i] = factor
140 |
141 |
142 | @ti.func
143 | def compute_DFSPH_factor_task(self, p_i, p_j, ret: ti.template()):
144 | if self.ps.material[p_j] == self.ps.material_fluid:
145 | # Fluid neighbors
146 | grad_p_j = -self.ps.m_V[p_j] * self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j])
147 | ret[3] += grad_p_j.norm_sqr() # sum_grad_p_k
148 | for i in ti.static(range(3)): # grad_p_i
149 | ret[i] -= grad_p_j[i]
150 | elif self.ps.material[p_j] == self.ps.material_solid:
151 | # Boundary neighbors
152 | ## Akinci2012
153 | grad_p_j = -self.ps.m_V[p_j] * self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j])
154 | for i in ti.static(range(3)): # grad_p_i
155 | ret[i] -= grad_p_j[i]
156 |
157 |
158 | @ti.kernel
159 | def compute_density_change(self):
160 | for p_i in ti.grouped(self.ps.x):
161 | if self.ps.material[p_i] != self.ps.material_fluid:
162 | continue
163 | ret = ti.Struct(density_adv=0.0, num_neighbors=0)
164 | self.ps.for_all_neighbors(p_i, self.compute_density_change_task, ret)
165 |
166 | # only correct positive divergence
167 | density_adv = ti.max(ret.density_adv, 0.0)
168 | num_neighbors = ret.num_neighbors
169 |
170 | # Do not perform divergence solve when paritlce deficiency happens
171 | if self.ps.dim == 3:
172 | if num_neighbors < 20:
173 | density_adv = 0.0
174 | else:
175 | if num_neighbors < 7:
176 | density_adv = 0.0
177 |
178 | self.ps.density_adv[p_i] = density_adv
179 |
180 |
181 | @ti.func
182 | def compute_density_change_task(self, p_i, p_j, ret: ti.template()):
183 | v_i = self.ps.v[p_i]
184 | v_j = self.ps.v[p_j]
185 | if self.ps.material[p_j] == self.ps.material_fluid:
186 | # Fluid neighbors
187 | ret.density_adv += self.ps.m_V[p_j] * (v_i - v_j).dot(self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j]))
188 | elif self.ps.material[p_j] == self.ps.material_solid:
189 | # Boundary neighbors
190 | ## Akinci2012
191 | ret.density_adv += self.ps.m_V[p_j] * (v_i - v_j).dot(self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j]))
192 |
193 | # Compute the number of neighbors
194 | ret.num_neighbors += 1
195 |
196 |
197 | @ti.kernel
198 | def compute_density_adv(self):
199 | for p_i in ti.grouped(self.ps.x):
200 | if self.ps.material[p_i] != self.ps.material_fluid:
201 | continue
202 | delta = 0.0
203 | self.ps.for_all_neighbors(p_i, self.compute_density_adv_task, delta)
204 | density_adv = self.ps.density[p_i] /self.density_0 + self.dt[None] * delta
205 | self.ps.density_adv[p_i] = ti.max(density_adv, 1.0)
206 |
207 |
208 | @ti.func
209 | def compute_density_adv_task(self, p_i, p_j, ret: ti.template()):
210 | v_i = self.ps.v[p_i]
211 | v_j = self.ps.v[p_j]
212 | if self.ps.material[p_j] == self.ps.material_fluid:
213 | # Fluid neighbors
214 | ret += self.ps.m_V[p_j] * (v_i - v_j).dot(self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j]))
215 | elif self.ps.material[p_j] == self.ps.material_solid:
216 | # Boundary neighbors
217 | ## Akinci2012
218 | ret += self.ps.m_V[p_j] * (v_i - v_j).dot(self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j]))
219 |
220 |
221 | @ti.kernel
222 | def compute_density_error(self, offset: float) -> float:
223 | density_error = 0.0
224 | for I in ti.grouped(self.ps.x):
225 | if self.ps.material[I] == self.ps.material_fluid:
226 | density_error += self.density_0 * self.ps.density_adv[I] - offset
227 | return density_error
228 |
229 | @ti.kernel
230 | def multiply_time_step(self, field: ti.template(), time_step: float):
231 | for I in ti.grouped(self.ps.x):
232 | if self.ps.material[I] == self.ps.material_fluid:
233 | field[I] *= time_step
234 |
235 |
236 | def divergence_solve(self):
237 | # TODO: warm start
238 | # Compute velocity of density change
239 | self.compute_density_change()
240 | inv_dt = 1 / self.dt[None]
241 | self.multiply_time_step(self.ps.dfsph_factor, inv_dt)
242 |
243 | m_iterations_v = 0
244 |
245 | # Start solver
246 | avg_density_err = 0.0
247 |
248 | while m_iterations_v < 1 or m_iterations_v < self.m_max_iterations_v:
249 |
250 | avg_density_err = self.divergence_solver_iteration()
251 | # Max allowed density fluctuation
252 | # use max density error divided by time step size
253 | eta = 1.0 / self.dt[None] * self.max_error_V * 0.01 * self.density_0
254 | # print("eta ", eta)
255 | if avg_density_err <= eta:
256 | break
257 | m_iterations_v += 1
258 | print(f"DFSPH - iteration V: {m_iterations_v} Avg density err: {avg_density_err}")
259 |
260 | # Multiply by h, the time step size has to be removed
261 | # to make the stiffness value independent
262 | # of the time step size
263 |
264 | # TODO: if warm start
265 | # also remove for kappa v
266 |
267 | self.multiply_time_step(self.ps.dfsph_factor, self.dt[None])
268 |
269 |
270 | def divergence_solver_iteration(self):
271 | self.divergence_solver_iteration_kernel()
272 | self.compute_density_change()
273 | density_err = self.compute_density_error(0.0)
274 | return density_err / self.ps.fluid_particle_num
275 |
276 |
277 | @ti.kernel
278 | def divergence_solver_iteration_kernel(self):
279 | # Perform Jacobi iteration
280 | for p_i in ti.grouped(self.ps.x):
281 | if self.ps.material[p_i] != self.ps.material_fluid:
282 | continue
283 | # evaluate rhs
284 | b_i = self.ps.density_adv[p_i]
285 | k_i = b_i*self.ps.dfsph_factor[p_i]
286 | ret = ti.Struct(dv=ti.Vector([0.0 for _ in range(self.ps.dim)]), k_i=k_i)
287 | # TODO: if warm start
288 | # get_kappa_V += k_i
289 | self.ps.for_all_neighbors(p_i, self.divergence_solver_iteration_task, ret)
290 | self.ps.v[p_i] += ret.dv
291 |
292 |
293 | @ti.func
294 | def divergence_solver_iteration_task(self, p_i, p_j, ret: ti.template()):
295 | if self.ps.material[p_j] == self.ps.material_fluid:
296 | # Fluid neighbors
297 | b_j = self.ps.density_adv[p_j]
298 | k_j = b_j * self.ps.dfsph_factor[p_j]
299 | k_sum = ret.k_i + self.density_0 / self.density_0 * k_j # TODO: make the neighbor density0 different for multiphase fluid
300 | if ti.abs(k_sum) > self.m_eps:
301 | grad_p_j = -self.ps.m_V[p_j] * self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j])
302 | ret.dv -= self.dt[None] * k_sum * grad_p_j
303 | elif self.ps.material[p_j] == self.ps.material_solid:
304 | # Boundary neighbors
305 | ## Akinci2012
306 | if ti.abs(ret.k_i) > self.m_eps:
307 | grad_p_j = -self.ps.m_V[p_j] * self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j])
308 | vel_change = -self.dt[None] * 1.0 * ret.k_i * grad_p_j
309 | ret.dv += vel_change
310 | if self.ps.is_dynamic_rigid_body(p_j):
311 | self.ps.acceleration[p_j] += -vel_change * (1 / self.dt[None]) * self.ps.density[p_i] / self.ps.density[p_j]
312 |
313 |
314 | def pressure_solve(self):
315 | inv_dt = 1 / self.dt[None]
316 | inv_dt2 = 1 / (self.dt[None] * self.dt[None])
317 |
318 | # TODO: warm start
319 |
320 | # Compute rho_adv
321 | self.compute_density_adv()
322 |
323 | self.multiply_time_step(self.ps.dfsph_factor, inv_dt2)
324 |
325 | m_iterations = 0
326 |
327 | # Start solver
328 | avg_density_err = 0.0
329 |
330 | while m_iterations < 1 or m_iterations < self.m_max_iterations:
331 |
332 | avg_density_err = self.pressure_solve_iteration()
333 | # Max allowed density fluctuation
334 | eta = self.max_error * 0.01 * self.density_0
335 | if avg_density_err <= eta:
336 | break
337 | m_iterations += 1
338 | print(f"DFSPH - iterations: {m_iterations} Avg density Err: {avg_density_err:.4f}")
339 | # Multiply by h, the time step size has to be removed
340 | # to make the stiffness value independent
341 | # of the time step size
342 |
343 | # TODO: if warm start
344 | # also remove for kappa v
345 |
346 | def pressure_solve_iteration(self):
347 | self.pressure_solve_iteration_kernel()
348 | self.compute_density_adv()
349 | density_err = self.compute_density_error(self.density_0)
350 | return density_err / self.ps.fluid_particle_num
351 |
352 |
353 | @ti.kernel
354 | def pressure_solve_iteration_kernel(self):
355 | # Compute pressure forces
356 | for p_i in ti.grouped(self.ps.x):
357 | if self.ps.material[p_i] != self.ps.material_fluid:
358 | continue
359 | # Evaluate rhs
360 | b_i = self.ps.density_adv[p_i] - 1.0
361 | k_i = b_i * self.ps.dfsph_factor[p_i]
362 |
363 | # TODO: if warmstart
364 | # get kappa V
365 | self.ps.for_all_neighbors(p_i, self.pressure_solve_iteration_task, k_i)
366 |
367 |
368 | @ti.func
369 | def pressure_solve_iteration_task(self, p_i, p_j, k_i: ti.template()):
370 | if self.ps.material[p_j] == self.ps.material_fluid:
371 | # Fluid neighbors
372 | b_j = self.ps.density_adv[p_j] - 1.0
373 | k_j = b_j * self.ps.dfsph_factor[p_j]
374 | k_sum = k_i + self.density_0 / self.density_0 * k_j # TODO: make the neighbor density0 different for multiphase fluid
375 | if ti.abs(k_sum) > self.m_eps:
376 | grad_p_j = -self.ps.m_V[p_j] * self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j])
377 | # Directly update velocities instead of storing pressure accelerations
378 | self.ps.v[p_i] -= self.dt[None] * k_sum * grad_p_j # ki, kj already contain inverse density
379 | elif self.ps.material[p_j] == self.ps.material_solid:
380 | # Boundary neighbors
381 | ## Akinci2012
382 | if ti.abs(k_i) > self.m_eps:
383 | grad_p_j = -self.ps.m_V[p_j] * self.cubic_kernel_derivative(self.ps.x[p_i] - self.ps.x[p_j])
384 |
385 | # Directly update velocities instead of storing pressure accelerations
386 | vel_change = - self.dt[None] * 1.0 * k_i * grad_p_j # kj already contains inverse density
387 | self.ps.v[p_i] += vel_change
388 | if self.ps.is_dynamic_rigid_body(p_j):
389 | self.ps.acceleration[p_j] += -vel_change * 1.0 / self.dt[None] * self.ps.density[p_i] / self.ps.density[p_j]
390 |
391 |
392 | @ti.kernel
393 | def predict_velocity(self):
394 | # compute new velocities only considering non-pressure forces
395 | for p_i in ti.grouped(self.ps.x):
396 | if self.ps.is_dynamic[p_i] and self.ps.material[p_i] == self.ps.material_fluid:
397 | self.ps.v[p_i] += self.dt[None] * self.ps.acceleration[p_i]
398 |
399 |
400 | def substep(self):
401 | self.compute_densities()
402 | self.compute_DFSPH_factor()
403 | if self.enable_divergence_solver:
404 | self.divergence_solve()
405 | self.compute_non_pressure_forces()
406 | self.predict_velocity()
407 | self.pressure_solve()
408 | self.advect()
409 |
--------------------------------------------------------------------------------
/IISPH.py:
--------------------------------------------------------------------------------
1 | import taichi as ti
2 | from sph_base import SPHBase
3 |
4 |
5 | class IISPHSolver(SPHBase):
6 | def __init__(self, particle_system):
7 | super().__init__(particle_system)
8 |
9 | self.a_ii = ti.field(dtype=float, shape=self.ps.particle_max_num)
10 | self.density_deviation = ti.field(dtype=float, shape=self.ps.particle_max_num)
11 | self.last_pressure = ti.field(dtype=float, shape=self.ps.particle_max_num)
12 | self.avg_density_error = ti.field(dtype=float, shape=())
13 |
14 | self.ps.acceleration = ti.Vector.field(self.ps.dim, dtype=float)
15 | self.pressure_accel = ti.Vector.field(self.ps.dim, dtype=float)
16 | particle_node = ti.root.dense(ti.i, self.ps.particle_max_num)
17 | particle_node.place(self.ps.acceleration, self.pressure_accel)
18 | self.dt[None] = 2e-4
19 |
20 | @ti.kernel
21 | def predict_advection(self):
22 | # Compute a_ii
23 | for p_i in range(self.ps.particle_num[None]):
24 | x_i = self.ps.x[p_i]
25 | sum_neighbor = 0.0
26 | sum_neighbor_of_neighbor = 0.0
27 | m_Vi = self.ps.m_V[p_i]
28 | density_i = self.ps.density[p_i]
29 | density_i2 = density_i * density_i
30 | density_02 = self.density_0 * self.density_0
31 | self.a_ii[p_i] = 0.0
32 | # Fluid neighbors
33 | for j in range(self.ps.fluid_neighbors_num[p_i]):
34 | p_j = self.ps.fluid_neighbors[p_i, j]
35 | x_j = self.ps.x[p_j]
36 | sum_neighbor_inner = ti.Vector([0.0 for _ in range(self.ps.dim)])
37 | for k in range(self.ps.fluid_neighbors_num[p_i]):
38 | density_k = self.ps.density[k]
39 | density_k2 = density_k * density_k
40 | p_k = self.ps.fluid_neighbors[p_i, j]
41 | x_k = self.ps.x[p_k]
42 | sum_neighbor_inner += self.ps.m_V[p_k] * self.cubic_kernel_derivative(x_i - x_k) / density_k2
43 |
44 | kernel_grad_ij = self.cubic_kernel_derivative(x_i - x_j)
45 | sum_neighbor -= (self.ps.m_V[p_j] * sum_neighbor_inner).dot(kernel_grad_ij)
46 |
47 | sum_neighbor_of_neighbor -= (self.ps.m_V[p_j] * kernel_grad_ij).dot(kernel_grad_ij)
48 | sum_neighbor_of_neighbor *= m_Vi / density_i2
49 | self.a_ii[p_i] += (sum_neighbor + sum_neighbor_of_neighbor) * self.dt[None] * self.dt[None] * density_02
50 |
51 | # Boundary neighbors
52 | ## Akinci2012
53 | for j in range(self.ps.solid_neighbors_num[p_i]):
54 | p_j = self.ps.solid_neighbors[p_i, j]
55 | x_j = self.ps.x[p_j]
56 | sum_neighbor_inner = ti.Vector([0.0 for _ in range(self.ps.dim)])
57 | for k in range(self.ps.solid_neighbors_num[p_i]):
58 | density_k = self.ps.density[k]
59 | density_k2 = density_k * density_k
60 | p_k = self.ps.solid_neighbors[p_i, j]
61 | x_k = self.ps.x[p_k]
62 | sum_neighbor_inner += self.ps.m_V[p_k] * self.cubic_kernel_derivative(x_i - x_k) / density_k2
63 |
64 | kernel_grad_ij = self.cubic_kernel_derivative(x_i - x_j)
65 | sum_neighbor -= (self.ps.m_V[p_j] * sum_neighbor_inner).dot(kernel_grad_ij)
66 |
67 | sum_neighbor_of_neighbor -= (self.ps.m_V[p_j] * kernel_grad_ij).dot(kernel_grad_ij)
68 | sum_neighbor_of_neighbor *= m_Vi / density_i2
69 | self.a_ii[p_i] += (sum_neighbor + sum_neighbor_of_neighbor) * self.dt[None] * self.dt[None] * density_02
70 |
71 | # Compute source term (i.e., density deviation)
72 | # Compute the predicted v^star
73 | for p_i in range(self.ps.particle_num[None]):
74 | if self.ps.material[p_i] == self.ps.material_fluid:
75 | self.ps.v[p_i] += self.dt[None] * self.ps.acceleration[p_i]
76 |
77 | for p_i in range(self.ps.particle_num[None]):
78 | x_i = self.ps.x[p_i]
79 | density_i = self.ps.density[p_i]
80 | divergence = 0.0
81 | # Fluid neighbors
82 | for j in range(self.ps.fluid_neighbors_num[p_i]):
83 | p_j = self.ps.fluid_neighbors[p_i, j]
84 | x_j = self.ps.x[p_j]
85 | divergence += self.ps.m_V[p_j] * (self.ps.v[p_i] - self.ps.v[p_j]).dot(self.cubic_kernel_derivative(x_i - x_j))
86 |
87 | # Boundary neighbors
88 | ## Akinci2012
89 | for j in range(self.ps.solid_neighbors_num[p_i]):
90 | p_j = self.ps.solid_neighbors[p_i, j]
91 | x_j = self.ps.x[p_j]
92 | divergence += self.ps.m_V[p_j] * (self.ps.v[p_i] - self.ps.v[p_j]).dot(self.cubic_kernel_derivative(x_i - x_j))
93 |
94 | self.density_deviation[p_i] = self.density_0 - density_i - self.dt[None] * divergence * self.density_0
95 |
96 | # Clear all pressures
97 | for p_i in range(self.ps.particle_num[None]):
98 | # self.last_pressure[p_i] = 0.0
99 | # self.ps.pressure[p_i] = 0.0
100 | self.last_pressure[p_i] = 0.5 * self.ps.pressure[p_i]
101 |
102 | def pressure_solve(self):
103 | iteration = 0
104 | while iteration < 1000:
105 | self.avg_density_error[None] = 0.0
106 | self.pressure_solve_iteration()
107 | iteration += 1
108 | if iteration % 100 == 0:
109 | print(f'iter {iteration}, density err {self.avg_density_error[None]}')
110 | if self.avg_density_error[None] < 1e-3:
111 | # print(f'Stop criterion satisfied at iter {iteration}, density err {self.avg_density_error[None]}')
112 | break
113 |
114 | @ti.kernel
115 | def pressure_solve_iteration(self):
116 | omega = 0.5
117 | # Compute pressure acceleration
118 | for p_i in range(self.ps.particle_num[None]):
119 | # if self.ps.material[p_i] != self.ps.material_fluid:
120 | # self.pressure_accel[p_i].fill(0)
121 | # continue
122 | x_i = self.ps.x[p_i]
123 | d_v = ti.Vector([0.0 for _ in range(self.ps.dim)])
124 |
125 | dpi = self.last_pressure[p_i] / self.ps.density[p_i] ** 2
126 | # Fluid neighbors
127 | for j in range(self.ps.fluid_neighbors_num[p_i]):
128 | p_j = self.ps.fluid_neighbors[p_i, j]
129 | x_j = self.ps.x[p_j]
130 | dpj = self.last_pressure[p_j] / self.ps.density[p_j] ** 2
131 | # Compute the pressure force contribution, Symmetric Formula
132 | d_v += -self.density_0 * self.ps.m_V[p_j] * (dpi + dpj) \
133 | * self.cubic_kernel_derivative(x_i - x_j)
134 |
135 | # Boundary neighbors
136 | dpj = self.last_pressure[p_i] / self.density_0 ** 2
137 | ## Akinci2012
138 | for j in range(self.ps.solid_neighbors_num[p_i]):
139 | p_j = self.ps.solid_neighbors[p_i, j]
140 | x_j = self.ps.x[p_j]
141 | # Compute the pressure force contribution, Symmetric Formula
142 | d_v += -self.density_0 * self.ps.m_V[p_j] * (dpi + dpj) \
143 | * self.cubic_kernel_derivative(x_i - x_j)
144 | self.pressure_accel[p_i] += d_v
145 |
146 | # Compute Ap and compute new pressure
147 | for p_i in range(self.ps.particle_num[None]):
148 | x_i = self.ps.x[p_i]
149 | Ap = 0.0
150 | dt2 = self.dt[None] * self.dt[None]
151 | accel_p_i = self.pressure_accel[p_i]
152 | # Fluid neighbors
153 | for j in range(self.ps.fluid_neighbors_num[p_i]):
154 | p_j = self.ps.fluid_neighbors[p_i, j]
155 | x_j = self.ps.x[p_j]
156 | Ap += self.ps.m_V[p_j] * (accel_p_i - self.pressure_accel[p_j]).dot(self.cubic_kernel_derivative(x_i - x_j))
157 | # Boundary neighbors
158 | ## Akinci2012
159 | for j in range(self.ps.solid_neighbors_num[p_i]):
160 | p_j = self.ps.solid_neighbors[p_i, j]
161 | x_j = self.ps.x[p_j]
162 | Ap += self.ps.m_V[p_j] * (accel_p_i - self.pressure_accel[p_j]).dot(self.cubic_kernel_derivative(x_i - x_j))
163 | Ap *= dt2 * self.density_0
164 | # print(self.a_ii[1])
165 | if abs(self.a_ii[p_i]) > 1e-6:
166 | # Relaxed Jacobi
167 | self.ps.pressure[p_i] = ti.max(self.last_pressure[p_i] + omega * (self.density_deviation[p_i] - Ap) / self.a_ii[p_i], 0.0)
168 | else:
169 | self.ps.pressure[p_i] = 0.0
170 |
171 | if self.ps.pressure[p_i] != 0.0:
172 | # new_density = self.density_0
173 | # if p_i == 100:
174 | # print(" Ap ", Ap, " density deviation ", self.density_deviation[p_i], 'a_ii ', self.a_ii[p_i])
175 | self.avg_density_error[None] += abs(Ap - self.density_deviation[p_i]) / self.density_0
176 | self.avg_density_error[None] /= self.ps.particle_num[None]
177 | for p_i in range(self.ps.particle_num[None]):
178 | # Update the pressure
179 | self.last_pressure[p_i] = self.ps.pressure[p_i]
180 |
181 |
182 | @ti.kernel
183 | def compute_densities(self):
184 | for p_i in range(self.ps.particle_num[None]):
185 | if self.ps.material[p_i] != self.ps.material_fluid:
186 | continue
187 | x_i = self.ps.x[p_i]
188 | self.ps.density[p_i] = self.ps.m_V[p_i] * self.cubic_kernel(0.0)
189 | # Fluid neighbors
190 | for j in range(self.ps.fluid_neighbors_num[p_i]):
191 | p_j = self.ps.fluid_neighbors[p_i, j]
192 | x_j = self.ps.x[p_j]
193 | self.ps.density[p_i] += self.ps.m_V[p_j] * self.cubic_kernel((x_i - x_j).norm())
194 | # Boundary neighbors
195 | ## Akinci2012
196 | for j in range(self.ps.solid_neighbors_num[p_i]):
197 | p_j = self.ps.solid_neighbors[p_i, j]
198 | x_j = self.ps.x[p_j]
199 | self.ps.density[p_i] += self.ps.m_V[p_j] * self.cubic_kernel((x_i - x_j).norm())
200 | self.ps.density[p_i] *= self.density_0
201 |
202 | @ti.kernel
203 | def compute_pressure_forces(self):
204 | for p_i in range(self.ps.particle_num[None]):
205 | if self.ps.material[p_i] != self.ps.material_fluid:
206 | self.pressure_accel[p_i].fill(0)
207 | continue
208 | self.pressure_accel[p_i].fill(0)
209 | x_i = self.ps.x[p_i]
210 | d_v = ti.Vector([0.0 for _ in range(self.ps.dim)])
211 |
212 | dpi = self.ps.pressure[p_i] / self.ps.density[p_i] ** 2
213 | # Fluid neighbors
214 | for j in range(self.ps.fluid_neighbors_num[p_i]):
215 | p_j = self.ps.fluid_neighbors[p_i, j]
216 | x_j = self.ps.x[p_j]
217 | dpj = self.ps.pressure[p_j] / self.ps.density[p_j] ** 2
218 | # Compute the pressure force contribution, Symmetric Formula
219 | d_v += -self.density_0 * self.ps.m_V[p_j] * (dpi + dpj) \
220 | * self.cubic_kernel_derivative(x_i - x_j)
221 |
222 | # Boundary neighbors
223 | dpj = self.ps.pressure[p_i] / self.density_0 ** 2
224 | # dpj = 0.0
225 | ## Akinci2012
226 | for j in range(self.ps.solid_neighbors_num[p_i]):
227 | p_j = self.ps.solid_neighbors[p_i, j]
228 | x_j = self.ps.x[p_j]
229 | # Compute the pressure force contribution, Symmetric Formula
230 | d_v += -self.density_0 * self.ps.m_V[p_j] * (dpi + dpj) \
231 | * self.cubic_kernel_derivative(x_i - x_j)
232 |
233 | self.pressure_accel[p_i] = d_v
234 |
235 | @ti.kernel
236 | def compute_non_pressure_forces(self):
237 | for p_i in range(self.ps.particle_num[None]):
238 | # if self.ps.material[p_i] != self.ps.material_fluid:
239 | # self.ps.acceleration[p_i].fill(0)
240 | # continue
241 | x_i = self.ps.x[p_i]
242 | # Add body force
243 | d_v = ti.Vector([0.0 for _ in range(self.ps.dim)])
244 | d_v[1] = self.g
245 | for j in range(self.ps.fluid_neighbors_num[p_i]):
246 | p_j = self.ps.fluid_neighbors[p_i, j]
247 | x_j = self.ps.x[p_j]
248 | d_v += self.viscosity_force(p_i, p_j, x_i - x_j)
249 | self.ps.acceleration[p_i] = d_v
250 |
251 | @ti.kernel
252 | def advect(self):
253 | # Symplectic Euler
254 | for p_i in range(self.ps.particle_num[None]):
255 | if self.ps.material[p_i] == self.ps.material_fluid:
256 | self.ps.v[p_i] += self.dt[None] * self.pressure_accel[p_i]
257 | self.ps.x[p_i] += self.dt[None] * self.ps.v[p_i]
258 |
259 | def substep(self):
260 | self.compute_densities()
261 | self.compute_non_pressure_forces()
262 |
263 | self.predict_advection()
264 | self.pressure_solve()
265 |
266 | self.compute_pressure_forces()
267 | self.advect()
268 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mingrui Zhang
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SPH Taichi
2 |
3 | A high-performance implementation of Smooth Particle Hydrodynamics (SPH) simulator in [Taichi](https://github.com/taichi-dev/taichi). (working in progress)
4 |
5 | ## Examples
6 |
7 | - Dragon Bath (~420 K particles, ~280 FPS on RTX 3090 GPU, with timestep 4e-4)
8 |
9 |
10 |
11 |
12 |
13 | - Armadillo Bath (~1.74 M particles, ~80 FPS on RTX 3090 GPU, with timestep 4e-4)
14 |
15 |
16 |
17 |
18 |
19 | ## Features
20 |
21 | Currently, the following features have been implemented:
22 | - Cross-platform: Windows, Linux
23 | - Support massively parallel GPU computing
24 | - Weakly Compressible SPH (WCSPH)[1]
25 | - One-way/two-way fluid-solid coupling[2]
26 | - Shape-matching based rigid-body simulator[3]
27 | - Neighborhood search accelerated by GPU parallel prefix sum + counting sort
28 |
29 | ### Note
30 | The GPU parallel prefix sum is only supported by cuda/vulkan backend currently.
31 |
32 | ## Install
33 |
34 | ```
35 | python -m pip install -r requirements.txt
36 | ```
37 |
38 | To reproduce the demos show above:
39 |
40 | ```
41 | python run_simulation.py --scene_file ./data/scenes/dragon_bath.json
42 | ```
43 |
44 | ```
45 | python run_simulation.py --scene_file ./data/scenes/armadillo_bath_dynamic.json
46 | ```
47 |
48 |
49 | ## Reference
50 | 1. M. Becker and M. Teschner (2007). "Weakly compressible SPH for free surface flows". In:Proceedings of the 2007 ACM SIGGRAPH/Eurographics symposium on Computer animation. Eurographics Association, pp. 209–217.
51 | 2. N. Akinci, M. Ihmsen, G. Akinci, B. Solenthaler, and M. Teschner. 2012. Versatile
52 | rigid-fluid coupling for incompressible SPH. ACM Transactions on Graphics 31, 4 (2012), 62:1–62:8.
53 | 3. Miles Macklin, Matthias Müller, Nuttapong Chentanez, and Tae-Yong Kim. 2014. Unified particle physics for real-time applications. ACM Trans. Graph. 33, 4, Article 153 (July 2014), 12 pages.
54 |
55 |
56 | ## Acknowledgement
57 | Implementation is largely inspired by [SPlisHSPlasH](https://github.com/InteractiveComputerGraphics/SPlisHSPlasH).
58 |
--------------------------------------------------------------------------------
/WCSPH.py:
--------------------------------------------------------------------------------
1 | import taichi as ti
2 | from sph_base import SPHBase
3 |
4 |
5 | class WCSPHSolver(SPHBase):
6 | def __init__(self, particle_system):
7 | super().__init__(particle_system)
8 | # Pressure state function parameters(WCSPH)
9 | self.exponent = 7.0
10 | self.exponent = self.ps.cfg.get_cfg("exponent")
11 |
12 | self.stiffness = 50000.0
13 | self.stiffness = self.ps.cfg.get_cfg("stiffness")
14 |
15 | self.surface_tension = 0.01
16 | self.dt[None] = self.ps.cfg.get_cfg("timeStepSize")
17 |
18 |
19 | @ti.func
20 | def compute_densities_task(self, p_i, p_j, ret: ti.template()):
21 | x_i = self.ps.x[p_i]
22 | if self.ps.material[p_j] == self.ps.material_fluid:
23 | # Fluid neighbors
24 | x_j = self.ps.x[p_j]
25 | ret += self.ps.m_V[p_j] * self.cubic_kernel((x_i - x_j).norm())
26 | elif self.ps.material[p_j] == self.ps.material_solid:
27 | # Boundary neighbors
28 | ## Akinci2012
29 | x_j = self.ps.x[p_j]
30 | ret += self.ps.m_V[p_j] * self.cubic_kernel((x_i - x_j).norm())
31 |
32 |
33 | @ti.kernel
34 | def compute_densities(self):
35 | # for p_i in range(self.ps.particle_num[None]):
36 | for p_i in ti.grouped(self.ps.x):
37 | if self.ps.material[p_i] != self.ps.material_fluid:
38 | continue
39 | self.ps.density[p_i] = self.ps.m_V[p_i] * self.cubic_kernel(0.0)
40 | den = 0.0
41 | self.ps.for_all_neighbors(p_i, self.compute_densities_task, den)
42 | self.ps.density[p_i] += den
43 | self.ps.density[p_i] *= self.density_0
44 |
45 |
46 | @ti.func
47 | def compute_pressure_forces_task(self, p_i, p_j, ret: ti.template()):
48 | x_i = self.ps.x[p_i]
49 | dpi = self.ps.pressure[p_i] / self.ps.density[p_i] ** 2
50 | # Fluid neighbors
51 | if self.ps.material[p_j] == self.ps.material_fluid:
52 | x_j = self.ps.x[p_j]
53 | density_j = self.ps.density[p_j] * self.density_0 / self.density_0 # TODO: The density_0 of the neighbor may be different when the fluid density is different
54 | dpj = self.ps.pressure[p_j] / (density_j * density_j)
55 | # Compute the pressure force contribution, Symmetric Formula
56 | ret += -self.density_0 * self.ps.m_V[p_j] * (dpi + dpj) \
57 | * self.cubic_kernel_derivative(x_i-x_j)
58 | elif self.ps.material[p_j] == self.ps.material_solid:
59 | # Boundary neighbors
60 | dpj = self.ps.pressure[p_i] / self.density_0 ** 2
61 | ## Akinci2012
62 | x_j = self.ps.x[p_j]
63 | # Compute the pressure force contribution, Symmetric Formula
64 | f_p = -self.density_0 * self.ps.m_V[p_j] * (dpi + dpj) \
65 | * self.cubic_kernel_derivative(x_i-x_j)
66 | ret += f_p
67 | if self.ps.is_dynamic_rigid_body(p_j):
68 | self.ps.acceleration[p_j] += -f_p * self.density_0 / self.ps.density[p_j]
69 |
70 | @ti.kernel
71 | def compute_pressure_forces(self):
72 | for p_i in ti.grouped(self.ps.x):
73 | if self.ps.material[p_i] != self.ps.material_fluid:
74 | continue
75 | self.ps.density[p_i] = ti.max(self.ps.density[p_i], self.density_0)
76 | self.ps.pressure[p_i] = self.stiffness * (ti.pow(self.ps.density[p_i] / self.density_0, self.exponent) - 1.0)
77 | for p_i in ti.grouped(self.ps.x):
78 | if self.ps.is_static_rigid_body(p_i):
79 | self.ps.acceleration[p_i].fill(0)
80 | continue
81 | elif self.ps.is_dynamic_rigid_body(p_i):
82 | continue
83 | dv = ti.Vector([0.0 for _ in range(self.ps.dim)])
84 | self.ps.for_all_neighbors(p_i, self.compute_pressure_forces_task, dv)
85 | self.ps.acceleration[p_i] += dv
86 |
87 |
88 | @ti.func
89 | def compute_non_pressure_forces_task(self, p_i, p_j, ret: ti.template()):
90 | x_i = self.ps.x[p_i]
91 |
92 | ############## Surface Tension ###############
93 | if self.ps.material[p_j] == self.ps.material_fluid:
94 | # Fluid neighbors
95 | diameter2 = self.ps.particle_diameter * self.ps.particle_diameter
96 | x_j = self.ps.x[p_j]
97 | r = x_i - x_j
98 | r2 = r.dot(r)
99 | if r2 > diameter2:
100 | ret -= self.surface_tension / self.ps.m[p_i] * self.ps.m[p_j] * r * self.cubic_kernel(r.norm())
101 | else:
102 | ret -= self.surface_tension / self.ps.m[p_i] * self.ps.m[p_j] * r * self.cubic_kernel(ti.Vector([self.ps.particle_diameter, 0.0, 0.0]).norm())
103 |
104 |
105 | ############### Viscosoty Force ###############
106 | d = 2 * (self.ps.dim + 2)
107 | x_j = self.ps.x[p_j]
108 | # Compute the viscosity force contribution
109 | r = x_i - x_j
110 | v_xy = (self.ps.v[p_i] -
111 | self.ps.v[p_j]).dot(r)
112 |
113 | if self.ps.material[p_j] == self.ps.material_fluid:
114 | f_v = d * self.viscosity * (self.ps.m[p_j] / (self.ps.density[p_j])) * v_xy / (
115 | r.norm()**2 + 0.01 * self.ps.support_radius**2) * self.cubic_kernel_derivative(r)
116 | ret += f_v
117 | elif self.ps.material[p_j] == self.ps.material_solid:
118 | boundary_viscosity = 0.0
119 | # Boundary neighbors
120 | ## Akinci2012
121 | f_v = d * boundary_viscosity * (self.density_0 * self.ps.m_V[p_j] / (self.ps.density[p_i])) * v_xy / (
122 | r.norm()**2 + 0.01 * self.ps.support_radius**2) * self.cubic_kernel_derivative(r)
123 | ret += f_v
124 | if self.ps.is_dynamic_rigid_body(p_j):
125 | self.ps.acceleration[p_j] += -f_v * self.density_0 / self.ps.density[p_j]
126 |
127 |
128 | @ti.kernel
129 | def compute_non_pressure_forces(self):
130 | for p_i in ti.grouped(self.ps.x):
131 | if self.ps.is_static_rigid_body(p_i):
132 | self.ps.acceleration[p_i].fill(0.0)
133 | continue
134 | ############## Body force ###############
135 | # Add body force
136 | d_v = ti.Vector(self.g)
137 | self.ps.acceleration[p_i] = d_v
138 | if self.ps.material[p_i] == self.ps.material_fluid:
139 | self.ps.for_all_neighbors(p_i, self.compute_non_pressure_forces_task, d_v)
140 | self.ps.acceleration[p_i] = d_v
141 |
142 |
143 | @ti.kernel
144 | def advect(self):
145 | # Symplectic Euler
146 | for p_i in ti.grouped(self.ps.x):
147 | if self.ps.is_dynamic[p_i]:
148 | self.ps.v[p_i] += self.dt[None] * self.ps.acceleration[p_i]
149 | self.ps.x[p_i] += self.dt[None] * self.ps.v[p_i]
150 |
151 |
152 | def substep(self):
153 | self.compute_densities()
154 | self.compute_non_pressure_forces()
155 | self.compute_pressure_forces()
156 | self.advect()
157 |
--------------------------------------------------------------------------------
/config_builder.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 |
4 | class SimConfig:
5 | def __init__(self, scene_file_path) -> None:
6 | self.config = None
7 | with open(scene_file_path, "r") as f:
8 | self.config = json.load(f)
9 | print(self.config)
10 |
11 | def get_cfg(self, name, enforce_exist=False):
12 | if enforce_exist:
13 | assert name in self.config["Configuration"]
14 | if name not in self.config["Configuration"]:
15 | if enforce_exist:
16 | assert name in self.config["Configuration"]
17 | else:
18 | return None
19 | return self.config["Configuration"][name]
20 |
21 | def get_rigid_bodies(self):
22 | if "RigidBodies" in self.config:
23 | return self.config["RigidBodies"]
24 | else:
25 | return []
26 |
27 | def get_rigid_blocks(self):
28 | if "RigidBlocks" in self.config:
29 | return self.config["RigidBlocks"]
30 | else:
31 | return []
32 |
33 | def get_fluid_blocks(self):
34 | if "FluidBlocks" in self.config:
35 | return self.config["FluidBlocks"]
36 | else:
37 | return []
38 |
--------------------------------------------------------------------------------
/data/BoxOpenedHole.obj:
--------------------------------------------------------------------------------
1 | # Blender v2.81 (sub 16) OBJ File: ''
2 | # www.blender.org
3 | v -0.500000 1.000000 0.500000
4 | v 0.500000 1.000000 0.500000
5 | v -0.500000 2.000000 0.500000
6 | v 0.500000 2.000000 0.500000
7 | v -0.500000 2.000000 -0.500000
8 | v 0.500000 2.000000 -0.500000
9 | v -0.500000 1.000000 -0.500000
10 | v 0.500000 1.000000 -0.500000
11 | v 0.000000 1.000000 0.098995
12 | v 0.098995 1.000000 0.000000
13 | v 0.000000 1.000000 -0.098995
14 | v -0.098995 1.000000 0.000000
15 | v -0.550000 0.905825 0.550000
16 | v -0.550000 2.005825 0.550000
17 | v 0.550000 0.905825 0.550000
18 | v 0.550000 2.005825 0.550000
19 | v -0.550000 2.005825 -0.550000
20 | v -0.550000 0.905825 -0.550000
21 | v 0.550000 2.005825 -0.550000
22 | v 0.550000 0.905825 -0.550000
23 | v -0.000000 0.905825 0.108895
24 | v 0.108894 0.905825 0.000000
25 | v -0.000000 0.905825 -0.108894
26 | v -0.108894 0.905825 0.000000
27 | vn 0.0000 0.0000 -1.0000
28 | vn 0.0000 -0.0000 1.0000
29 | vn -1.0000 0.0000 0.0000
30 | vn 1.0000 0.0000 0.0000
31 | vn 0.0000 1.0000 0.0000
32 | vn 0.0000 -1.0000 -0.0000
33 | vn -0.1157 0.9933 0.0000
34 | vn -0.7052 -0.0741 0.7052
35 | vn 0.0000 0.9933 0.1157
36 | vn 0.1157 0.9933 0.0000
37 | vn 0.7052 -0.0741 0.7052
38 | vn -0.7052 -0.0741 -0.7052
39 | vn 0.7052 -0.0741 -0.7052
40 | vn 0.0000 0.9933 -0.1157
41 | s off
42 | f 1//1 3//1 2//1
43 | f 3//1 4//1 2//1
44 | f 5//2 7//2 6//2
45 | f 7//2 8//2 6//2
46 | f 2//3 4//3 8//3
47 | f 4//3 6//3 8//3
48 | f 7//4 5//4 1//4
49 | f 5//4 3//4 1//4
50 | f 1//5 2//5 9//5
51 | f 9//5 2//5 10//5
52 | f 2//5 8//5 10//5
53 | f 10//5 8//5 11//5
54 | f 11//5 8//5 7//5
55 | f 11//5 7//5 12//5
56 | f 7//5 1//5 12//5
57 | f 12//5 1//5 9//5
58 | f 13//2 15//2 14//2
59 | f 14//2 15//2 16//2
60 | f 17//1 19//1 18//1
61 | f 18//1 19//1 20//1
62 | f 15//4 20//4 16//4
63 | f 16//4 20//4 19//4
64 | f 18//3 13//3 17//3
65 | f 17//3 13//3 14//3
66 | f 13//6 21//6 15//6
67 | f 21//6 22//6 15//6
68 | f 15//6 22//6 20//6
69 | f 22//6 23//6 20//6
70 | f 23//6 18//6 20//6
71 | f 23//6 24//6 18//6
72 | f 18//6 24//6 13//6
73 | f 24//6 21//6 13//6
74 | f 4//7 19//7 6//7
75 | f 11//8 22//8 10//8
76 | f 6//9 17//9 5//9
77 | f 3//10 17//10 14//10
78 | f 12//11 23//11 11//11
79 | f 9//12 22//12 21//12
80 | f 9//13 24//13 12//13
81 | f 3//14 16//14 4//14
82 | f 4//7 16//7 19//7
83 | f 11//8 23//8 22//8
84 | f 6//9 19//9 17//9
85 | f 3//10 5//10 17//10
86 | f 12//11 24//11 23//11
87 | f 9//12 10//12 22//12
88 | f 9//13 21//13 24//13
89 | f 3//14 14//14 16//14
90 |
--------------------------------------------------------------------------------
/data/gif/armadillo_bath.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/data/gif/armadillo_bath.gif
--------------------------------------------------------------------------------
/data/gif/dragon_bath_large.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/data/gif/dragon_bath_large.gif
--------------------------------------------------------------------------------
/data/models/bunny.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/data/models/bunny.stl
--------------------------------------------------------------------------------
/data/scenes/armadillo_bath_dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "Configuration":
3 | {
4 | "domainStart": [0.0, 0.0, 0.0],
5 | "domainEnd": [5.0, 3.0, 2.0],
6 | "particleRadius": 0.01,
7 | "numberOfStepsPerRenderUpdate": 1,
8 | "density0": 1000,
9 | "simulationMethod": 0,
10 | "gravitation": [0.0, -9.81, 0.0],
11 | "timeStepSize": 0.0004,
12 | "stiffness": 50000,
13 | "exponent": 7,
14 | "boundaryHandlingMethod": 0,
15 | "exportFrame": false,
16 | "exportPly": false,
17 | "exportObj": false
18 | },
19 | "RigidBodies": [
20 | {
21 | "objectId": 1,
22 | "geometryFile": "./data/models/armadillo_small.obj",
23 | "translation": [4.0, 2.0, 1.2],
24 | "rotationAxis": [0, 1, 0],
25 | "rotationAngle": 180,
26 | "scale": [0.25, 0.25, 0.25],
27 | "velocity": [0.0, -5.0, 0.0],
28 | "density": 7874.0,
29 | "color": [255, 255, 255],
30 | "isDynamic": true
31 | },
32 | {
33 | "objectId": 2,
34 | "geometryFile": "./data/models/armadillo_small.obj",
35 | "translation": [2.5, 2.0, 1.2],
36 | "rotationAxis": [0, 1, 0],
37 | "rotationAngle": 180,
38 | "scale": [0.25, 0.25, 0.25],
39 | "velocity": [0.0, -5.0, 0.0],
40 | "density": 1700.0,
41 | "color": [255, 100, 50],
42 | "isDynamic": true
43 | },
44 | {
45 | "objectId": 3,
46 | "geometryFile": "./data/models/armadillo_small.obj",
47 | "translation": [1.0, 2.0, 1.2],
48 | "rotationAxis": [0, 1, 0],
49 | "rotationAngle": 180,
50 | "scale": [0.25, 0.25, 0.25],
51 | "velocity": [0.0, -5.0, 0.0],
52 | "density": 300.0,
53 | "color": [100, 100, 50],
54 | "isDynamic": true
55 | }
56 | ],
57 | "FluidBlocks": [
58 | {
59 | "objectId": 0,
60 | "start": [0.04, 0.04, 0.04],
61 | "end": [4.96, 1.50, 1.96],
62 | "translation": [0.0, 0.0, 0.0],
63 | "scale": [1, 1, 1],
64 | "velocity": [0.0, 0.0, 0.0],
65 | "density": 1000.0,
66 | "color": [50, 100, 200]
67 |
68 | }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/data/scenes/armadillo_bath_dynamic_dfsph.json:
--------------------------------------------------------------------------------
1 | {
2 | "Configuration":
3 | {
4 | "domainStart": [0.0, 0.0, 0.0],
5 | "domainEnd": [5.0, 3.0, 2.0],
6 | "particleRadius": 0.01,
7 | "numberOfStepsPerRenderUpdate": 1,
8 | "density0": 1000,
9 | "simulationMethod": 4,
10 | "gravitation": [0.0, -9.81, 0.0],
11 | "timeStepSize": 0.004,
12 | "stiffness": 50000,
13 | "exponent": 7,
14 | "boundaryHandlingMethod": 0,
15 | "exportFrame": false,
16 | "exportPly": false,
17 | "exportObj": false
18 | },
19 | "RigidBodies": [
20 | {
21 | "objectId": 1,
22 | "geometryFile": "./data/models/armadillo_small.obj",
23 | "translation": [4.0, 2.0, 1.2],
24 | "rotationAxis": [0, 1, 0],
25 | "rotationAngle": 180,
26 | "scale": [0.25, 0.25, 0.25],
27 | "velocity": [0.0, -5.0, 0.0],
28 | "density": 7874.0,
29 | "color": [255, 255, 255],
30 | "isDynamic": true
31 | },
32 | {
33 | "objectId": 2,
34 | "geometryFile": "./data/models/armadillo_small.obj",
35 | "translation": [2.5, 2.0, 1.2],
36 | "rotationAxis": [0, 1, 0],
37 | "rotationAngle": 180,
38 | "scale": [0.25, 0.25, 0.25],
39 | "velocity": [0.0, -5.0, 0.0],
40 | "density": 1700.0,
41 | "color": [255, 100, 50],
42 | "isDynamic": true
43 | },
44 | {
45 | "objectId": 3,
46 | "geometryFile": "./data/models/armadillo_small.obj",
47 | "translation": [1.0, 2.0, 1.2],
48 | "rotationAxis": [0, 1, 0],
49 | "rotationAngle": 180,
50 | "scale": [0.25, 0.25, 0.25],
51 | "velocity": [0.0, -5.0, 0.0],
52 | "density": 300.0,
53 | "color": [100, 100, 50],
54 | "isDynamic": true
55 | }
56 | ],
57 | "FluidBlocks": [
58 | {
59 | "objectId": 0,
60 | "start": [0.04, 0.04, 0.04],
61 | "end": [4.96, 1.50, 1.96],
62 | "translation": [0.0, 0.0, 0.0],
63 | "scale": [1, 1, 1],
64 | "velocity": [0.0, 0.0, 0.0],
65 | "density": 1000.0,
66 | "color": [50, 100, 200]
67 |
68 | }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/data/scenes/dragon_bath.json:
--------------------------------------------------------------------------------
1 | {
2 | "Configuration":
3 | {
4 | "domainStart": [0.0, 0.0, 0.0],
5 | "domainEnd": [5.0, 3.0, 2.0],
6 | "particleRadius": 0.01,
7 | "numberOfStepsPerRenderUpdate": 1,
8 | "density0": 1000,
9 | "simulationMethod": 0,
10 | "gravitation": [0.0, -9.81, 0.0],
11 | "timeStepSize": 0.0004,
12 | "stiffness": 50000,
13 | "exponent": 7,
14 | "boundaryHandlingMethod": 0,
15 | "exportFrame": false,
16 | "exportPly": false,
17 | "exportObj": false
18 | },
19 | "RigidBodies": [
20 | {
21 | "objectId": 1,
22 | "geometryFile": "./data/models/Dragon_50k.obj",
23 | "translation": [3.5, 0.05, 1.0],
24 | "rotationAxis": [0, 1, 0],
25 | "rotationAngle": 0,
26 | "scale": [1, 1, 1],
27 | "velocity": [0.0, 0.0, 0.0],
28 | "density": 1000.0,
29 | "color": [255, 255, 255],
30 | "isDynamic": false
31 | }
32 | ],
33 | "FluidBlocks": [
34 | {
35 | "objectId": 0,
36 | "start": [0.1, 0.1, 0.5],
37 | "end": [1.2, 2.9, 1.6],
38 | "translation": [0.2, 0.0, 0.2],
39 | "scale": [1, 1, 1],
40 | "velocity": [0.0, -1.0, 0.0],
41 | "density": 1000.0,
42 | "color": [50, 100, 200]
43 | }
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/data/scenes/dragon_bath_dfsph.json:
--------------------------------------------------------------------------------
1 | {
2 | "Configuration":
3 | {
4 | "domainStart": [0.0, 0.0, 0.0],
5 | "domainEnd": [5.0, 3.0, 2.0],
6 | "particleRadius": 0.01,
7 | "numberOfStepsPerRenderUpdate": 1,
8 | "density0": 1000,
9 | "simulationMethod": 4,
10 | "gravitation": [0.0, -9.81, 0.0],
11 | "timeStepSize": 0.004,
12 | "stiffness": 50000,
13 | "exponent": 7,
14 | "boundaryHandlingMethod": 0,
15 | "exportFrame": false,
16 | "exportPly": false,
17 | "exportObj": false
18 | },
19 | "RigidBodies": [
20 | {
21 | "objectId": 1,
22 | "geometryFile": "./data/models/Dragon_50k.obj",
23 | "translation": [3.5, 0.05, 1.0],
24 | "rotationAxis": [0, 1, 0],
25 | "rotationAngle": 0,
26 | "scale": [1, 1, 1],
27 | "velocity": [0.0, 0.0, 0.0],
28 | "density": 1000.0,
29 | "color": [255, 255, 255],
30 | "isDynamic": false
31 | }
32 | ],
33 | "FluidBlocks": [
34 | {
35 | "objectId": 0,
36 | "start": [0.1, 0.1, 0.5],
37 | "end": [1.2, 2.9, 1.6],
38 | "translation": [0.2, 0.0, 0.2],
39 | "scale": [1, 1, 1],
40 | "velocity": [0.0, -1.0, 0.0],
41 | "density": 1000.0,
42 | "color": [50, 100, 200]
43 | }
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/data/scenes/dragon_bath_dynamic_dfsph.json:
--------------------------------------------------------------------------------
1 | {
2 | "Configuration":
3 | {
4 | "domainStart": [0.0, 0.0, 0.0],
5 | "domainEnd": [5.0, 3.0, 2.0],
6 | "particleRadius": 0.01,
7 | "numberOfStepsPerRenderUpdate": 1,
8 | "density0": 1000,
9 | "simulationMethod": 4,
10 | "gravitation": [0.0, -9.81, 0.0],
11 | "timeStepSize": 0.004,
12 | "stiffness": 50000,
13 | "exponent": 7,
14 | "boundaryHandlingMethod": 0,
15 | "exportFrame": false,
16 | "exportPly": false,
17 | "exportObj": false
18 | },
19 | "RigidBodies": [
20 | {
21 | "objectId": 1,
22 | "geometryFile": "./data/models/Dragon_50k.obj",
23 | "translation": [3.5, 0.05, 1.0],
24 | "rotationAxis": [0, 1, 0],
25 | "rotationAngle": 0,
26 | "scale": [1, 1, 1],
27 | "velocity": [0.0, 0.0, 0.0],
28 | "density": 1000.0,
29 | "color": [255, 255, 255],
30 | "isDynamic": true
31 | }
32 | ],
33 | "FluidBlocks": [
34 | {
35 | "objectId": 0,
36 | "start": [0.1, 0.1, 0.5],
37 | "end": [1.2, 2.9, 1.6],
38 | "translation": [0.2, 0.0, 0.2],
39 | "scale": [1, 1, 1],
40 | "velocity": [0.0, -1.0, 0.0],
41 | "density": 1000.0,
42 | "color": [50, 100, 200]
43 | }
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/data/scenes/high_fluid_dfsph.json:
--------------------------------------------------------------------------------
1 | {
2 | "Configuration":
3 | {
4 | "domainStart": [0.0, 0.0, 0.0],
5 | "domainEnd": [2.0, 6.0, 2.0],
6 | "particleRadius": 0.01,
7 | "numberOfStepsPerRenderUpdate": 1,
8 | "density0": 1000,
9 | "simulationMethod": 4,
10 | "gravitation": [0.0, -9.81, 0.0],
11 | "timeStepSize": 0.004,
12 | "stiffness": 50000,
13 | "exponent": 7,
14 | "boundaryHandlingMethod": 0,
15 | "exportFrame": false,
16 | "exportPly": false,
17 | "exportObj": false
18 | },
19 | "FluidBlocks": [
20 | {
21 | "objectId": 0,
22 | "start": [0.0, 0.0, 0.0],
23 | "end": [0.6, 5.4, 0.6],
24 | "translation": [0.1, 0.1, 0.1],
25 | "scale": [1, 1, 1],
26 | "velocity": [0.0, 0.0, 0.0],
27 | "density": 1000.0,
28 | "color": [50, 100, 200]
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/data/scenes/high_fluid_wcsph.json:
--------------------------------------------------------------------------------
1 | {
2 | "Configuration":
3 | {
4 | "domainStart": [0.0, 0.0, 0.0],
5 | "domainEnd": [2.0, 6.0, 2.0],
6 | "particleRadius": 0.01,
7 | "numberOfStepsPerRenderUpdate": 1,
8 | "density0": 1000,
9 | "simulationMethod": 0,
10 | "gravitation": [0.0, -9.81, 0.0],
11 | "timeStepSize": 0.0004,
12 | "stiffness": 50000,
13 | "exponent": 7,
14 | "boundaryHandlingMethod": 0,
15 | "exportFrame": false,
16 | "exportPly": false,
17 | "exportObj": false
18 | },
19 | "FluidBlocks": [
20 | {
21 | "objectId": 0,
22 | "start": [0.0, 0.0, 0.0],
23 | "end": [0.6, 5.4, 0.6],
24 | "translation": [0.1, 0.1, 0.1],
25 | "scale": [1, 1, 1],
26 | "velocity": [0.0, 0.0, 0.0],
27 | "density": 1000.0,
28 | "color": [50, 100, 200]
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/demo_high_fluid.py:
--------------------------------------------------------------------------------
1 | import taichi as ti
2 | import numpy as np
3 | import trimesh as tm
4 | from particle_system import ParticleSystem
5 | from WCSPH import WCSPHSolver
6 | from IISPH import IISPHSolver
7 |
8 | # ti.init(arch=ti.cpu)
9 |
10 | # Use GPU for higher peformance if available
11 | ti.init(arch=ti.cuda, device_memory_GB=2, kernel_profiler=True)
12 |
13 |
14 | if __name__ == "__main__":
15 | x_max = 2.0
16 | y_max = 6.0
17 | z_max = 2.0
18 |
19 | domain_size = np.array([x_max, y_max, z_max])
20 |
21 | box_anchors = ti.Vector.field(3, dtype=ti.f32, shape = 8)
22 | box_anchors[0] = ti.Vector([0.0, 0.0, 0.0])
23 | box_anchors[1] = ti.Vector([0.0, y_max, 0.0])
24 | box_anchors[2] = ti.Vector([x_max, 0.0, 0.0])
25 | box_anchors[3] = ti.Vector([x_max, y_max, 0.0])
26 |
27 | box_anchors[4] = ti.Vector([0.0, 0.0, z_max])
28 | box_anchors[5] = ti.Vector([0.0, y_max, z_max])
29 | box_anchors[6] = ti.Vector([x_max, 0.0, z_max])
30 | box_anchors[7] = ti.Vector([x_max, y_max, z_max])
31 |
32 | box_lines_indices = ti.field(int, shape=(2 * 12))
33 |
34 | for i, val in enumerate([0, 1, 0, 2, 1, 3, 2, 3, 4, 5, 4, 6, 5, 7, 6, 7, 0, 4, 1, 5, 2, 6, 3, 7]):
35 | box_lines_indices[i] = val
36 |
37 | dim = 3
38 | substeps = 1
39 | output_frames = False
40 | output_ply = False
41 | solver_type = "WCSPH"
42 | # solver_type = "IISPH"
43 | ps = ParticleSystem(domain_size, GGUI=True)
44 |
45 | x_offset = 0.2
46 | y_offset = 0.2
47 | z_offset = 0.2
48 |
49 | # mesh = tm.load("./data/Dragon_50k.obj")
50 | # # mesh = tm.load("./data/bunny_sparse.obj")
51 | # # mesh = tm.load("./data/bunny.stl")
52 | # mesh_scale = 0.8
53 | # mesh.apply_scale(mesh_scale)
54 | # offset = np.array([1.5, 0.0 + y_offset, 1.0])
55 | # is_success = tm.repair.fill_holes(mesh)
56 | # print("Is the mesh successfully repaired? ", is_success)
57 | # voxelized_mesh = mesh.voxelized(pitch=ps.particle_diameter).fill()
58 | # # voxelized_mesh = mesh.voxelized(pitch=ps.particle_diameter).hollow()
59 | # # voxelized_mesh.show()
60 | # voxelized_points_np = voxelized_mesh.points + offset
61 | # num_particles_obj = voxelized_points_np.shape[0]
62 | # voxelized_points = ti.Vector.field(3, ti.f32, num_particles_obj)
63 | # voxelized_points.from_numpy(voxelized_points_np)
64 |
65 | # print("Rigid body, num of particles: ", num_particles_obj)
66 |
67 | # ps.add_particles(2,
68 | # num_particles_obj,
69 | # voxelized_points_np, # position
70 | # 0.0 * np.ones((num_particles_obj, 3)), # velocity
71 | # 10 * np.ones(num_particles_obj), # density
72 | # np.zeros(num_particles_obj), # pressure
73 | # np.array([0 for _ in range(num_particles_obj)], dtype=int), # material
74 | # 1 * np.ones(num_particles_obj), # is_dynamic
75 | # 255 * np.ones((num_particles_obj, 3))) # color
76 |
77 | # Fluid -1
78 | ps.add_cube(object_id=0,
79 | lower_corner=[0.1+x_offset, 0.1 + y_offset, 0.5+z_offset],
80 | cube_size=[0.6, 5.4, 0.6],
81 | velocity=[0.0, -0.0, 0.0],
82 | density=1000.0,
83 | is_dynamic=1,
84 | color=(50,100,200),
85 | material=1)
86 |
87 | # # Bottom boundary
88 | # ps.add_cube(object_id=1,
89 | # lower_corner=[0.0+x_offset, 0.0 + y_offset, 0.0+z_offset],
90 | # cube_size=[x_max-x_offset*2, ps.particle_diameter-0.001, z_max-z_offset*2],
91 | # velocity=[0.0, 0.0, 0.0],
92 | # density=1000.0,
93 | # is_dynamic=0,
94 | # color=(255,255,255),
95 | # material=0)
96 |
97 | # # left boundary
98 | # ps.add_cube(object_id=1,
99 | # lower_corner=[0.0+x_offset, 0.0 + y_offset, 0.0+z_offset],
100 | # cube_size=[ps.particle_diameter-0.001, y_max-y_offset*2, z_max-z_offset*2],
101 | # velocity=[0.0, 0.0, 0.0],
102 | # density=1000.0,
103 | # is_dynamic=0,
104 | # color=(255,255,255),
105 | # material=0)
106 |
107 | # # back
108 | # ps.add_cube(object_id=1,
109 | # lower_corner=[0.0+x_offset, 0.0 + y_offset, 0.0+z_offset],
110 | # cube_size=[x_max-x_offset*2, y_max-y_offset*2, ps.particle_diameter-0.001],
111 | # velocity=[0.0, 0.0, 0.0],
112 | # density=1000.0,
113 | # is_dynamic=0,
114 | # color=(255,255,255),
115 | # material=0)
116 |
117 | # # front
118 | # ps.add_cube(object_id=1,
119 | # lower_corner=[0.0+x_offset, 0.0 + y_offset, z_max - z_offset],
120 | # cube_size=[x_max-x_offset*2, y_max-y_offset*2, ps.particle_diameter-0.001],
121 | # velocity=[0.0, 0.0, 0.0],
122 | # density=1000.0,
123 | # is_dynamic=0,
124 | # color=(255,255,255),
125 | # material=0)
126 |
127 | # # right
128 | # ps.add_cube(object_id=1,
129 | # lower_corner=[x_max-x_offset, 0.0 + y_offset, 0.0+z_offset],
130 | # cube_size=[ps.particle_diameter-0.001, y_max-y_offset*2, z_max-z_offset*2],
131 | # velocity=[0.0, 0.0, 0.0],
132 | # density=1000.0,
133 | # is_dynamic=0,
134 | # color=(255,255,255),
135 | # material=0)
136 |
137 | if solver_type == "WCSPH":
138 | solver = WCSPHSolver(ps)
139 | elif solver_type == "IISPH":
140 | solver = IISPHSolver(ps)
141 |
142 |
143 | window = ti.ui.Window('SPH', (1024, 1024), show_window = True, vsync=False)
144 |
145 | scene = ti.ui.Scene()
146 | camera = ti.ui.make_camera()
147 | camera.position(0.0, 3.0, 5.0)
148 | camera.up(0.0, 1.0, 0.0)
149 | camera.lookat(0.0, 0.0, 0.0)
150 | camera.fov(60)
151 | scene.set_camera(camera)
152 |
153 | canvas = window.get_canvas()
154 | radius = 0.002
155 | movement_speed = 0.02
156 | background_color = (0, 0, 0) # 0xFFFFFF
157 | particle_color = (1, 1, 1)
158 |
159 | cnt = 0
160 | cnt_ply = 0
161 | solver.initialize_solver()
162 | series_prefix = "output/object_{}_demo_test.ply"
163 |
164 |
165 | # ti.profiler.clear_kernel_profiler_info()
166 | while window.running:
167 | for i in range(substeps):
168 | solver.step()
169 | # ps.copy_to_vis_buffer(invisible_objects=[1])
170 | if ps.dim == 2:
171 | canvas.set_background_color(background_color)
172 | canvas.circles(ps.x_vis_buffer, radius=ps.particle_radius / 5, color=particle_color)
173 | elif ps.dim == 3:
174 | # # user controlling of camera
175 | # position_change = ti.Vector([0.0, 0.0, 0.0])
176 | # up = ti.Vector([0.0, 1.0, 0.0])
177 | # # move camera up and down
178 | # if window.is_pressed("e"):
179 | # position_change = up * movement_speed
180 | # if window.is_pressed("q"):
181 | # position_change = -up * movement_speed
182 | # camera.position(*(camera.curr_position + position_change))
183 | # camera.lookat(*(camera.curr_lookat + position_change))
184 | camera.track_user_inputs(window, movement_speed=movement_speed, hold_key=ti.ui.LMB)
185 | scene.set_camera(camera)
186 |
187 | scene.point_light((2.0, 2.0, 2.0), color=(1.0, 1.0, 1.0))
188 | # scene.particles(ps.x_vis_buffer, radius=ps.particle_radius, per_vertex_color=ps.color_vis_buffer)
189 | scene.particles(ps.x, radius=ps.particle_radius, color=(50/255,100/255,200/255))
190 |
191 | scene.lines(box_anchors, indices=box_lines_indices, color = (0.99, 0.68, 0.28), width = 1.0)
192 | canvas.scene(scene)
193 |
194 | if output_frames:
195 | if cnt % 2 == 0:
196 | window.write_image(f"img_high_fluid_output/{cnt:04}.png")
197 | if output_ply:
198 | if cnt % 20 == 0:
199 | obj_id = 0
200 | obj_data = ps.dump(obj_id=obj_id)
201 | np_pos = obj_data["position"]
202 | writer = ti.tools.PLYWriter(num_vertices=ps.object_collection[obj_id])
203 | writer.add_vertex_pos(np_pos[:, 0], np_pos[:, 1], np_pos[:, 2])
204 | writer.export_frame_ascii(cnt_ply, series_prefix.format(0))
205 | cnt_ply += 1
206 | cnt += 1
207 | window.show()
208 | ti.profiler.print_kernel_profiler_info()
209 |
--------------------------------------------------------------------------------
/legacy/README.md:
--------------------------------------------------------------------------------
1 | # SPH_Taichi
2 | A [Taichi](https://github.com/taichi-dev/taichi) implementation of Smooth Particle Hydrodynamics (SPH) simulator. Taichi is a productive & portable programming language for high-performance, sparse & differentiable computing. The 2D case below can be run and rendered efficiently on a laptop thanks to Taichi.
3 |
4 | ## Features
5 | Currently, the following features have been implemented:
6 | - Weakly Compressible SPH (WCSPH)[1]
7 | - Predictive-Corrective Incompressible SPH (PCISPH)[2]
8 | - Divergence free SPH (DFSPH)[3]
9 |
10 | ### Note: Updates on November 6, 2021
11 | The code is now compatible with Taichi `v0.8.3`.
12 |
13 | ## Example
14 |
15 | - Demo (PCISPH, 4.5k particles)
16 |
17 | Run ```python scene.py --method PCISPH```
18 |
19 |
20 |
21 |
22 | Demos for the other two methods: ```python scene.py --method WCSPH``` or ```python scene.py --method DFSPH```
23 |
24 | ## Reference
25 | 1. M. Becker and M. Teschner (2007). "Weakly compressible SPH for free surface flows". In:Proceedings of the 2007 ACM SIGGRAPH/Eurographics symposium on Computer animation. Eurographics Association, pp. 209–217.
26 | 2. B. Solenthaler and R. Pajarola (2009). “Predictive-corrective incompressible SPH”. In: ACM SIGGRAPH 2009 papers, pp. 1–6.
27 | 3. J. Bender, D. Koschier (2015) Divergence-free smoothed particle hydrodynamics[C]//Proceedings of the 14th ACM SIGGRAPH/Eurographics symposium on computer animation. ACM, 2015: 147-155.
28 |
29 |
--------------------------------------------------------------------------------
/legacy/engine/__init__.py:
--------------------------------------------------------------------------------
1 | from . import sph_solver
2 |
--------------------------------------------------------------------------------
/legacy/engine/sph_solver.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import time
3 | from functools import reduce
4 | import taichi as ti
5 |
6 |
7 | @ti.data_oriented
8 | class SPHSolver:
9 | method_WCSPH = 0
10 | method_PCISPH = 1
11 | method_DFSPH = 2
12 | methods = {
13 | 'WCSPH': method_WCSPH,
14 | 'PCISPH': method_PCISPH,
15 | 'DFSPH': method_DFSPH
16 | }
17 | material_fluid = 1
18 | material_bound = 0
19 | materials = {'fluid': material_fluid, 'bound': material_bound}
20 |
21 | def __init__(self,
22 | res,
23 | screen_to_world_ratio,
24 | bound,
25 | alpha=0.5,
26 | dx=0.2,
27 | max_num_particles=2**20,
28 | padding=12,
29 | max_time=5.0,
30 | max_steps=50000,
31 | dynamic_allocate=False,
32 | adaptive_time_step=True,
33 | method=0):
34 | self.method = method
35 | self.adaptive_time_step = adaptive_time_step
36 | self.dim = len(res)
37 | self.res = res
38 | self.screen_to_world_ratio = screen_to_world_ratio
39 | self.dynamic_allocate = dynamic_allocate
40 | # self.padding = padding / screen_to_world_ratio
41 | self.padding = 2 * dx
42 | self.max_time = max_time
43 | self.max_steps = max_steps
44 | self.max_num_particles = max_num_particles
45 |
46 | self.g = -9.80 # Gravity
47 | self.alpha = alpha # viscosity
48 | self.rho_0 = 1000.0 # reference density
49 | self.CFL_v = 0.25 # CFL coefficient for velocity
50 | self.CFL_a = 0.05 # CFL coefficient for acceleration
51 |
52 | self.df_fac = 1.3
53 | self.dx = dx # Particle radius
54 | self.dh = self.dx * self.df_fac # Smooth length
55 | # Declare dt as ti var for adaptive time step
56 | self.dt = ti.field(ti.f32, shape=())
57 | # Particle parameters
58 | self.m = self.dx**self.dim * self.rho_0
59 |
60 | self.grid_size = 2 * self.dh
61 | self.grid_pos = np.ceil(
62 | np.array(res) / self.screen_to_world_ratio /
63 | self.grid_size).astype(int)
64 |
65 | self.top_bound = bound[0] # top_bound
66 | self.bottom_bound = bound[1] # bottom_bound
67 | self.left_bound = bound[2] # left_bound
68 | self.right_bound = bound[3] # right_bound
69 |
70 | # ----------- WCSPH parameters-----------------
71 | # Pressure state function parameters(WCSPH)
72 | self.gamma = 7.0
73 | self.c_0 = 200.0
74 |
75 | # ----------- PCISPH parameters-----------------
76 | # Scaling factor for PCISPH
77 | self.s_f = ti.field(ti.f32, shape=())
78 | # Max iteration steps for pressure correction
79 | self.it = 0
80 | self.max_it = 0
81 | self.sub_max_iteration = 3
82 | self.rho_err = ti.Vector.field(1, dtype=ti.f32, shape=())
83 | self.max_rho_err = ti.Vector.field(1, dtype=ti.f32, shape=())
84 |
85 | # ----------- DFSPH parameters-----------------
86 | # Summing up the rho for all particles to compute the average rho
87 | self.sum_rho_err = ti.field(ti.f32, shape=())
88 | self.sum_drho = ti.field(ti.f32, shape=())
89 |
90 | # Dynamic Fill particles use
91 | self.source_bound = ti.Vector.field(self.dim, dtype=ti.f32, shape=2)
92 | self.source_velocity = ti.Vector.field(self.dim, dtype=ti.f32, shape=())
93 | self.source_pressure = ti.Vector.field(1, dtype=ti.f32, shape=())
94 | self.source_density = ti.Vector.field(1, dtype=ti.f32, shape=())
95 |
96 | self.particle_num = ti.field(ti.i32, shape=())
97 | self.particle_positions = ti.Vector.field(self.dim, dtype=ti.f32)
98 | self.particle_velocity = ti.Vector.field(self.dim, dtype=ti.f32)
99 | self.particle_positions_new = ti.Vector.field(
100 | self.dim, dtype=ti.f32) # Prediction values for P-C scheme use
101 | self.particle_velocity_new = ti.Vector.field(
102 | self.dim, dtype=ti.f32) # Prediction values for P-C scheme use
103 | self.particle_pressure = ti.Vector.field(1, dtype=ti.f32)
104 | self.particle_pressure_acc = ti.Vector.field(
105 | self.dim, dtype=ti.f32) # pressure force for PCISPH use
106 | self.particle_density = ti.Vector.field(1, dtype=ti.f32)
107 | self.particle_density_new = ti.Vector.field(
108 | 1, dtype=ti.f32) # Prediction values for P-C scheme use
109 | self.particle_alpha = ti.Vector.field(1, dtype=ti.f32) # For DFSPH use
110 | self.particle_stiff = ti.Vector.field(1, dtype=ti.f32) # For DFSPH use
111 |
112 | self.color = ti.field(dtype=ti.f32)
113 | self.material = ti.field(dtype=ti.f32)
114 |
115 | self.d_velocity = ti.Vector.field(self.dim, dtype=ti.f32)
116 | self.d_density = ti.Vector.field(1, dtype=ti.f32)
117 |
118 | self.grid_num_particles = ti.field(ti.i32)
119 | self.grid2particles = ti.field(ti.i32)
120 | self.particle_num_neighbors = ti.field(ti.i32)
121 | self.particle_neighbors = ti.field(ti.i32)
122 |
123 | self.max_num_particles_per_cell = 100
124 | self.max_num_neighbors = 100
125 |
126 | self.max_v = 0.0
127 | self.max_a = 0.0
128 | self.max_rho = 0.0
129 | self.max_pressure = 0.0
130 |
131 | if dynamic_allocate:
132 | ti.root.dynamic(ti.i, max_num_particles, 2**18).place(
133 | self.particle_positions, self.particle_velocity,
134 | self.particle_pressure, self.particle_density,
135 | self.particle_density_new, self.d_velocity, self.d_density,
136 | self.material, self.color, self.particle_positions_new,
137 | self.particle_velocity_new, self.particle_pressure_acc,
138 | self.particle_alpha, self.particle_stiff)
139 | else:
140 | # Allocate enough memory
141 | ti.root.dense(ti.i, 2**18).place(
142 | self.particle_positions, self.particle_velocity,
143 | self.particle_pressure, self.particle_density,
144 | self.particle_density_new, self.d_velocity, self.d_density,
145 | self.material, self.color, self.particle_positions_new,
146 | self.particle_velocity_new, self.particle_pressure_acc,
147 | self.particle_alpha, self.particle_stiff)
148 |
149 | if self.dim == 2:
150 | grid_snode = ti.root.dense(ti.ij, self.grid_pos)
151 | grid_snode.place(self.grid_num_particles)
152 | grid_snode.dense(ti.k, self.max_num_particles_per_cell).place(
153 | self.grid2particles)
154 | else:
155 | grid_snode = ti.root.dense(ti.ijk, self.grid_pos)
156 | grid_snode.place(self.grid_num_particles)
157 | grid_snode.dense(ti.l, self.max_num_particles_per_cell).place(
158 | self.grid2particles)
159 |
160 | nb_node = ti.root.dynamic(ti.i, max_num_particles)
161 | nb_node.place(self.particle_num_neighbors)
162 | nb_node.dense(ti.j,
163 | self.max_num_neighbors).place(self.particle_neighbors)
164 |
165 | # Initialize dt
166 | if method == SPHSolver.method_WCSPH:
167 | self.dt.from_numpy(
168 | np.array(0.1 * self.dh / self.c_0, dtype=np.float32))
169 | self.CFL_v = 0.20 # CFL coefficient for velocity
170 | self.CFL_a = 0.20 # CFL coefficient for acceleration
171 |
172 | if method == SPHSolver.method_PCISPH:
173 | self.s_f.from_numpy(np.array(1.0, dtype=np.float32))
174 | if self.adaptive_time_step:
175 | self.dt.from_numpy(np.array(0.0015, dtype=np.float32))
176 | else:
177 | self.dt.from_numpy(np.array(0.00015, dtype=np.float32))
178 |
179 | if method == SPHSolver.method_DFSPH:
180 | self.dt.from_numpy(
181 | np.array(1.0 * self.dh / self.c_0, dtype=np.float32))
182 | self.CFL_v = 0.30 # CFL coefficient for velocity
183 | self.CFL_a = 0.05 # CFL coefficient for acceleration
184 |
185 | @ti.func
186 | def compute_grid_index(self, pos):
187 | return (pos / (2 * self.dh)).cast(int)
188 |
189 | @ti.kernel
190 | def allocate_particles(self):
191 | # Ref to pbf2d example from by Ye Kuang (k-ye)
192 | # https://github.com/taichi-dev/taichi/blob/master/examples/pbf2d.py
193 | # allocate particles to grid
194 | for p_i in range(self.particle_num[None]):
195 | # Compute the grid index
196 | cell = self.compute_grid_index(self.particle_positions[p_i])
197 | offs = self.grid_num_particles[cell].atomic_add(1)
198 | self.grid2particles[cell, offs] = p_i
199 |
200 | @ti.func
201 | def is_in_grid(self, c):
202 | res = 1
203 | for i in ti.static(range(self.dim)):
204 | res = ti.atomic_and(res, (0 <= c[i] < self.grid_pos[i]))
205 | return res
206 |
207 | @ti.func
208 | def is_fluid(self, p):
209 | # check fluid particle or bound particle
210 | return self.material[p]
211 |
212 | @ti.kernel
213 | def search_neighbors(self):
214 | # Ref to pbf2d example from by Ye Kuang (k-ye)
215 | # https://github.com/taichi-dev/taichi/blob/master/examples/pbf2d.py
216 | for p_i in range(self.particle_num[None]):
217 | pos_i = self.particle_positions[p_i]
218 | nb_i = 0
219 | if self.is_fluid(p_i) == 1 or self.is_fluid(p_i) == 0:
220 | # Compute the grid index on the fly
221 | cell = self.compute_grid_index(self.particle_positions[p_i])
222 | for offs in ti.static(
223 | ti.grouped(ti.ndrange(*((-1, 2), ) * self.dim))):
224 | cell_to_check = cell + offs
225 | if self.is_in_grid(cell_to_check) == 1:
226 | for j in range(self.grid_num_particles[cell_to_check]):
227 | p_j = self.grid2particles[cell_to_check, j]
228 | if nb_i < self.max_num_neighbors and p_j != p_i and (
229 | pos_i - self.particle_positions[p_j]
230 | ).norm() < self.dh * 2.00:
231 | self.particle_neighbors[p_i, nb_i] = p_j
232 | nb_i.atomic_add(1)
233 | self.particle_num_neighbors[p_i] = nb_i
234 |
235 | @ti.func
236 | def cubic_kernel(self, r, h):
237 | # value of cubic spline smoothing kernel
238 | k = 10. / (7. * np.pi * h**self.dim)
239 | q = r / h
240 | # assert q >= 0.0 # Metal backend is not happy with assert
241 | res = ti.cast(0.0, ti.f32)
242 | if q <= 1.0:
243 | res = k * (1 - 1.5 * q**2 + 0.75 * q**3)
244 | elif q < 2.0:
245 | res = k * 0.25 * (2 - q)**3
246 | return res
247 |
248 | @ti.func
249 | def cubic_kernel_derivative(self, r, h):
250 | # derivative of cubic spline smoothing kernel
251 | k = 10. / (7. * np.pi * h**self.dim)
252 | q = r / h
253 | # assert q > 0.0
254 | res = ti.cast(0.0, ti.f32)
255 | if q < 1.0:
256 | res = (k / h) * (-3 * q + 2.25 * q**2)
257 | elif q < 2.0:
258 | res = -0.75 * (k / h) * (2 - q)**2
259 | return res
260 |
261 | @ti.func
262 | def rho_derivative(self, ptc_i, ptc_j, r, r_mod):
263 | # density delta, i.e. divergence
264 | return self.m * self.cubic_kernel_derivative(r_mod, self.dh) \
265 | * (self.particle_velocity[ptc_i] - self.particle_velocity[ptc_j]).dot(r / r_mod)
266 |
267 | @ti.func
268 | def p_update(self, rho, rho_0=1000.0, gamma=7.0, c_0=20.0):
269 | # Weakly compressible, tait function
270 | b = rho_0 * c_0**2 / gamma
271 | return b * ((rho / rho_0)**gamma - 1.0)
272 |
273 | @ti.func
274 | def pressure_force(self, ptc_i, ptc_j, r, r_mod):
275 | # Compute the pressure force contribution, Symmetric Formula
276 | res = ti.Vector([0.0 for _ in range(self.dim)])
277 | res = -self.m * (self.particle_pressure[ptc_i][0] / self.particle_density[ptc_i][0] ** 2
278 | + self.particle_pressure[ptc_j][0] / self.particle_density[ptc_j][0] ** 2) \
279 | * self.cubic_kernel_derivative(r_mod, self.dh) * r / r_mod
280 | return res
281 |
282 | @ti.func
283 | def viscosity_force(self, ptc_i, ptc_j, r, r_mod):
284 | # Compute the viscosity force contribution, artificial viscosity
285 | res = ti.Vector([0.0 for _ in range(self.dim)])
286 | v_xy = (self.particle_velocity[ptc_i] -
287 | self.particle_velocity[ptc_j]).dot(r)
288 | if v_xy < 0:
289 | # Artifical viscosity
290 | vmu = -2.0 * self.alpha * self.dx * self.c_0 / (
291 | self.particle_density[ptc_i][0] +
292 | self.particle_density[ptc_j][0])
293 | res = -self.m * vmu * v_xy / (
294 | r_mod**2 + 0.01 * self.dx**2) * self.cubic_kernel_derivative(
295 | r_mod, self.dh) * r / r_mod
296 | return res
297 |
298 | @ti.func
299 | def simulate_collisions(self, ptc_i, vec, d):
300 | # Collision factor, assume roughly (1-c_f)*velocity loss after collision
301 | c_f = 0.3
302 | self.particle_positions[ptc_i] += vec * d
303 | self.particle_velocity[ptc_i] -= (
304 | 1.0 + c_f) * self.particle_velocity[ptc_i].dot(vec) * vec
305 | if self.method == SPHSolver.method_DFSPH:
306 | self.particle_velocity_new[ptc_i] -= (
307 | 1.0 + c_f) * self.particle_velocity_new[ptc_i].dot(vec) * vec
308 |
309 | @ti.kernel
310 | def enforce_boundary(self):
311 | # TODO: only handle 2D case currently
312 | for p_i in range(self.particle_num[None]):
313 | if self.is_fluid(p_i) == 1:
314 | pos = self.particle_positions[p_i]
315 | if pos[0] < self.left_bound + 0.5 * self.padding:
316 | self.simulate_collisions(
317 | p_i, ti.Vector([1.0, 0.0]),
318 | self.left_bound + 0.5 * self.padding - pos[0])
319 | if pos[0] > self.right_bound - 0.5 * self.padding:
320 | self.simulate_collisions(
321 | p_i, ti.Vector([-1.0, 0.0]),
322 | pos[0] - self.right_bound + 0.5 * self.padding)
323 | if pos[1] > self.top_bound - self.padding:
324 | self.simulate_collisions(
325 | p_i, ti.Vector([0.0, -1.0]),
326 | pos[1] - self.top_bound + self.padding)
327 | if pos[1] < self.bottom_bound + self.padding:
328 | self.simulate_collisions(
329 | p_i, ti.Vector([0.0, 1.0]),
330 | self.bottom_bound + self.padding - pos[1])
331 |
332 | @ti.kernel
333 | def wc_compute_deltas(self):
334 | for p_i in range(self.particle_num[None]):
335 | pos_i = self.particle_positions[p_i]
336 | d_v = ti.Vector([0.0 for _ in range(self.dim)])
337 | d_rho = 0.0
338 | for j in range(self.particle_num_neighbors[p_i]):
339 | p_j = self.particle_neighbors[p_i, j]
340 | pos_j = self.particle_positions[p_j]
341 |
342 | # Compute distance and its mod
343 | r = pos_i - pos_j
344 | r_mod = ti.max(r.norm(), 1e-5)
345 |
346 | # Compute Density change
347 | d_rho += self.rho_derivative(p_i, p_j, r, r_mod)
348 |
349 | if self.is_fluid(p_i) == 1:
350 | # Compute Viscosity force contribution
351 | d_v += self.viscosity_force(p_i, p_j, r, r_mod)
352 |
353 | # Compute Pressure force contribution
354 | d_v += self.pressure_force(p_i, p_j, r, r_mod)
355 |
356 | # Add body force
357 | if self.is_fluid(p_i) == 1:
358 | val = [0.0 for _ in range(self.dim - 1)]
359 | val.extend([self.g])
360 | d_v += ti.Vector(val)
361 | self.d_velocity[p_i] = d_v
362 | self.d_density[p_i][0] = d_rho
363 |
364 | @ti.kernel
365 | def wc_update_time_step(self):
366 | # Simple Forward Euler currently
367 | for p_i in range(self.particle_num[None]):
368 | if self.is_fluid(p_i) == 1:
369 | self.particle_velocity[p_i] += self.dt[None] * self.d_velocity[p_i]
370 | self.particle_positions[
371 | p_i] += self.dt[None] * self.particle_velocity[p_i]
372 | self.particle_density[p_i][0] += self.dt[None] * self.d_density[p_i][0]
373 | self.particle_pressure[p_i][0] = self.p_update(
374 | self.particle_density[p_i][0], self.rho_0, self.gamma,
375 | self.c_0)
376 |
377 | @ti.kernel
378 | def pci_scaling_factor(self):
379 | grad_sum = ti.Vector([0.0 for _ in range(self.dim)])
380 | grad_dot_sum = 0.0
381 | range_num = ti.cast(self.dh * 2.0 / self.dx, ti.i32)
382 | half_range = ti.cast(0.5 * range_num, ti.i32)
383 | # TODO: only handle 2D case currently
384 | for x in range(-half_range, half_range):
385 | for y in range(-half_range, half_range):
386 | r = ti.Vector([-x * self.dx, -y * self.dx])
387 | r_mod = r.norm()
388 | if 2.0 * self.dh > r_mod > 1e-5:
389 | grad = self.cubic_kernel_derivative(r_mod,
390 | self.dh) * r / r_mod
391 | grad_sum += grad
392 | grad_dot_sum += grad.dot(grad)
393 |
394 | beta = 2 * (self.dt[None] * self.m / self.rho_0)**2
395 | self.s_f[None] = 1.0 / ti.max(
396 | beta * (grad_sum.dot(grad_sum) + grad_dot_sum), 1e-6)
397 |
398 | @ti.kernel
399 | def pci_pos_vel_prediction(self):
400 | for p_i in range(self.particle_num[None]):
401 | if self.is_fluid(p_i) == 1:
402 | self.particle_velocity_new[
403 | p_i] = self.particle_velocity[p_i] + self.dt[None] * (
404 | self.d_velocity[p_i] + self.particle_pressure_acc[p_i])
405 | self.particle_positions_new[p_i] = self.particle_positions[
406 | p_i] + self.dt[None] * self.particle_velocity_new[p_i]
407 | # Initialize the max_rho_err
408 | self.max_rho_err[None][0] = 0.0
409 |
410 | @ti.kernel
411 | def pci_update_pressure(self):
412 | for p_i in range(self.particle_num[None]):
413 | pos_i = self.particle_positions_new[p_i]
414 | d_rho = 0.0
415 | curr_rho = 0.0
416 | for j in range(self.particle_num_neighbors[p_i]):
417 | p_j = self.particle_neighbors[p_i, j]
418 | pos_j = self.particle_positions_new[p_j]
419 |
420 | # Compute distance and its mod
421 | r = pos_i - pos_j
422 | r_mod = r.norm()
423 | if r_mod > 1e-5:
424 | # Compute Density change
425 | d_rho += self.cubic_kernel_derivative(r_mod, self.dh) \
426 | * (self.particle_velocity_new[p_i] - self.particle_velocity_new[p_j]).dot(r / r_mod)
427 |
428 | self.d_density[p_i][0] = d_rho
429 | # Avoid negative density variation
430 | self.rho_err[None][0] = self.particle_density[p_i][
431 | 0] + self.dt[None] * d_rho - self.rho_0
432 | self.max_rho_err[None][0] = max(abs(self.rho_err[None][0]),
433 | self.max_rho_err[None][0])
434 | self.particle_pressure[p_i][
435 | 0] += self.s_f[None] * self.rho_err[None][0]
436 |
437 | @ti.kernel
438 | def pci_update_pressure_force(self):
439 | for p_i in range(self.particle_num[None]):
440 | pos_i = self.particle_positions_new[p_i]
441 | d_vp = ti.Vector([0.0 for _ in range(self.dim)])
442 | for j in range(self.particle_num_neighbors[p_i]):
443 | p_j = self.particle_neighbors[p_i, j]
444 | pos_j = self.particle_positions_new[p_j]
445 | # Compute distance and its mod
446 | r = pos_i - pos_j
447 | r_mod = r.norm()
448 | if r_mod > 1e-5:
449 | # Compute Pressure force contribution
450 | d_vp += self.pressure_force(p_i, p_j, r, r_mod)
451 | self.particle_pressure_acc[p_i] = d_vp
452 |
453 | def pci_pc_iteration(self):
454 | self.pci_pos_vel_prediction()
455 | self.pci_update_pressure()
456 | self.pci_update_pressure_force()
457 |
458 | @ti.kernel
459 | def pci_compute_deltas(self):
460 | for p_i in range(self.particle_num[None]):
461 | pos_i = self.particle_positions[p_i]
462 | d_v = ti.Vector([0.0 for _ in range(self.dim)])
463 |
464 | for j in range(self.particle_num_neighbors[p_i]):
465 | p_j = self.particle_neighbors[p_i, j]
466 | pos_j = self.particle_positions[p_j]
467 |
468 | # Compute distance and its mod
469 | r = pos_i - pos_j
470 | r_mod = r.norm()
471 |
472 | if r_mod > 1e-5 and self.is_fluid(p_i) == 1:
473 | # Compute Viscosity force contribution
474 | d_v += self.viscosity_force(p_i, p_j, r, r_mod)
475 |
476 | # Add body force
477 | if self.is_fluid(p_i) == 1:
478 | val = [0.0 for _ in range(self.dim - 1)]
479 | val.extend([self.g])
480 | d_v += ti.Vector(val)
481 | self.d_velocity[p_i] = d_v
482 | # Initialize the pressure
483 | self.particle_pressure[p_i][0] = 0.0
484 | self.particle_pressure_acc[p_i] = ti.Vector(
485 | [0.0 for _ in range(self.dim)])
486 |
487 | @ti.kernel
488 | def pci_update_time_step(self):
489 | # Final position and velocity update
490 | for p_i in range(self.particle_num[None]):
491 | if self.is_fluid(p_i) == 1:
492 | self.particle_velocity[p_i] += self.dt[None] * (
493 | self.d_velocity[p_i] + self.particle_pressure_acc[p_i])
494 | self.particle_positions[
495 | p_i] += self.dt[None] * self.particle_velocity[p_i]
496 | # Update density
497 | self.particle_density[p_i][0] += self.dt[None] * self.d_density[p_i][0]
498 |
499 | @ti.kernel
500 | def df_compute_deltas(self):
501 | for p_i in range(self.particle_num[None]):
502 | pos_i = self.particle_positions[p_i]
503 | d_v = ti.Vector([0.0 for _ in range(self.dim)])
504 | for j in range(self.particle_num_neighbors[p_i]):
505 | p_j = self.particle_neighbors[p_i, j]
506 | pos_j = self.particle_positions[p_j]
507 |
508 | # Compute distance and its mod
509 | r = pos_i - pos_j
510 | r_mod = r.norm()
511 |
512 | if r_mod > 1e-4 and self.is_fluid(p_i) == 1:
513 | # Compute Viscosity force contribution
514 | d_v += self.viscosity_force(p_i, p_j, r, r_mod)
515 |
516 | # Add body force
517 | if self.is_fluid(p_i) == 1:
518 | val = [0.0 for _ in range(self.dim - 1)]
519 | val.extend([self.g])
520 | d_v += ti.Vector(val)
521 | self.d_velocity[p_i] = d_v
522 |
523 | @ti.kernel
524 | def df_predict_velocities(self):
525 | for p_i in range(self.particle_num[None]):
526 | if self.is_fluid(p_i) == 1:
527 | self.particle_velocity_new[p_i] = self.particle_velocity[
528 | p_i] + self.dt[None] * self.d_velocity[p_i]
529 |
530 | @ti.kernel
531 | def df_correct_density_predict(self):
532 | for p_i in range(self.particle_num[None]):
533 | pos_i = self.particle_positions[p_i]
534 | d_rho = 0.0
535 | for j in range(self.particle_num_neighbors[p_i]):
536 | p_j = self.particle_neighbors[p_i, j]
537 | pos_j = self.particle_positions[p_j]
538 |
539 | # Compute distance and its mod
540 | r = pos_i - pos_j
541 | r_mod = ti.max(r.norm(), 1e-5)
542 |
543 | # Compute Density change
544 | if self.is_fluid(p_j) == 1:
545 | d_rho += self.m * self.cubic_kernel_derivative(r_mod, self.dh) \
546 | * (self.particle_velocity_new[p_i] - self.particle_velocity_new[p_j]).dot(r / r_mod)
547 | elif self.is_fluid(p_j) == 0:
548 | d_rho += self.m * self.cubic_kernel_derivative(r_mod, self.dh) \
549 | * self.particle_velocity_new[p_i].dot(r / r_mod)
550 |
551 | # Compute the predicted density rho star
552 | self.particle_density_new[p_i][
553 | 0] = self.particle_density[p_i][0] + self.dt[None] * d_rho
554 |
555 | # Only consider compressed
556 | err = ti.max(0.0, self.particle_density_new[p_i][0] - self.rho_0)
557 | self.particle_stiff[p_i][0] = err * self.particle_alpha[p_i][0]
558 |
559 | # Compute the density error sum for average use
560 | self.sum_rho_err[None] += err
561 |
562 | @ti.kernel
563 | def df_correct_density_adapt_vel(self):
564 | for p_i in range(self.particle_num[None]):
565 | pos_i = self.particle_positions[p_i]
566 | d_v = ti.Vector([0.0 for _ in range(self.dim)])
567 | for j in range(self.particle_num_neighbors[p_i]):
568 | p_j = self.particle_neighbors[p_i, j]
569 | pos_j = self.particle_positions[p_j]
570 | # Compute distance and its mod
571 | r = pos_i - pos_j
572 | r_mod = r.norm()
573 |
574 | if r_mod > 1e-4:
575 | if self.is_fluid(p_j) == 1:
576 | d_v += self.m * (self.particle_stiff[p_i][0] +
577 | self.particle_stiff[p_j][0]
578 | ) * self.cubic_kernel_derivative(
579 | r_mod, self.dh) * r / r_mod
580 | elif self.is_fluid(p_j) == 0:
581 | d_v += self.m * self.particle_stiff[
582 | p_i][0] * self.cubic_kernel_derivative(
583 | r_mod, self.dh) * r / r_mod
584 |
585 | # Predict velocity using pressure contribution
586 | self.particle_velocity_new[p_i] += d_v / ti.max(self.dt[None], 1e-5)
587 | # Store the pressure contribution to acceleration
588 | self.particle_pressure_acc[p_i] = d_v / ti.max(
589 | self.dt[None] * self.dt[None], 1e-8)
590 |
591 | @ti.kernel
592 | def df_update_positions(self):
593 | for p_i in range(self.particle_num[None]):
594 | # Update the positions
595 | if self.is_fluid(p_i) == 1:
596 | self.particle_positions[
597 | p_i] += self.dt[None] * self.particle_velocity_new[p_i]
598 |
599 | @ti.kernel
600 | def df_compute_density_alpha(self):
601 | for p_i in range(self.particle_num[None]):
602 | pos_i = self.particle_positions[p_i]
603 | grad_sum = ti.Vector([0.0 for _ in range(self.dim)])
604 | grad_square_sum = 0.0
605 | curr_rho = 0.0
606 | for j in range(self.particle_num_neighbors[p_i]):
607 | p_j = self.particle_neighbors[p_i, j]
608 | pos_j = self.particle_positions[p_j]
609 | # Compute distance and its mod
610 | r = pos_i - pos_j
611 | r_mod = r.norm()
612 | if r_mod > 1e-4:
613 | # Compute the grad sum and grad square sum for denominator alpha
614 | grad_val = self.m * self.cubic_kernel_derivative(
615 | r_mod, self.dh) * r / r_mod
616 | grad_sum += grad_val
617 |
618 | if self.is_fluid(p_j) == 1:
619 | grad_square_sum += grad_val.dot(grad_val)
620 | # Compute the density
621 | curr_rho += self.m * self.cubic_kernel(r_mod, self.dh)
622 | # Update the density
623 | self.particle_density[p_i][0] = curr_rho
624 | # Set a threshold of 10^-6 to avoid instability
625 | self.particle_alpha[p_i][0] = -1.0 / ti.max(
626 | grad_sum.dot(grad_sum) + grad_square_sum, 1e-6)
627 |
628 | @ti.kernel
629 | def df_correct_divergence_compute_drho(self):
630 | for p_i in range(self.particle_num[None]):
631 | pos_i = self.particle_positions[p_i]
632 | d_rho = 0.0
633 | for j in range(self.particle_num_neighbors[p_i]):
634 | p_j = self.particle_neighbors[p_i, j]
635 | pos_j = self.particle_positions[p_j]
636 | # Compute distance and its mod
637 | r = pos_i - pos_j
638 | r_mod = r.norm()
639 |
640 | if r_mod > 1e-4:
641 | if self.is_fluid(p_j) == 1:
642 | d_rho += self.m * (
643 | self.particle_velocity_new[p_i] -
644 | self.particle_velocity_new[p_j]).dot(
645 | r / r_mod) * self.cubic_kernel_derivative(
646 | r_mod, self.dh)
647 | # Boundary particles have no contributions to pressure force
648 | elif self.is_fluid(p_j) == 0:
649 | d_rho += self.m * self.particle_velocity_new[p_i].dot(
650 | r / r_mod) * self.cubic_kernel_derivative(
651 | r_mod, self.dh)
652 |
653 | self.d_density[p_i][0] = ti.max(d_rho, 0.0)
654 |
655 | # if the density is less than the rest density, skip update in this iteration
656 | if self.particle_density[p_i][0] + self.dt[None] * self.d_density[p_i][
657 | 0] < self.rho_0 and self.particle_density[p_i][
658 | 0] < self.rho_0:
659 | self.d_density[p_i][0] = 0.0
660 | self.particle_stiff[p_i][
661 | 0] = self.d_density[p_i][0] * self.particle_alpha[p_i][0]
662 |
663 | # Compute the predicted density rho star
664 | self.sum_drho[None] += self.d_density[p_i][0]
665 |
666 | @ti.kernel
667 | def df_correct_divergence_adapt_vel(self):
668 | for p_i in range(self.particle_num[None]):
669 | pos_i = self.particle_positions[p_i]
670 | d_v = ti.Vector([0.0 for _ in range(self.dim)])
671 |
672 | for j in range(self.particle_num_neighbors[p_i]):
673 | p_j = self.particle_neighbors[p_i, j]
674 | pos_j = self.particle_positions[p_j]
675 | # Compute distance and its mod
676 | r = pos_i - pos_j
677 | r_mod = r.norm()
678 |
679 | if r_mod > 1e-5:
680 | if self.is_fluid(p_j) == 1:
681 | d_v += self.m * (self.particle_stiff[p_i][0] +
682 | self.particle_stiff[p_j][0]
683 | ) * self.cubic_kernel_derivative(
684 | r_mod, self.dh) * r / r_mod
685 | elif self.is_fluid(p_j) == 0:
686 | d_v += self.m * self.particle_stiff[
687 | p_i][0] * self.cubic_kernel_derivative(
688 | r_mod, self.dh) * r / r_mod
689 |
690 | # Predict velocity using pressure contribution, dt has been cancelled
691 | self.particle_velocity_new[p_i] += d_v
692 | # Store the pressure contribution to acceleration
693 | self.particle_pressure_acc[p_i] = d_v / self.dt[None]
694 |
695 | @ti.kernel
696 | def df_update_velocities(self):
697 | for p_i in range(self.particle_num[None]):
698 | if self.is_fluid(p_i) == 1:
699 | # Update the velocities from prediction values to next step
700 | self.particle_velocity[p_i] = self.particle_velocity_new[p_i]
701 |
702 | def sim_info(self, output=False):
703 | print("Time step: ", self.dt[None])
704 | print(
705 | "Domain: (%s, %s, %s, %s)" %
706 | (self.x_min, self.x_max, self.y_min, self.y_max), )
707 | print("Fluid area: (%s, %s, %s, %s)" %
708 | (self.left_bound, self.right_bound, self.bottom_bound,
709 | self.top_bound))
710 | print("Grid: ", self.grid_pos)
711 |
712 | def sim_info_realtime(self, frame, t, curr_start, curr_end, total_start):
713 | print(
714 | "Step: %d, physics time: %s, progress: %s %%, time used: %s, total time used: %s"
715 | % (frame, t,
716 | 100 * np.max([t / self.max_time, frame / self.max_steps]),
717 | curr_end - curr_start, curr_end - total_start))
718 | print(
719 | "Max velocity: %s, Max acceleration: %s, Max density: %s, Max pressure: %s"
720 | % (self.max_v, self.max_a, self.max_rho, self.max_pressure))
721 | if self.method == SPHSolver.methods['PCISPH']:
722 | print("Max iter: %d, Max density variation: %s" %
723 | (self.max_it, self.max_rho_err[None][0]))
724 | if self.method == SPHSolver.methods['DFSPH']:
725 | print("Max iter: %d, Max density variation: %s" %
726 | (self.it, self.sum_rho_err[None] / self.particle_num[None]))
727 | print("Max iter: %d, Max divergence variation: %s" %
728 | (self.it, self.sum_drho[None] / self.particle_num[None]))
729 | print("Adaptive time step: ", self.dt[None])
730 |
731 | def adaptive_step(self):
732 | total_num = self.particle_num[None]
733 | self.max_v = np.max(
734 | np.linalg.norm(self.particle_velocity.to_numpy()[:total_num],
735 | 2,
736 | axis=1))
737 | # CFL analysis, constrained by v_max
738 | dt_cfl = self.CFL_v * self.dh / self.max_v
739 |
740 | self.max_a = np.max(
741 | np.linalg.norm((self.d_velocity.to_numpy() +
742 | self.particle_pressure_acc.to_numpy())[:total_num],
743 | 2,
744 | axis=1))
745 | # Constrained by a_max
746 | dt_f = self.CFL_a * np.sqrt(self.dh / self.max_a)
747 |
748 | if self.adaptive_time_step and self.method == SPHSolver.method_DFSPH:
749 | self.dt[None] = np.min([dt_cfl, dt_f])
750 | return
751 |
752 | self.max_rho = np.max(self.particle_density.to_numpy()[:total_num])
753 | self.max_pressure = np.max(
754 | self.particle_pressure.to_numpy()[:total_num])
755 | dt_a = 0.20 * self.dh / (self.c_0 * np.sqrt(
756 | (self.max_rho / self.rho_0)**self.gamma))
757 |
758 | if self.adaptive_time_step and self.method == SPHSolver.method_WCSPH:
759 | self.dt[None] = np.min([dt_cfl, dt_f, dt_a])
760 | if self.adaptive_time_step and self.method == SPHSolver.method_PCISPH:
761 | self.dt[None] = np.min([dt_cfl, dt_f])
762 |
763 | def step(self, frame, t, total_start):
764 | curr_start = time.process_time()
765 |
766 | self.grid_num_particles.fill(0)
767 | self.particle_neighbors.fill(-1)
768 | self.allocate_particles()
769 | self.search_neighbors()
770 |
771 | if self.method == SPHSolver.methods['WCSPH']:
772 | # Compute deltas
773 | self.wc_compute_deltas()
774 | # timestep Update
775 | self.wc_update_time_step()
776 | elif self.method == SPHSolver.methods['PCISPH']:
777 | # Compute viscosity and gravity force
778 | self.pci_compute_deltas()
779 | # Compute the scaling factor for pressure update
780 | self.pci_scaling_factor()
781 | # Start density prediction-correction iteration process
782 | self.it = 0
783 | self.max_it = 0
784 | # 1% rho_0
785 | while self.max_rho_err[None][
786 | 0] >= 0.01 * self.rho_0 or self.it < self.sub_max_iteration:
787 | self.pci_pc_iteration()
788 | self.it += 1
789 | self.max_it += 1
790 | self.max_it = max(self.it, self.max_it)
791 | if self.it > 1000:
792 | print(
793 | "Warning: PCISPH density does not converge, iterated %d steps"
794 | % self.it)
795 | break
796 | # Compute new velocity, position and density
797 | self.pci_update_time_step()
798 | elif self.method == SPHSolver.methods['DFSPH']:
799 | # The step order changed a bit to fit the step function
800 | # update rho and alpha
801 | self.df_compute_density_alpha()
802 | # Correct divergence error
803 | self.it = 0
804 | self.sum_drho[None] = 0.0
805 | # 1% rho_0
806 | while self.sum_drho[None] >= 0.01 * self.particle_num[
807 | None] * self.rho_0 or self.it < 1:
808 | self.sum_drho[None] = 0.0
809 | self.df_correct_divergence_compute_drho()
810 | self.df_correct_divergence_adapt_vel()
811 | self.it += 1
812 | if self.it > 1000:
813 | print(
814 | "Warning: DFSPH divergence does not converge, iterated %d steps"
815 | % self.it)
816 | break
817 | # Update velocities v
818 | self.df_update_velocities()
819 | # Compute non-pressure forces
820 | self.df_compute_deltas()
821 | # Adapt time step
822 | self.adaptive_step()
823 | # Predict velocities v_star
824 | self.df_predict_velocities()
825 | # Correct density error
826 | self.it = 0
827 | self.sum_rho_err[None] = 0.0
828 | # 1% rho_0
829 | while self.sum_rho_err[None] >= 0.01 * self.particle_num[
830 | None] * self.rho_0 or self.it < 2:
831 | self.sum_rho_err[None] = 0.0
832 | self.df_correct_density_predict()
833 | self.df_correct_density_adapt_vel()
834 | self.it += 1
835 | if self.it > 1000:
836 | print(
837 | "Warning: DFSPH density does not converge, iterated %d steps"
838 | % self.it)
839 | break
840 | self.df_update_positions()
841 |
842 | # Handle potential leak particles
843 | self.enforce_boundary()
844 | if self.method != SPHSolver.methods['DFSPH']:
845 | self.adaptive_step()
846 |
847 | curr_end = time.process_time()
848 |
849 | if frame % 10 == 0:
850 | self.sim_info_realtime(frame, t, curr_start, curr_end, total_start)
851 | return self.dt[None]
852 |
853 | @ti.func
854 | def fill_particle(self, i, x, material, color, velocity, pressure,
855 | density):
856 | self.particle_positions[i] = x
857 | self.particle_positions_new[i] = x
858 | self.particle_velocity[i] = velocity
859 | self.particle_velocity_new[i] = velocity
860 | self.d_velocity[i] = ti.Vector([0.0 for _ in range(self.dim)])
861 | self.particle_pressure[i] = pressure
862 | self.particle_pressure_acc[i] = ti.Vector([0.0 for _ in range(self.dim)])
863 | self.particle_density[i] = density
864 | self.particle_density_new[i] = density
865 | self.d_density[i][0] = 0.0
866 | self.particle_alpha[i][0] = 0.0
867 | self.particle_stiff[i][0] = 0.0
868 | self.color[i] = color
869 | self.material[i] = material
870 |
871 | @ti.kernel
872 | def fill(self, new_particles: ti.i32, new_positions: ti.types.ndarray(),
873 | new_material: ti.i32, color: ti.i32):
874 | for i in range(self.particle_num[None],
875 | self.particle_num[None] + new_particles):
876 | self.material[i] = new_material
877 | x = ti.Vector.zero(ti.f32, self.dim)
878 | for k in ti.static(range(self.dim)):
879 | x[k] = new_positions[k, i - self.particle_num[None]]
880 | self.fill_particle(i, x, new_material, color,
881 | self.source_velocity[None],
882 | self.source_pressure[None],
883 | self.source_density[None])
884 |
885 | def set_source_velocity(self, velocity):
886 | if velocity is not None:
887 | velocity = list(velocity)
888 | assert len(velocity) == self.dim
889 | self.source_velocity[None] = velocity
890 | else:
891 | for i in range(self.dim):
892 | self.source_velocity[None][i] = 0
893 |
894 | def set_source_pressure(self, pressure):
895 | if pressure is not None:
896 | self.source_pressure[None] = pressure
897 | else:
898 | self.source_pressure[None][0] = 0.0
899 |
900 | def set_source_density(self, density):
901 | if density is not None:
902 | self.source_density[None] = density
903 | else:
904 | self.source_density[None][0] = 0.0
905 |
906 | def add_cube(self,
907 | lower_corner,
908 | cube_size,
909 | material,
910 | color=0xFFFFFF,
911 | density=None,
912 | pressure=None,
913 | velocity=None):
914 |
915 | num_dim = []
916 | for i in range(self.dim):
917 | num_dim.append(
918 | np.arange(lower_corner[i], lower_corner[i] + cube_size[i],
919 | self.dx))
920 | num_new_particles = reduce(lambda x, y: x * y,
921 | [len(n) for n in num_dim])
922 | assert self.particle_num[
923 | None] + num_new_particles <= self.max_num_particles
924 |
925 | new_positions = np.array(np.meshgrid(*num_dim,
926 | sparse=False,
927 | indexing='ij'),
928 | dtype=np.float32)
929 | new_positions = new_positions.reshape(
930 | -1, reduce(lambda x, y: x * y, list(new_positions.shape[1:])))
931 | print(new_positions.shape)
932 |
933 | for i in range(self.dim):
934 | self.source_bound[0][i] = lower_corner[i]
935 | self.source_bound[1][i] = cube_size[i]
936 |
937 | self.set_source_velocity(velocity=velocity)
938 | self.set_source_pressure(pressure=pressure)
939 | self.set_source_density(density=density)
940 |
941 | self.fill(num_new_particles, new_positions, material, color)
942 | # Add to current particles count
943 | self.particle_num[None] += num_new_particles
944 |
945 | @ti.kernel
946 | def copy_dynamic_nd(self, np_x: ti.types.ndarray(), input_x: ti.template()):
947 | for i in range(self.particle_num[None]):
948 | for j in ti.static(range(self.dim)):
949 | np_x[i, j] = input_x[i][j]
950 |
951 | @ti.kernel
952 | def copy_dynamic(self, np_x: ti.types.ndarray(), input_x: ti.template()):
953 | for i in range(self.particle_num[None]):
954 | np_x[i] = input_x[i]
955 |
956 | def particle_info(self):
957 | np_x = np.ndarray((self.particle_num[None], self.dim),
958 | dtype=np.float32)
959 | self.copy_dynamic_nd(np_x, self.particle_positions)
960 | np_v = np.ndarray((self.particle_num[None], self.dim),
961 | dtype=np.float32)
962 | self.copy_dynamic_nd(np_v, self.particle_velocity)
963 | np_material = np.ndarray((self.particle_num[None], ), dtype=np.int32)
964 | self.copy_dynamic(np_material, self.material)
965 | np_color = np.ndarray((self.particle_num[None], ), dtype=np.int32)
966 | self.copy_dynamic(np_color, self.color)
967 | return {
968 | 'position': np_x,
969 | 'velocity': np_v,
970 | 'material': np_material,
971 | 'color': np_color
972 | }
973 |
--------------------------------------------------------------------------------
/legacy/img/DFSPH.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/legacy/img/DFSPH.gif
--------------------------------------------------------------------------------
/legacy/img/PCISPH.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/legacy/img/PCISPH.gif
--------------------------------------------------------------------------------
/legacy/img/WCSPH.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/legacy/img/WCSPH.gif
--------------------------------------------------------------------------------
/legacy/img/sph_hv.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/legacy/img/sph_hv.gif
--------------------------------------------------------------------------------
/legacy/img/wcsph_alpha030.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erizmr/SPH_Taichi/b02e13a7ce4c4abda144d6d3aaf2a7697f0f72e2/legacy/img/wcsph_alpha030.gif
--------------------------------------------------------------------------------
/legacy/scene.py:
--------------------------------------------------------------------------------
1 | # SPH taichi implementation by mzhang
2 | import taichi as ti
3 | from engine.sph_solver import *
4 | import argparse
5 |
6 | # Default run on CPU
7 | # cuda performance has not been tested
8 | ti.init(arch=ti.cpu)
9 |
10 |
11 | def main(opt):
12 | dynamic_allocate = opt.dynamic_allocate
13 | save_frames = opt.save
14 | adaptive_time_step = opt.adaptive
15 | method_name = opt.method
16 | sim_physical_time = 5.0
17 | max_frame = 50000
18 |
19 | res = (400, 400)
20 | screen_to_world_ratio = 35
21 | dx = 0.1
22 | u, b, l, r = np.array([res[1], 0, 0, res[0]]) / screen_to_world_ratio
23 |
24 | gui = ti.GUI('SPH', res, background_color=0x112F41)
25 | sph = SPHSolver(res,
26 | screen_to_world_ratio, [u, b, l, r],
27 | alpha=0.30,
28 | dx=dx,
29 | max_time=sim_physical_time,
30 | max_steps=max_frame,
31 | dynamic_allocate=dynamic_allocate,
32 | adaptive_time_step=adaptive_time_step,
33 | method=SPHSolver.methods[method_name])
34 |
35 | print("Method use: %s" % method_name)
36 | # Add fluid particles
37 | sph.add_cube(lower_corner=[res[0] / 2 / screen_to_world_ratio - 3, 4 * dx],
38 | cube_size=[6, 6],
39 | velocity=[0.0, -5.0],
40 | density=[1000],
41 | color=0x068587,
42 | material=SPHSolver.material_fluid)
43 |
44 | colors = np.array([0xED553B, 0x068587, 0xEEEEF0, 0xFFFF00],
45 | dtype=np.uint32)
46 | add_cnt = 0.0
47 | add = True
48 | save_cnt = 0.0
49 | output_fps = 60
50 | save_point = 1.0 / output_fps
51 | t = 0.0
52 | frame = 0
53 | total_start = time.process_time()
54 | while frame < max_frame and t < sim_physical_time:
55 | dt = sph.step(frame, t, total_start)
56 | particles = sph.particle_info()
57 |
58 | # if frame == 1000:
59 | if add and add_cnt > 0.40:
60 | sph.add_cube(lower_corner=[6, 6],
61 | cube_size=[2.0, 2.0],
62 | velocity=[0.0, -5.0],
63 | density=[1000.0],
64 | color=0xED553B,
65 | material=SPHSolver.material_fluid)
66 |
67 | # if frame == 1000:
68 | if add and add_cnt > 0.40:
69 | sph.add_cube(lower_corner=[3, 8],
70 | cube_size=[1.0, 1.0],
71 | velocity=[0.0, -10.0],
72 | density=[1000.0],
73 | color=0xEEEEF0,
74 | material=SPHSolver.material_fluid)
75 | add = False
76 |
77 | for pos in particles['position']:
78 | for j in range(len(res)):
79 | pos[j] *= screen_to_world_ratio / res[j]
80 |
81 | gui.circles(particles['position'],
82 | radius=1.5,
83 | color=particles['color'])
84 |
85 | # Save in fixed frame interval, for fixed time step
86 | if not adaptive_time_step:
87 | if frame % 50 == 0:
88 | gui.show(f'{frame:06d}.png' if save_frames else None)
89 | else:
90 | gui.show()
91 |
92 | # Save in fixed frame per second, for adaptive time step
93 | if adaptive_time_step:
94 | if save_cnt >= save_point:
95 | gui.show(f'{frame:06d}.png' if save_frames else None)
96 | else:
97 | gui.show()
98 | save_cnt = 0.0
99 |
100 | frame += 1
101 | t += dt
102 | save_cnt += dt
103 | add_cnt += dt
104 |
105 | print('done')
106 |
107 |
108 | if __name__ == '__main__':
109 | parser = argparse.ArgumentParser()
110 | parser.add_argument("--method",
111 | type=str,
112 | default="PCISPH",
113 | help="SPH methods: WCSPH, PCISPH, DFSPH")
114 | parser.add_argument("--save",
115 | action='store_true',
116 | help="save frames")
117 | parser.add_argument("--adaptive",
118 | action='store_true',
119 | help="whether apply adaptive step size")
120 | parser.add_argument("--dynamic-allocate",
121 | action='store_true',
122 | help="whether apply dynamic allocation")
123 | opt = parser.parse_args()
124 | main(opt)
125 |
--------------------------------------------------------------------------------
/legacy/test_sample.py:
--------------------------------------------------------------------------------
1 | # WCSPH implementation by mzhang
2 | import numpy as np
3 | import matplotlib.pyplot as plt
4 | import time
5 | from itertools import count
6 | import taichi as ti
7 |
8 | ti.init(arch=ti.cpu)
9 |
10 | df_fac = 1.3
11 | dx = 0.2
12 | dh = dx * df_fac
13 |
14 | ###### Scene parameters ########
15 | w = 20
16 | h = 10
17 | w_bound = 22
18 | h_bound = 12
19 |
20 | bottom_bound = 0.0
21 | top_bound = 0.0
22 | left_bound = 0.0
23 | right_bound = 0.0
24 |
25 | assert w_bound > w
26 | assert h_bound > h
27 | x_min = (w_bound - w) / 2.0
28 | y_min = (h_bound - h) / 2.0
29 | x_max = w_bound - (w_bound - w) / 2.0
30 | y_max = h_bound - (h_bound - h) / 2.0
31 |
32 | screen_res = (800, 400)
33 | screen_to_world_ratio = 35.0
34 | bg_color = 0x112f41
35 | particle_color = 0x068587
36 | boundary_color = 0xebaca2
37 | particle_radius = 3.0
38 | particle_radius_in_world = particle_radius / screen_to_world_ratio
39 |
40 |
41 | def setup():
42 | def computeGridIndex(x, y):
43 | idx = np.floor(x / (2 * dh)).astype(int)
44 | idy = np.floor(y / (2 * dh)).astype(int)
45 | return idx, idy
46 |
47 | def placeParticles(position_list, paticle_list, wall_mark, bound=0):
48 | # position_list: [start_x, start_y, end_x, end_y]
49 | start_x, start_y, end_x, end_y = position_list
50 | vel_x, vel_y, p, rho = 0.0, 0.0, 0.0, 1000.0
51 | for pos_x in np.arange(start_x, end_x, dx, dtype=np.float32):
52 | for pos_y in np.arange(start_y, end_y, dx, dtype=np.float32):
53 | paticle_list.append([pos_x, pos_y])
54 | if bound:
55 | wall_mark.append(0)
56 | else:
57 | wall_mark.append(1)
58 | particle_list = []
59 | wall_mark = []
60 |
61 | #### Dam break #######
62 | start_x = x_min + 0.5 * dx
63 | start_y = y_min - 1.0 * dx
64 | end_x = start_x + 0.5 * h
65 | end_y = start_y + 0.6 * h
66 |
67 | ## Constrcut wall
68 | # Bottom square
69 | b_start_x = 0.0
70 | b_start_y = 0.0
71 | b_end_x = b_start_x + w
72 | b_end_y = b_start_y + y_min - 2 * dx
73 |
74 | bottom_bound = b_end_y
75 |
76 | # Top square
77 | t_start_x = 0.0
78 | t_start_y = h - y_min + 2 * dx
79 | t_end_x = t_start_x + w
80 | t_end_y = h
81 |
82 | top_bound = t_start_y
83 |
84 | # left square
85 | l_start_x = 0.0
86 | l_start_y = y_min - 2 * dx
87 | l_end_x = l_start_x + x_min - 2 * dx
88 | l_end_y = l_start_y + h - 2 * y_min + 4 * dx
89 |
90 | left_bound = l_end_x
91 |
92 | # right square
93 | r_start_x = w - x_min + 2 * dx
94 | r_start_y = y_min - 2 * dx
95 | r_end_x = w - dx
96 | r_end_y = r_start_y + h - 2 * y_min + 4 * dx
97 |
98 | right_bound = r_start_x
99 |
100 | pos_list_fluid = [start_x, start_y, end_x, end_y]
101 | placeParticles(pos_list_fluid, particle_list, wall_mark)
102 |
103 | # These are boundaries
104 | pos_list_bs = [b_start_x, b_start_y, b_end_x, b_end_y]
105 | placeParticles(pos_list_bs, particle_list, wall_mark, bound=1)
106 |
107 | pos_list_ts = [t_start_x, t_start_y, t_end_x, t_end_y]
108 | placeParticles(pos_list_ts, particle_list, wall_mark, bound=1)
109 |
110 | pos_list_ls = [l_start_x, l_start_y, l_end_x, l_end_y]
111 | placeParticles(pos_list_ls, particle_list, wall_mark, bound=1)
112 |
113 | pos_list_rs = [r_start_x, r_start_y, r_end_x, r_end_y]
114 | placeParticles(pos_list_rs, particle_list, wall_mark, bound=1)
115 |
116 | return particle_list,wall_mark, top_bound, bottom_bound, left_bound, right_bound
117 |
118 | def makeGrid():
119 | grid_size = 2*dh
120 | num_x = np.ceil(w_bound / grid_size).astype(int)
121 | num_y = np.ceil(h_bound / grid_size).astype(int)
122 |
123 | grid_x = num_x
124 | grid_y = num_y
125 | return grid_x, grid_y
126 |
127 | @ti.data_oriented
128 | class sph_solver:
129 | def __init__(self, particle_list,
130 | wall_mark,
131 | grid,
132 | bound, alpha=0.5, dx=0.2, max_time=10000, max_steps=1000,gui=None):
133 | ######## Solver parameters ##########
134 | self.max_time = max_time
135 | self.max_steps = max_steps
136 | self.gui = gui
137 | # Gravity
138 | self.g = -9.80
139 | # viscosity
140 | self.alpha = alpha
141 | # reference density
142 | self.rho_0 = 1000.0
143 | # CFL coefficient
144 | self.CFL = 0.20
145 |
146 | # Smooth kernel norm factor
147 | self.kernel_norm = 1.0
148 |
149 | # Pressure state function parameters
150 | self.gamma = 7.0
151 | self.c_0 = 20.0
152 |
153 | ###### Scene parameters ########
154 | self.w = 20
155 | self.h = 10
156 | self.w_bound = 22
157 | self.h_bound = 12
158 |
159 | assert self.w_bound > self.w
160 | assert self.h_bound > self.h
161 | self.x_min = (self.w_bound - self.w) / 2.0
162 | self.y_min = (self.h_bound - self.h) / 2.0
163 | self.x_max = self.w_bound - (self.w_bound - self.w) / 2.0
164 | self.y_max = self.h_bound - (self.h_bound - self.h) / 2.0
165 |
166 | self.top_bound = bound[0] # top_bound
167 | self.bottom_bound = bound[1] #bottom_bound
168 | self.left_bound = bound[2] #left_bound
169 | self.right_bound = bound[3] #right_bound
170 |
171 | self.df_fac = 1.3
172 | self.dx = 0.2
173 | self.dh = self.dx * self.df_fac
174 | self.kernel_norm = 10. / (7. * np.pi * self.dh ** 2)
175 |
176 | ###### Particles #######
177 | self.dim = 2
178 | self.particle_numbers = len(particle_list)
179 |
180 | self.grid_x = grid[0]
181 | self.grid_y = grid[1]
182 |
183 | # Fluid particles
184 | self.old_positions = ti.Vector(self.dim, dt=ti.f32)
185 | self.particle_positions = ti.Vector(self.dim, dt=ti.f32)
186 | self.particle_velocity = ti.Vector(self.dim, dt=ti.f32)
187 | self.particle_pressure = ti.Vector(1, dt=ti.f32)
188 | self.particle_density = ti.Vector(1, dt=ti.f32)
189 | self.wall_mark_list = ti.Vector(1, dt=ti.f32)
190 |
191 | self.d_velocity = ti.Vector(self.dim, dt=ti.f32)
192 | self.d_density = ti.Vector(1, dt=ti.f32)
193 |
194 | self.dx = dx
195 | self.m = self.dx**2 * 1000.0
196 |
197 | self.particle_list = np.array(particle_list, dtype=np.float32)
198 | self.wall_mark = np.array(wall_mark, dtype=np.int32)
199 |
200 | self.grid_num_particles = ti.var(ti.i32)
201 | self.grid2particles = ti.var(ti.i32)
202 | self.particle_num_neighbors = ti.var(ti.i32)
203 | self.particle_neighbors = ti.var(ti.i32)
204 |
205 | self.max_num_particles_per_cell = 100
206 | self.max_num_neighbors = 100
207 |
208 | ti.root.dense(ti.i, self.particle_numbers).place(self.old_positions, self.particle_positions,
209 | self.particle_velocity, self.particle_pressure,
210 | self.particle_density, self.d_velocity, self.d_density,
211 | self.wall_mark_list)
212 |
213 | grid_snode = ti.root.dense(ti.ij, (self.grid_x, self.grid_y))
214 | grid_snode.place(self.grid_num_particles)
215 | grid_snode.dense(ti.k, self.max_num_particles_per_cell).place(self.grid2particles)
216 |
217 | nb_node = ti.root.dense(ti.i, self.particle_numbers)
218 | nb_node.place(self.particle_num_neighbors)
219 | nb_node.dense(ti.j, self.max_num_neighbors).place(self.particle_neighbors)
220 |
221 | @ti.kernel
222 | def init(self, p_list:ti.types.ndarray(), w_list:ti.types.ndarray()):
223 | for i in range(self.particle_numbers):
224 | for j in ti.static(range(self.dim)):
225 | self.particle_positions[i][j] = p_list[i,j]
226 | self.particle_velocity[i][j] = ti.cast(0.0, ti.f32)
227 | self.d_velocity[i][0] = ti.cast(0.0, ti.f32)
228 | self.d_velocity[i][1] = ti.cast(-9.8, ti.f32)
229 |
230 | self.wall_mark_list[i][0] = w_list[i]
231 | self.d_density[i][0] = ti.cast(0.0, ti.f32)
232 | self.particle_pressure[i][0] = ti.cast(0.0, ti.f32)
233 | self.particle_density[i][0] = ti.cast(1000.0, ti.f32)
234 |
235 | @ti.func
236 | def computeGridIndex(self, pos):
237 | return (pos / (2 * dh)).cast(int)
238 |
239 | @ti.kernel
240 | def allocateParticles(self):
241 | # Ref to pbf2d example from by Ye Kuang (k-ye)
242 | # https://github.com/taichi-dev/taichi/blob/master/examples/pbf2d.py
243 | # allocate particles to grid
244 | for p_i in self.particle_positions:
245 | # Compute the grid index on the fly
246 | cell = self.computeGridIndex(self.particle_positions[p_i])
247 | offs = self.grid_num_particles[cell].atomic_add(1)
248 | self.grid2particles[cell, offs] = p_i
249 |
250 | @ti.func
251 | def is_in_grid(self, c):
252 | # Ref to pbf2d example from by Ye Kuang (k-ye)
253 | # https://github.com/taichi-dev/taichi/blob/master/examples/pbf2d.py
254 | return 0 <= c[0] and c[0] < self.grid_x and 0 <= c[1] and c[1] < self.grid_y
255 |
256 | @ti.func
257 | def isFluid(self, p):
258 | # check fluid particle or bound particle
259 | return self.wall_mark_list[p][0]
260 |
261 | @ti.kernel
262 | def search_neighbors(self):
263 | # Ref to pbf2d example from by Ye Kuang (k-ye)
264 | # https://github.com/taichi-dev/taichi/blob/master/examples/pbf2d.py
265 | for p_i in self.particle_positions:
266 | pos_i = self.particle_positions[p_i]
267 | nb_i = 0
268 | if self.isFluid(p_i) == 1:
269 | # Compute the grid index on the fly
270 | cell = self.computeGridIndex(self.particle_positions[p_i])
271 | for offs in ti.static(ti.grouped(ti.ndrange((-1, 2), (-1, 2)))):
272 | cell_to_check = cell + offs
273 | if self.is_in_grid(cell_to_check):
274 | for j in range(self.grid_num_particles[cell_to_check]):
275 | p_j = self.grid2particles[cell_to_check, j]
276 | if nb_i < self.max_num_neighbors and p_j != p_i and (
277 | pos_i - self.particle_positions[p_j]).norm() < self.dh * 2.00:
278 | self.particle_neighbors[p_i, nb_i] = p_j
279 | nb_i += 1
280 | self.particle_num_neighbors[p_i] = nb_i
281 |
282 | @ti.func
283 | def cubicKernel(self, r, h):
284 | # value of cubic spline smoothing kernel
285 | k = 10. / (7. * np.pi * h ** 2)
286 | q = r / h
287 | # assert q >= 0.0
288 | res = ti.cast(0.0, ti.f32)
289 | if q <= 1.0:
290 | res = k * (1 - 1.5 * q ** 2 + 0.75 * q ** 3)
291 | elif q < 2.0:
292 | res = k * 0.25 * (2 - q) ** 3
293 | return res
294 |
295 | @ti.func
296 | def cubicKernelDerivative(self, r, h):
297 | # derivative of cubcic spline smoothing kernel
298 | k = 10. / (7. * np.pi * h ** 2)
299 | q = r / h
300 | # assert q > 0.0
301 | res = ti.cast(0.0, ti.f32)
302 | if q < 1.0:
303 | res = (k / h) * (-3 * q + 2.25 * q ** 2)
304 | elif q < 2.0:
305 | res = -0.75 * (k / h) * (2 - q) ** 2
306 | return res
307 |
308 | @ti.func
309 | def rhoDerivative(self, ptc_i, ptc_j, r, r_mod):
310 | # density delta
311 | return self.m * self.cubicKernelDerivative(r_mod, self.dh) \
312 | * (self.particle_velocity[ptc_i] - self.particle_velocity[ptc_j]).dot(r / r_mod)
313 |
314 |
315 | @ti.func
316 | def pUpdate(self, rho, rho_0=1000, gamma=7.0, c_0=20.0):
317 | # Weakly compressible, tait function
318 | b = rho_0 * c_0 ** 2 / gamma
319 | return b * ((rho / rho_0) ** gamma - 1.0)
320 |
321 | @ti.func
322 | def pressureForce(self, ptc_i, ptc_j, r, r_mod, mirror_pressure=0):
323 | # Compute the pressure force contribution, Symmetric Formula
324 | res = ti.Vector([0.0, 0.0])
325 | # Disable the mirror force, use collision instead
326 | # Use pressure mirror method to handle boundary leak
327 | # if mirror_pressure == 1:
328 | # res = - self.m * (self.particle_pressure[ptc_i][0]/ self.particle_density[ptc_i][0] ** 2
329 | # + self.particle_pressure[ptc_i][0]/self.rho_0**2)* self.cubicKernelDerivative(r_mod, h) * r / r_mod
330 | # else:
331 | res = -self.m * (self.particle_pressure[ptc_i][0] / self.particle_density[ptc_i][0] ** 2
332 | + self.particle_pressure[ptc_j][0] / self.particle_density[ptc_j][0] ** 2) \
333 | * self.cubicKernelDerivative(r_mod, self.dh) * r / r_mod
334 | return res
335 |
336 | @ti.func
337 | def viscosityForce(self, ptc_i, ptc_j, r, r_mod):
338 | # Compute the viscosity force contribution, artificial viscosity
339 | res = ti.Vector([0.0, 0.0])
340 | v_xy= (self.particle_velocity[ptc_i]- self.particle_velocity[ptc_j]).dot(r)
341 | if v_xy < 0:
342 | # Artifical viscosity
343 | vmu = -2.0 * self.alpha * self.dx * self.c_0 / (self.particle_density[ptc_i][0] + self.particle_density[ptc_j][0])
344 | res = -self.m * vmu * v_xy/(r_mod**2 + 0.01*self.dx**2)* self.cubicKernelDerivative(r_mod, self.dh) * r / r_mod
345 | return res
346 |
347 | @ti.func
348 | def simualteCollisions(self, ptc_i, vec, d):
349 | # Collision factor, assume roughly 50% velocity loss after collision, i.e. m_f /(m_f + m_b)
350 | c_f = 0.5
351 | self.particle_positions[ptc_i] += vec * d
352 | self.particle_velocity[ptc_i] -= (1.0+c_f) * self.particle_velocity[ptc_i].dot(vec) * vec
353 |
354 | @ti.kernel
355 | def enforceBoundary(self):
356 | for p_i in self.particle_positions:
357 | if self.isFluid(p_i) == 1:
358 | pos = self.particle_positions[p_i]
359 | if pos[0] < self.left_bound:
360 | self.simualteCollisions(p_i, ti.Vector([1.0, 0.0]), self.left_bound - pos[0])
361 | if pos[0] > self.right_bound:
362 | self.simualteCollisions(p_i, ti.Vector([-1.0, 0.0]), pos[0] - self.right_bound)
363 | if pos[1] > self.top_bound:
364 | self.simualteCollisions(p_i, ti.Vector([0.0, -1.0]), pos[1] - self.top_bound)
365 | if pos[1] < self.bottom_bound:
366 | self.simualteCollisions(p_i, ti.Vector([0.0, 1.0]), self.bottom_bound - pos[1])
367 |
368 | @ti.kernel
369 | def computeDeltas(self):
370 | for p_i in self.particle_positions:
371 | pos_i = self.particle_positions[p_i]
372 | d_v = ti.Vector([0.0, 0.0], dt=ti.f32)
373 | d_rho = ti.cast(0.0, ti.f32)
374 | # if self.isFluid(p_i) == 1:
375 | # d_v = ti.Vector([0.0, -9.8])
376 | for j in range(self.particle_num_neighbors[p_i]):
377 | p_j = self.particle_neighbors[p_i, j]
378 | pos_j = self.particle_positions[p_j]
379 |
380 | # Disable mirror force
381 | # mirror_pressure = 0
382 | # if self.isFluid(p_j) == 0:
383 | # mirror_pressure = 1
384 |
385 | # Compute distance and its mod
386 | r = pos_i - pos_j
387 | r_mod = r.norm()
388 |
389 | # Compute Density change
390 | d_rho += self.rhoDerivative(p_i, p_j, r, r_mod)
391 |
392 | if self.isFluid(p_i) == 1:
393 | # Compute Viscosity force contribution
394 | d_v += self.viscosityForce(p_i, p_j, r, r_mod)
395 |
396 | # Compute Pressure force contribution
397 | d_v += self.pressureForce(p_i, p_j, r, r_mod)
398 |
399 | # Add body force
400 | if self.isFluid(p_i) == 1:
401 | d_v += ti.Vector([0.0, self.g], dt=ti.f32)
402 | self.d_velocity[p_i] = d_v
403 | self.d_density[p_i][0] = d_rho
404 |
405 | @ti.kernel
406 | def updateTimeStep(self):
407 | # Simple Forward Euler currently
408 | for p_i in self.particle_positions:
409 | if self.isFluid(p_i) == 1:
410 | self.particle_positions[p_i] += self.dt * self.particle_velocity[p_i]
411 | self.particle_velocity[p_i] += self.dt * self.d_velocity[p_i]
412 | self.particle_density[p_i][0] += self.dt * self.d_density[p_i][0]
413 | self.particle_pressure[p_i][0] = self.pUpdate(self.particle_density[p_i][0], self.rho_0, self.gamma, self.c_0)
414 |
415 | def solve(self, output=False):
416 | # Compute dt, a naive initial test value
417 | self.dt = 0.1 * self.dh / self.c_0
418 | print("Time step: ", self.dt)
419 | print("Domain: (%s, %s, %s, %s)" % (self.x_min, self.x_max, self.y_min, self.y_max), )
420 | print("Fluid area: (%s, %s, %s, %s)"%(self.left_bound, self.right_bound, self.bottom_bound, self.top_bound))
421 | print("Grid: (%d, %d)"%(self.grid_x, self.grid_y))
422 |
423 | step = 1
424 | t = 0.0
425 | total_start = time.process_time()
426 | while t < self.max_time and step < self.max_steps:
427 | curr_start = time.process_time()
428 | self.solveUpdate()
429 | max_v = np.max(np.linalg.norm(self.particle_velocity.to_numpy(),2, axis=1))
430 | max_a = np.max(np.linalg.norm(self.d_velocity.to_numpy(),2, axis=1))
431 | max_rho = np.max(self.particle_density.to_numpy())
432 | max_pressure = np.max(self.particle_pressure.to_numpy())
433 |
434 | curr_end = time.process_time()
435 | t += self.dt
436 | step += 1
437 |
438 | # CFL analysis, adaptive dt
439 | dt_cfl = self.dh / max_v
440 | dt_f = np.sqrt(self.dh / max_a)
441 | dt_a = self.dh / (self.c_0 * np.sqrt((max_rho / self.rho_0)**self.gamma))
442 | self.dt = self.CFL * np.min([dt_cfl, dt_f, dt_a])
443 | if step % 10 == 0:
444 | print("Step: %d, physics time: %s, progress: %s %%, time used: %s, total time used: %s"
445 | % (step, t, 100*np.max([t / self.max_time, step / self.max_steps]), curr_end-curr_start, curr_end-total_start))
446 | print("Max velocity: %s, Max acceleration: %s, Max density: %s, Max pressure: %s" % (max_v, max_a, max_rho, max_pressure))
447 | print("Adaptive time step: ", self.dt)
448 | self.render(step, self.gui, output)
449 | total_end = time.process_time()
450 | print("Total time used: %s " % (total_end - total_start))
451 |
452 | def solveUpdate(self):
453 | self.grid_num_particles.fill(0)
454 | self.particle_neighbors.fill(-1)
455 | self.allocateParticles()
456 | self.search_neighbors()
457 | # Compute deltas
458 | self.computeDeltas()
459 | # timestep Update
460 | self.updateTimeStep()
461 | # Handle potential leak particles
462 | self.enforceBoundary()
463 |
464 | def isFluidNP(self, p):
465 | # ti.func cannot be called in python scope
466 | # for render use
467 | return self.wall_mark[p]
468 |
469 | def render(self, step, gui, output=False):
470 | canvas = gui.canvas
471 | canvas.clear(bg_color)
472 | pos_np = self.particle_positions.to_numpy()
473 | fluid_p = []
474 | wall_p = []
475 | for i, pos in enumerate(pos_np):
476 | if self.isFluidNP(i) == 1:
477 | fluid_p.append(pos)
478 | else:
479 | wall_p.append(pos)
480 | fluid_p = np.array(fluid_p)
481 | wall_p = np.array(wall_p)
482 |
483 | for pos in fluid_p:
484 | for j in range(self.dim):
485 | pos[j] *= screen_to_world_ratio / screen_res[j]
486 |
487 | for pos in wall_p:
488 | for j in range(self.dim):
489 | pos[j] *= screen_to_world_ratio / screen_res[j]
490 |
491 | gui.circles(fluid_p, radius=particle_radius, color=particle_color)
492 | gui.circles(wall_p, radius=particle_radius, color=boundary_color)
493 | if output:
494 | if step%10 == 0:
495 | gui.show(f"{step:04d}.png")
496 | else:
497 | gui.show()
498 |
499 | def main():
500 | OUTPUT = False
501 | gui = ti.GUI('SPH2D', screen_res)
502 | grid_shape = makeGrid()
503 | particle_list, wall_mark, u, b, l, r = setup()
504 | sph = sph_solver(particle_list, wall_mark, grid_shape, [u,b,l,r],alpha=1.0, dx = dx, gui=gui, max_steps=10000)
505 | sph.init(sph.particle_list, sph.wall_mark)
506 | sph.solve(output=OUTPUT)
507 |
508 | print('done')
509 |
510 | if __name__ == '__main__':
511 | main()
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
--------------------------------------------------------------------------------
/particle_system.py:
--------------------------------------------------------------------------------
1 | import taichi as ti
2 | import numpy as np
3 | import trimesh as tm
4 | from functools import reduce
5 | from config_builder import SimConfig
6 | from WCSPH import WCSPHSolver
7 | from DFSPH import DFSPHSolver
8 | from scan_single_buffer import parallel_prefix_sum_inclusive_inplace
9 |
10 | @ti.data_oriented
11 | class ParticleSystem:
12 | def __init__(self, config: SimConfig, GGUI=False):
13 | self.cfg = config
14 | self.GGUI = GGUI
15 |
16 | self.domain_start = np.array([0.0, 0.0, 0.0])
17 | self.domain_start = np.array(self.cfg.get_cfg("domainStart"))
18 |
19 | self.domain_end = np.array([1.0, 1.0, 1.0])
20 | self.domian_end = np.array(self.cfg.get_cfg("domainEnd"))
21 |
22 | self.domain_size = self.domian_end - self.domain_start
23 |
24 | self.dim = len(self.domain_size)
25 | assert self.dim > 1
26 | # Simulation method
27 | self.simulation_method = self.cfg.get_cfg("simulationMethod")
28 |
29 | # Material
30 | self.material_solid = 0
31 | self.material_fluid = 1
32 |
33 | self.particle_radius = 0.01 # particle radius
34 | self.particle_radius = self.cfg.get_cfg("particleRadius")
35 |
36 | self.particle_diameter = 2 * self.particle_radius
37 | self.support_radius = self.particle_radius * 4.0 # support radius
38 | self.m_V0 = 0.8 * self.particle_diameter ** self.dim
39 |
40 | self.particle_num = ti.field(int, shape=())
41 |
42 | # Grid related properties
43 | self.grid_size = self.support_radius
44 | self.grid_num = np.ceil(self.domain_size / self.grid_size).astype(int)
45 | print("grid size: ", self.grid_num)
46 | self.padding = self.grid_size
47 |
48 | # All objects id and its particle num
49 | self.object_collection = dict()
50 | self.object_id_rigid_body = set()
51 |
52 | #========== Compute number of particles ==========#
53 | #### Process Fluid Blocks ####
54 | fluid_blocks = self.cfg.get_fluid_blocks()
55 | fluid_particle_num = 0
56 | for fluid in fluid_blocks:
57 | particle_num = self.compute_cube_particle_num(fluid["start"], fluid["end"])
58 | fluid["particleNum"] = particle_num
59 | self.object_collection[fluid["objectId"]] = fluid
60 | fluid_particle_num += particle_num
61 |
62 | #### Process Rigid Blocks ####
63 | rigid_blocks = self.cfg.get_rigid_blocks()
64 | rigid_particle_num = 0
65 | for rigid in rigid_blocks:
66 | particle_num = self.compute_cube_particle_num(rigid["start"], rigid["end"])
67 | rigid["particleNum"] = particle_num
68 | self.object_collection[rigid["objectId"]] = rigid
69 | rigid_particle_num += particle_num
70 |
71 | #### Process Rigid Bodies ####
72 | rigid_bodies = self.cfg.get_rigid_bodies()
73 | for rigid_body in rigid_bodies:
74 | voxelized_points_np = self.load_rigid_body(rigid_body)
75 | rigid_body["particleNum"] = voxelized_points_np.shape[0]
76 | rigid_body["voxelizedPoints"] = voxelized_points_np
77 | self.object_collection[rigid_body["objectId"]] = rigid_body
78 | rigid_particle_num += voxelized_points_np.shape[0]
79 |
80 | self.fluid_particle_num = fluid_particle_num
81 | self.solid_particle_num = rigid_particle_num
82 | self.particle_max_num = fluid_particle_num + rigid_particle_num
83 | self.num_rigid_bodies = len(rigid_blocks)+len(rigid_bodies)
84 |
85 | #### TODO: Handle the Particle Emitter ####
86 | # self.particle_max_num += emitted particles
87 | print(f"Current particle num: {self.particle_num[None]}, Particle max num: {self.particle_max_num}")
88 |
89 | #========== Allocate memory ==========#
90 | # Rigid body properties
91 | if self.num_rigid_bodies > 0:
92 | # TODO: Here we actually only need to store rigid boides, however the object id of rigid may not start from 0, so allocate center of mass for all objects
93 | self.rigid_rest_cm = ti.Vector.field(self.dim, dtype=float, shape=self.num_rigid_bodies + len(fluid_blocks))
94 |
95 | # Particle num of each grid
96 | self.grid_particles_num = ti.field(int, shape=int(self.grid_num[0]*self.grid_num[1]*self.grid_num[2]))
97 | self.grid_particles_num_temp = ti.field(int, shape=int(self.grid_num[0]*self.grid_num[1]*self.grid_num[2]))
98 |
99 | self.prefix_sum_executor = ti.algorithms.PrefixSumExecutor(self.grid_particles_num.shape[0])
100 |
101 | # Particle related properties
102 | self.object_id = ti.field(dtype=int, shape=self.particle_max_num)
103 | self.x = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
104 | self.x_0 = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
105 | self.v = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
106 | self.acceleration = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
107 | self.m_V = ti.field(dtype=float, shape=self.particle_max_num)
108 | self.m = ti.field(dtype=float, shape=self.particle_max_num)
109 | self.density = ti.field(dtype=float, shape=self.particle_max_num)
110 | self.pressure = ti.field(dtype=float, shape=self.particle_max_num)
111 | self.material = ti.field(dtype=int, shape=self.particle_max_num)
112 | self.color = ti.Vector.field(3, dtype=int, shape=self.particle_max_num)
113 | self.is_dynamic = ti.field(dtype=int, shape=self.particle_max_num)
114 |
115 | if self.cfg.get_cfg("simulationMethod") == 4:
116 | self.dfsph_factor = ti.field(dtype=float, shape=self.particle_max_num)
117 | self.density_adv = ti.field(dtype=float, shape=self.particle_max_num)
118 |
119 | # Buffer for sort
120 | self.object_id_buffer = ti.field(dtype=int, shape=self.particle_max_num)
121 | self.x_buffer = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
122 | self.x_0_buffer = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
123 | self.v_buffer = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
124 | self.acceleration_buffer = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
125 | self.m_V_buffer = ti.field(dtype=float, shape=self.particle_max_num)
126 | self.m_buffer = ti.field(dtype=float, shape=self.particle_max_num)
127 | self.density_buffer = ti.field(dtype=float, shape=self.particle_max_num)
128 | self.pressure_buffer = ti.field(dtype=float, shape=self.particle_max_num)
129 | self.material_buffer = ti.field(dtype=int, shape=self.particle_max_num)
130 | self.color_buffer = ti.Vector.field(3, dtype=int, shape=self.particle_max_num)
131 | self.is_dynamic_buffer = ti.field(dtype=int, shape=self.particle_max_num)
132 |
133 | if self.cfg.get_cfg("simulationMethod") == 4:
134 | self.dfsph_factor_buffer = ti.field(dtype=float, shape=self.particle_max_num)
135 | self.density_adv_buffer = ti.field(dtype=float, shape=self.particle_max_num)
136 |
137 | # Grid id for each particle
138 | self.grid_ids = ti.field(int, shape=self.particle_max_num)
139 | self.grid_ids_buffer = ti.field(int, shape=self.particle_max_num)
140 | self.grid_ids_new = ti.field(int, shape=self.particle_max_num)
141 |
142 | self.x_vis_buffer = None
143 | if self.GGUI:
144 | self.x_vis_buffer = ti.Vector.field(self.dim, dtype=float, shape=self.particle_max_num)
145 | self.color_vis_buffer = ti.Vector.field(3, dtype=float, shape=self.particle_max_num)
146 |
147 |
148 | #========== Initialize particles ==========#
149 |
150 | # Fluid block
151 | for fluid in fluid_blocks:
152 | obj_id = fluid["objectId"]
153 | offset = np.array(fluid["translation"])
154 | start = np.array(fluid["start"]) + offset
155 | end = np.array(fluid["end"]) + offset
156 | scale = np.array(fluid["scale"])
157 | velocity = fluid["velocity"]
158 | density = fluid["density"]
159 | color = fluid["color"]
160 | self.add_cube(object_id=obj_id,
161 | lower_corner=start,
162 | cube_size=(end-start)*scale,
163 | velocity=velocity,
164 | density=density,
165 | is_dynamic=1, # enforce fluid dynamic
166 | color=color,
167 | material=1) # 1 indicates fluid
168 |
169 | # TODO: Handle rigid block
170 | # Rigid block
171 | for rigid in rigid_blocks:
172 | obj_id = rigid["objectId"]
173 | offset = np.array(rigid["translation"])
174 | start = np.array(rigid["start"]) + offset
175 | end = np.array(rigid["end"]) + offset
176 | scale = np.array(rigid["scale"])
177 | velocity = rigid["velocity"]
178 | density = rigid["density"]
179 | color = rigid["color"]
180 | is_dynamic = rigid["isDynamic"]
181 | self.add_cube(object_id=obj_id,
182 | lower_corner=start,
183 | cube_size=(end-start)*scale,
184 | velocity=velocity,
185 | density=density,
186 | is_dynamic=is_dynamic,
187 | color=color,
188 | material=0) # 1 indicates solid
189 |
190 | # Rigid bodies
191 | for rigid_body in rigid_bodies:
192 | obj_id = rigid_body["objectId"]
193 | self.object_id_rigid_body.add(obj_id)
194 | num_particles_obj = rigid_body["particleNum"]
195 | voxelized_points_np = rigid_body["voxelizedPoints"]
196 | is_dynamic = rigid_body["isDynamic"]
197 | if is_dynamic:
198 | velocity = np.array(rigid_body["velocity"], dtype=np.float32)
199 | else:
200 | velocity = np.array([0.0 for _ in range(self.dim)], dtype=np.float32)
201 | density = rigid_body["density"]
202 | color = np.array(rigid_body["color"], dtype=np.int32)
203 | self.add_particles(obj_id,
204 | num_particles_obj,
205 | np.array(voxelized_points_np, dtype=np.float32), # position
206 | np.stack([velocity for _ in range(num_particles_obj)]), # velocity
207 | density * np.ones(num_particles_obj, dtype=np.float32), # density
208 | np.zeros(num_particles_obj, dtype=np.float32), # pressure
209 | np.array([0 for _ in range(num_particles_obj)], dtype=np.int32), # material is solid
210 | is_dynamic * np.ones(num_particles_obj, dtype=np.int32), # is_dynamic
211 | np.stack([color for _ in range(num_particles_obj)])) # color
212 |
213 |
214 | def build_solver(self):
215 | solver_type = self.cfg.get_cfg("simulationMethod")
216 | if solver_type == 0:
217 | return WCSPHSolver(self)
218 | elif solver_type == 4:
219 | return DFSPHSolver(self)
220 | else:
221 | raise NotImplementedError(f"Solver type {solver_type} has not been implemented.")
222 |
223 | @ti.func
224 | def add_particle(self, p, obj_id, x, v, density, pressure, material, is_dynamic, color):
225 | self.object_id[p] = obj_id
226 | self.x[p] = x
227 | self.x_0[p] = x
228 | self.v[p] = v
229 | self.density[p] = density
230 | self.m_V[p] = self.m_V0
231 | self.m[p] = self.m_V0 * density
232 | self.pressure[p] = pressure
233 | self.material[p] = material
234 | self.is_dynamic[p] = is_dynamic
235 | self.color[p] = color
236 |
237 | def add_particles(self,
238 | object_id: int,
239 | new_particles_num: int,
240 | new_particles_positions: ti.types.ndarray(),
241 | new_particles_velocity: ti.types.ndarray(),
242 | new_particle_density: ti.types.ndarray(),
243 | new_particle_pressure: ti.types.ndarray(),
244 | new_particles_material: ti.types.ndarray(),
245 | new_particles_is_dynamic: ti.types.ndarray(),
246 | new_particles_color: ti.types.ndarray()
247 | ):
248 |
249 | self._add_particles(object_id,
250 | new_particles_num,
251 | new_particles_positions,
252 | new_particles_velocity,
253 | new_particle_density,
254 | new_particle_pressure,
255 | new_particles_material,
256 | new_particles_is_dynamic,
257 | new_particles_color
258 | )
259 |
260 | @ti.kernel
261 | def _add_particles(self,
262 | object_id: int,
263 | new_particles_num: int,
264 | new_particles_positions: ti.types.ndarray(),
265 | new_particles_velocity: ti.types.ndarray(),
266 | new_particle_density: ti.types.ndarray(),
267 | new_particle_pressure: ti.types.ndarray(),
268 | new_particles_material: ti.types.ndarray(),
269 | new_particles_is_dynamic: ti.types.ndarray(),
270 | new_particles_color: ti.types.ndarray()):
271 | for p in range(self.particle_num[None], self.particle_num[None] + new_particles_num):
272 | v = ti.Vector.zero(float, self.dim)
273 | x = ti.Vector.zero(float, self.dim)
274 | for d in ti.static(range(self.dim)):
275 | v[d] = new_particles_velocity[p - self.particle_num[None], d]
276 | x[d] = new_particles_positions[p - self.particle_num[None], d]
277 | self.add_particle(p, object_id, x, v,
278 | new_particle_density[p - self.particle_num[None]],
279 | new_particle_pressure[p - self.particle_num[None]],
280 | new_particles_material[p - self.particle_num[None]],
281 | new_particles_is_dynamic[p - self.particle_num[None]],
282 | ti.Vector([new_particles_color[p - self.particle_num[None], i] for i in range(3)])
283 | )
284 | self.particle_num[None] += new_particles_num
285 |
286 |
287 | @ti.func
288 | def pos_to_index(self, pos):
289 | return (pos / self.grid_size).cast(int)
290 |
291 |
292 | @ti.func
293 | def flatten_grid_index(self, grid_index):
294 | return grid_index[0] * self.grid_num[1] * self.grid_num[2] + grid_index[1] * self.grid_num[2] + grid_index[2]
295 |
296 | @ti.func
297 | def get_flatten_grid_index(self, pos):
298 | return self.flatten_grid_index(self.pos_to_index(pos))
299 |
300 |
301 | @ti.func
302 | def is_static_rigid_body(self, p):
303 | return self.material[p] == self.material_solid and (not self.is_dynamic[p])
304 |
305 |
306 | @ti.func
307 | def is_dynamic_rigid_body(self, p):
308 | return self.material[p] == self.material_solid and self.is_dynamic[p]
309 |
310 |
311 | @ti.kernel
312 | def update_grid_id(self):
313 | for I in ti.grouped(self.grid_particles_num):
314 | self.grid_particles_num[I] = 0
315 | for I in ti.grouped(self.x):
316 | grid_index = self.get_flatten_grid_index(self.x[I])
317 | self.grid_ids[I] = grid_index
318 | ti.atomic_add(self.grid_particles_num[grid_index], 1)
319 | for I in ti.grouped(self.grid_particles_num):
320 | self.grid_particles_num_temp[I] = self.grid_particles_num[I]
321 |
322 | @ti.kernel
323 | def counting_sort(self):
324 | # FIXME: make it the actual particle num
325 | for i in range(self.particle_max_num):
326 | I = self.particle_max_num - 1 - i
327 | base_offset = 0
328 | if self.grid_ids[I] - 1 >= 0:
329 | base_offset = self.grid_particles_num[self.grid_ids[I]-1]
330 | self.grid_ids_new[I] = ti.atomic_sub(self.grid_particles_num_temp[self.grid_ids[I]], 1) - 1 + base_offset
331 |
332 | for I in ti.grouped(self.grid_ids):
333 | new_index = self.grid_ids_new[I]
334 | self.grid_ids_buffer[new_index] = self.grid_ids[I]
335 | self.object_id_buffer[new_index] = self.object_id[I]
336 | self.x_0_buffer[new_index] = self.x_0[I]
337 | self.x_buffer[new_index] = self.x[I]
338 | self.v_buffer[new_index] = self.v[I]
339 | self.acceleration_buffer[new_index] = self.acceleration[I]
340 | self.m_V_buffer[new_index] = self.m_V[I]
341 | self.m_buffer[new_index] = self.m[I]
342 | self.density_buffer[new_index] = self.density[I]
343 | self.pressure_buffer[new_index] = self.pressure[I]
344 | self.material_buffer[new_index] = self.material[I]
345 | self.color_buffer[new_index] = self.color[I]
346 | self.is_dynamic_buffer[new_index] = self.is_dynamic[I]
347 |
348 | if ti.static(self.simulation_method == 4):
349 | self.dfsph_factor_buffer[new_index] = self.dfsph_factor[I]
350 | self.density_adv_buffer[new_index] = self.density_adv[I]
351 |
352 | for I in ti.grouped(self.x):
353 | self.grid_ids[I] = self.grid_ids_buffer[I]
354 | self.object_id[I] = self.object_id_buffer[I]
355 | self.x_0[I] = self.x_0_buffer[I]
356 | self.x[I] = self.x_buffer[I]
357 | self.v[I] = self.v_buffer[I]
358 | self.acceleration[I] = self.acceleration_buffer[I]
359 | self.m_V[I] = self.m_V_buffer[I]
360 | self.m[I] = self.m_buffer[I]
361 | self.density[I] = self.density_buffer[I]
362 | self.pressure[I] = self.pressure_buffer[I]
363 | self.material[I] = self.material_buffer[I]
364 | self.color[I] = self.color_buffer[I]
365 | self.is_dynamic[I] = self.is_dynamic_buffer[I]
366 |
367 | if ti.static(self.simulation_method == 4):
368 | self.dfsph_factor[I] = self.dfsph_factor_buffer[I]
369 | self.density_adv[I] = self.density_adv_buffer[I]
370 |
371 |
372 | def initialize_particle_system(self):
373 | self.update_grid_id()
374 | self.prefix_sum_executor.run(self.grid_particles_num)
375 | self.counting_sort()
376 |
377 |
378 | @ti.func
379 | def for_all_neighbors(self, p_i, task: ti.template(), ret: ti.template()):
380 | center_cell = self.pos_to_index(self.x[p_i])
381 | for offset in ti.grouped(ti.ndrange(*((-1, 2),) * self.dim)):
382 | grid_index = self.flatten_grid_index(center_cell + offset)
383 | for p_j in range(self.grid_particles_num[ti.max(0, grid_index-1)], self.grid_particles_num[grid_index]):
384 | if p_i[0] != p_j and (self.x[p_i] - self.x[p_j]).norm() < self.support_radius:
385 | task(p_i, p_j, ret)
386 |
387 | @ti.kernel
388 | def copy_to_numpy(self, np_arr: ti.types.ndarray(), src_arr: ti.template()):
389 | for i in range(self.particle_num[None]):
390 | np_arr[i] = src_arr[i]
391 |
392 | def copy_to_vis_buffer(self, invisible_objects=[]):
393 | if len(invisible_objects) != 0:
394 | self.x_vis_buffer.fill(0.0)
395 | self.color_vis_buffer.fill(0.0)
396 | for obj_id in self.object_collection:
397 | if obj_id not in invisible_objects:
398 | self._copy_to_vis_buffer(obj_id)
399 |
400 | @ti.kernel
401 | def _copy_to_vis_buffer(self, obj_id: int):
402 | assert self.GGUI
403 | # FIXME: make it equal to actual particle num
404 | for i in range(self.particle_max_num):
405 | if self.object_id[i] == obj_id:
406 | self.x_vis_buffer[i] = self.x[i]
407 | self.color_vis_buffer[i] = self.color[i] / 255.0
408 |
409 | def dump(self, obj_id):
410 | np_object_id = self.object_id.to_numpy()
411 | mask = (np_object_id == obj_id).nonzero()
412 | np_x = self.x.to_numpy()[mask]
413 | np_v = self.v.to_numpy()[mask]
414 |
415 | return {
416 | 'position': np_x,
417 | 'velocity': np_v
418 | }
419 |
420 |
421 | def load_rigid_body(self, rigid_body):
422 | obj_id = rigid_body["objectId"]
423 | mesh = tm.load(rigid_body["geometryFile"])
424 | mesh.apply_scale(rigid_body["scale"])
425 | offset = np.array(rigid_body["translation"])
426 |
427 | angle = rigid_body["rotationAngle"] / 360 * 2 * 3.1415926
428 | direction = rigid_body["rotationAxis"]
429 | rot_matrix = tm.transformations.rotation_matrix(angle, direction, mesh.vertices.mean(axis=0))
430 | mesh.apply_transform(rot_matrix)
431 | mesh.vertices += offset
432 |
433 | # Backup the original mesh for exporting obj
434 | mesh_backup = mesh.copy()
435 | rigid_body["mesh"] = mesh_backup
436 | rigid_body["restPosition"] = mesh_backup.vertices
437 | rigid_body["restCenterOfMass"] = mesh_backup.vertices.mean(axis=0)
438 | is_success = tm.repair.fill_holes(mesh)
439 | # print("Is the mesh successfully repaired? ", is_success)
440 | voxelized_mesh = mesh.voxelized(pitch=self.particle_diameter)
441 | voxelized_mesh = mesh.voxelized(pitch=self.particle_diameter).fill()
442 | # voxelized_mesh = mesh.voxelized(pitch=self.particle_diameter).hollow()
443 | # voxelized_mesh.show()
444 | voxelized_points_np = voxelized_mesh.points
445 | print(f"rigid body {obj_id} num: {voxelized_points_np.shape[0]}")
446 |
447 | return voxelized_points_np
448 |
449 |
450 | def compute_cube_particle_num(self, start, end):
451 | num_dim = []
452 | for i in range(self.dim):
453 | num_dim.append(
454 | np.arange(start[i], end[i], self.particle_diameter))
455 | return reduce(lambda x, y: x * y,
456 | [len(n) for n in num_dim])
457 |
458 | def add_cube(self,
459 | object_id,
460 | lower_corner,
461 | cube_size,
462 | material,
463 | is_dynamic,
464 | color=(0,0,0),
465 | density=None,
466 | pressure=None,
467 | velocity=None):
468 |
469 | num_dim = []
470 | for i in range(self.dim):
471 | num_dim.append(
472 | np.arange(lower_corner[i], lower_corner[i] + cube_size[i],
473 | self.particle_diameter))
474 | num_new_particles = reduce(lambda x, y: x * y,
475 | [len(n) for n in num_dim])
476 | print('particle num ', num_new_particles)
477 |
478 | new_positions = np.array(np.meshgrid(*num_dim,
479 | sparse=False,
480 | indexing='ij'),
481 | dtype=np.float32)
482 | new_positions = new_positions.reshape(-1,
483 | reduce(lambda x, y: x * y, list(new_positions.shape[1:]))).transpose()
484 | print("new position shape ", new_positions.shape)
485 | if velocity is None:
486 | velocity_arr = np.full_like(new_positions, 0, dtype=np.float32)
487 | else:
488 | velocity_arr = np.array([velocity for _ in range(num_new_particles)], dtype=np.float32)
489 |
490 | material_arr = np.full_like(np.zeros(num_new_particles, dtype=np.int32), material)
491 | is_dynamic_arr = np.full_like(np.zeros(num_new_particles, dtype=np.int32), is_dynamic)
492 | color_arr = np.stack([np.full_like(np.zeros(num_new_particles, dtype=np.int32), c) for c in color], axis=1)
493 | density_arr = np.full_like(np.zeros(num_new_particles, dtype=np.float32), density if density is not None else 1000.)
494 | pressure_arr = np.full_like(np.zeros(num_new_particles, dtype=np.float32), pressure if pressure is not None else 0.)
495 | self.add_particles(object_id, num_new_particles, new_positions, velocity_arr, density_arr, pressure_arr, material_arr, is_dynamic_arr, color_arr)
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | taichi>=1.2.0
2 | trimesh
--------------------------------------------------------------------------------
/run_simulation.py:
--------------------------------------------------------------------------------
1 | import os
2 | import argparse
3 | import taichi as ti
4 | import numpy as np
5 | from config_builder import SimConfig
6 | from particle_system import ParticleSystem
7 |
8 | ti.init(arch=ti.gpu, device_memory_fraction=0.5)
9 |
10 |
11 | if __name__ == "__main__":
12 | parser = argparse.ArgumentParser(description='SPH Taichi')
13 | parser.add_argument('--scene_file',
14 | default='',
15 | help='scene file')
16 | args = parser.parse_args()
17 | scene_path = args.scene_file
18 | config = SimConfig(scene_file_path=scene_path)
19 | scene_name = scene_path.split("/")[-1].split(".")[0]
20 |
21 | substeps = config.get_cfg("numberOfStepsPerRenderUpdate")
22 | output_frames = config.get_cfg("exportFrame")
23 | output_interval = int(0.016 / config.get_cfg("timeStepSize"))
24 | output_ply = config.get_cfg("exportPly")
25 | output_obj = config.get_cfg("exportObj")
26 | series_prefix = "{}_output/particle_object_{}.ply".format(scene_name, "{}")
27 | if output_frames:
28 | os.makedirs(f"{scene_name}_output_img", exist_ok=True)
29 | if output_ply:
30 | os.makedirs(f"{scene_name}_output", exist_ok=True)
31 |
32 |
33 | ps = ParticleSystem(config, GGUI=True)
34 | solver = ps.build_solver()
35 | solver.initialize()
36 |
37 | window = ti.ui.Window('SPH', (1024, 1024), show_window = True, vsync=False)
38 |
39 | scene = ti.ui.Scene()
40 | camera = ti.ui.Camera()
41 | camera.position(5.5, 2.5, 4.0)
42 | camera.up(0.0, 1.0, 0.0)
43 | camera.lookat(-1.0, 0.0, 0.0)
44 | camera.fov(70)
45 | scene.set_camera(camera)
46 |
47 | canvas = window.get_canvas()
48 | radius = 0.002
49 | movement_speed = 0.02
50 | background_color = (0, 0, 0) # 0xFFFFFF
51 | particle_color = (1, 1, 1)
52 |
53 | # Invisible objects
54 | invisible_objects = config.get_cfg("invisibleObjects")
55 | if not invisible_objects:
56 | invisible_objects = []
57 |
58 | # Draw the lines for domain
59 | x_max, y_max, z_max = config.get_cfg("domainEnd")
60 | box_anchors = ti.Vector.field(3, dtype=ti.f32, shape = 8)
61 | box_anchors[0] = ti.Vector([0.0, 0.0, 0.0])
62 | box_anchors[1] = ti.Vector([0.0, y_max, 0.0])
63 | box_anchors[2] = ti.Vector([x_max, 0.0, 0.0])
64 | box_anchors[3] = ti.Vector([x_max, y_max, 0.0])
65 |
66 | box_anchors[4] = ti.Vector([0.0, 0.0, z_max])
67 | box_anchors[5] = ti.Vector([0.0, y_max, z_max])
68 | box_anchors[6] = ti.Vector([x_max, 0.0, z_max])
69 | box_anchors[7] = ti.Vector([x_max, y_max, z_max])
70 |
71 | box_lines_indices = ti.field(int, shape=(2 * 12))
72 |
73 | for i, val in enumerate([0, 1, 0, 2, 1, 3, 2, 3, 4, 5, 4, 6, 5, 7, 6, 7, 0, 4, 1, 5, 2, 6, 3, 7]):
74 | box_lines_indices[i] = val
75 |
76 | cnt = 0
77 | cnt_ply = 0
78 |
79 | while window.running:
80 | for i in range(substeps):
81 | solver.step()
82 | ps.copy_to_vis_buffer(invisible_objects=invisible_objects)
83 | if ps.dim == 2:
84 | canvas.set_background_color(background_color)
85 | canvas.circles(ps.x_vis_buffer, radius=ps.particle_radius, color=particle_color)
86 | elif ps.dim == 3:
87 | camera.track_user_inputs(window, movement_speed=movement_speed, hold_key=ti.ui.LMB)
88 | scene.set_camera(camera)
89 |
90 | scene.point_light((2.0, 2.0, 2.0), color=(1.0, 1.0, 1.0))
91 | scene.particles(ps.x_vis_buffer, radius=ps.particle_radius, per_vertex_color=ps.color_vis_buffer)
92 |
93 | scene.lines(box_anchors, indices=box_lines_indices, color = (0.99, 0.68, 0.28), width = 1.0)
94 | canvas.scene(scene)
95 |
96 | if output_frames:
97 | if cnt % output_interval == 0:
98 | window.write_image(f"{scene_name}_output_img/{cnt:06}.png")
99 |
100 | if cnt % output_interval == 0:
101 | if output_ply:
102 | obj_id = 0
103 | obj_data = ps.dump(obj_id=obj_id)
104 | np_pos = obj_data["position"]
105 | writer = ti.tools.PLYWriter(num_vertices=ps.object_collection[obj_id]["particleNum"])
106 | writer.add_vertex_pos(np_pos[:, 0], np_pos[:, 1], np_pos[:, 2])
107 | writer.export_frame_ascii(cnt_ply, series_prefix.format(0))
108 | if output_obj:
109 | for r_body_id in ps.object_id_rigid_body:
110 | with open(f"{scene_name}_output/obj_{r_body_id}_{cnt_ply:06}.obj", "w") as f:
111 | e = ps.object_collection[r_body_id]["mesh"].export(file_type='obj')
112 | f.write(e)
113 | cnt_ply += 1
114 |
115 | cnt += 1
116 | # if cnt > 6000:
117 | # break
118 | window.show()
119 |
--------------------------------------------------------------------------------
/scan_single_buffer.py:
--------------------------------------------------------------------------------
1 | import taichi as ti
2 |
3 |
4 | @ti.func
5 | def warp_inclusive_add_cuda(val: ti.template()):
6 | global_tid = ti.global_thread_idx()
7 | lane_id = global_tid % 32
8 | # Intra-warp scan, manually unroll
9 | offset_j = 1
10 | n = ti.simt.warp.shfl_up_i32(ti.simt.warp.active_mask(), val, offset_j)
11 | if (lane_id >= offset_j):
12 | val += n
13 | offset_j = 2
14 | n = ti.simt.warp.shfl_up_i32(ti.simt.warp.active_mask(), val, offset_j)
15 | if (lane_id >= offset_j):
16 | val += n
17 | offset_j = 4
18 | n = ti.simt.warp.shfl_up_i32(ti.simt.warp.active_mask(), val, offset_j)
19 | if (lane_id >= offset_j):
20 | val += n
21 | offset_j = 8
22 | n = ti.simt.warp.shfl_up_i32(ti.simt.warp.active_mask(), val, offset_j)
23 | if (lane_id >= offset_j):
24 | val += n
25 | offset_j = 16
26 | n = ti.simt.warp.shfl_up_i32(ti.simt.warp.active_mask(), val, offset_j)
27 | if (lane_id >= offset_j):
28 | val += n
29 | return val
30 |
31 |
32 | # target = ti.cfg.cuda
33 | target = ti.cuda
34 | if target == ti.cuda:
35 | inclusive_add = warp_inclusive_add_cuda
36 | barrier = ti.simt.block.sync
37 | elif target == ti.vulkan:
38 | inclusive_add = ti.simt.subgroup.inclusive_add
39 | barrier = ti.simt.subgroup.barrier
40 | else:
41 | raise RuntimeError(f"Arch {target} not supported for parallel scan.")
42 |
43 |
44 | @ti.kernel
45 | def shfl_scan(arr_in: ti.template(), in_beg: ti.i32, in_end: ti.i32,
46 | sum_smem: ti.template(), single_block: ti.template()):
47 | ti.loop_config(block_dim=BLOCK_SZ)
48 | for i in range(in_beg, in_end):
49 | val = arr_in[i]
50 |
51 | thread_id = i % BLOCK_SZ
52 | block_id = int((i - in_beg) // BLOCK_SZ)
53 | lane_id = thread_id % WARP_SZ
54 | warp_id = thread_id // WARP_SZ
55 |
56 | val = inclusive_add(val)
57 | barrier()
58 |
59 | # Put warp scan results to smem
60 | if (thread_id % WARP_SZ == WARP_SZ - 1):
61 | sum_smem[block_id, warp_id] = val
62 | barrier()
63 |
64 | # Inter-warp scan, use the first thread in the first warp
65 | if (warp_id == 0 and lane_id == 0):
66 | for k in range(1, BLOCK_SZ / WARP_SZ):
67 | sum_smem[block_id, k] += sum_smem[block_id, k - 1]
68 | barrier()
69 |
70 | # Update data with warp sums
71 | warp_sum = 0
72 | if (warp_id > 0):
73 | warp_sum = sum_smem[block_id, warp_id - 1]
74 | val += warp_sum
75 | arr_in[i] = val
76 |
77 | # Update partial sums
78 | if not single_block and (thread_id == BLOCK_SZ - 1):
79 | arr_in[in_end + block_id] = val
80 |
81 |
82 | @ti.kernel
83 | def uniform_add(arr_in: ti.template(), in_beg: ti.i32, in_end: ti.i32):
84 | ti.loop_config(block_dim=BLOCK_SZ)
85 | for i in range(in_beg + BLOCK_SZ, in_end):
86 | block_id = int((i - in_beg) // BLOCK_SZ)
87 | arr_in[i] += arr_in[in_end + block_id - 1]
88 |
89 |
90 | WARP_SZ = 32
91 | BLOCK_SZ = 128
92 |
93 | @ti.kernel
94 | def blit_from_field_to_field(
95 | dst: ti.template(), src: ti.template(), offset: ti.i32, size: ti.i32
96 | ):
97 | for i in range(size):
98 | dst[i + offset] = src[i]
99 |
100 |
101 | smem = None
102 | arrs = [0]
103 | ele_nums = [0]
104 | ele_nums_pos = [0]
105 | large_arr = None
106 |
107 |
108 | def parallel_prefix_sum_inclusive_inplace(input_arr, length):
109 |
110 | global smem
111 | global arrs
112 | global ele_nums
113 | global large_arr
114 |
115 | GRID_SZ = int((length + BLOCK_SZ - 1) / BLOCK_SZ)
116 | if smem is None:
117 |
118 | # Declare input array and all partial sums
119 | ele_num = length
120 |
121 | # Get starting position and length
122 | ele_nums[0] = ele_num
123 | start_pos = 0
124 | ele_nums_pos[0] = start_pos
125 |
126 | while (ele_num > 1):
127 | ele_num = int((ele_num + BLOCK_SZ - 1) / BLOCK_SZ)
128 | ele_nums.append(ele_num)
129 | start_pos += BLOCK_SZ * ele_num
130 | ele_nums_pos.append(start_pos)
131 |
132 | large_arr = ti.field(ti.i32, shape = start_pos)
133 | smem = ti.field(ti.i32, shape=(int(GRID_SZ), 64))
134 |
135 | blit_from_field_to_field(large_arr, input_arr, 0, length)
136 |
137 | for i in range(len(ele_nums) - 1):
138 | if i == len(ele_nums) - 2:
139 | shfl_scan(large_arr, ele_nums_pos[i], ele_nums_pos[i + 1], smem, True)
140 | else:
141 | shfl_scan(large_arr, ele_nums_pos[i], ele_nums_pos[i + 1], smem, False)
142 |
143 | for i in range(len(ele_nums) - 3, -1, -1):
144 | uniform_add(large_arr, ele_nums_pos[i], ele_nums_pos[i + 1])
145 |
146 | blit_from_field_to_field(input_arr, large_arr, 0, length)
147 |
--------------------------------------------------------------------------------
/sph_base.py:
--------------------------------------------------------------------------------
1 | from matplotlib.pyplot import axis
2 | import taichi as ti
3 | import numpy as np
4 |
5 |
6 | @ti.data_oriented
7 | class SPHBase:
8 | def __init__(self, particle_system):
9 | self.ps = particle_system
10 | self.g = ti.Vector([0.0, -9.81, 0.0]) # Gravity
11 | if self.ps.dim == 2:
12 | self.g = ti.Vector([0.0, -9.81])
13 | self.g = np.array(self.ps.cfg.get_cfg("gravitation"))
14 |
15 | self.viscosity = 0.01 # viscosity
16 |
17 | self.density_0 = 1000.0 # reference density
18 | self.density_0 = self.ps.cfg.get_cfg("density0")
19 |
20 | self.dt = ti.field(float, shape=())
21 | self.dt[None] = 1e-4
22 |
23 | @ti.func
24 | def cubic_kernel(self, r_norm):
25 | res = ti.cast(0.0, ti.f32)
26 | h = self.ps.support_radius
27 | # value of cubic spline smoothing kernel
28 | k = 1.0
29 | if self.ps.dim == 1:
30 | k = 4 / 3
31 | elif self.ps.dim == 2:
32 | k = 40 / 7 / np.pi
33 | elif self.ps.dim == 3:
34 | k = 8 / np.pi
35 | k /= h ** self.ps.dim
36 | q = r_norm / h
37 | if q <= 1.0:
38 | if q <= 0.5:
39 | q2 = q * q
40 | q3 = q2 * q
41 | res = k * (6.0 * q3 - 6.0 * q2 + 1)
42 | else:
43 | res = k * 2 * ti.pow(1 - q, 3.0)
44 | return res
45 |
46 | @ti.func
47 | def cubic_kernel_derivative(self, r):
48 | h = self.ps.support_radius
49 | # derivative of cubic spline smoothing kernel
50 | k = 1.0
51 | if self.ps.dim == 1:
52 | k = 4 / 3
53 | elif self.ps.dim == 2:
54 | k = 40 / 7 / np.pi
55 | elif self.ps.dim == 3:
56 | k = 8 / np.pi
57 | k = 6. * k / h ** self.ps.dim
58 | r_norm = r.norm()
59 | q = r_norm / h
60 | res = ti.Vector([0.0 for _ in range(self.ps.dim)])
61 | if r_norm > 1e-5 and q <= 1.0:
62 | grad_q = r / (r_norm * h)
63 | if q <= 0.5:
64 | res = k * q * (3.0 * q - 2.0) * grad_q
65 | else:
66 | factor = 1.0 - q
67 | res = k * (-factor * factor) * grad_q
68 | return res
69 |
70 | @ti.func
71 | def viscosity_force(self, p_i, p_j, r):
72 | # Compute the viscosity force contribution
73 | v_xy = (self.ps.v[p_i] -
74 | self.ps.v[p_j]).dot(r)
75 | res = 2 * (self.ps.dim + 2) * self.viscosity * (self.ps.m[p_j] / (self.ps.density[p_j])) * v_xy / (
76 | r.norm()**2 + 0.01 * self.ps.support_radius**2) * self.cubic_kernel_derivative(
77 | r)
78 | return res
79 |
80 | def initialize(self):
81 | self.ps.initialize_particle_system()
82 | for r_obj_id in self.ps.object_id_rigid_body:
83 | self.compute_rigid_rest_cm(r_obj_id)
84 | self.compute_static_boundary_volume()
85 | self.compute_moving_boundary_volume()
86 |
87 | @ti.kernel
88 | def compute_rigid_rest_cm(self, object_id: int):
89 | self.ps.rigid_rest_cm[object_id] = self.compute_com(object_id)
90 |
91 | @ti.kernel
92 | def compute_static_boundary_volume(self):
93 | for p_i in ti.grouped(self.ps.x):
94 | if not self.ps.is_static_rigid_body(p_i):
95 | continue
96 | delta = self.cubic_kernel(0.0)
97 | self.ps.for_all_neighbors(p_i, self.compute_boundary_volume_task, delta)
98 | self.ps.m_V[p_i] = 1.0 / delta * 3.0 # TODO: the 3.0 here is a coefficient for missing particles by trail and error... need to figure out how to determine it sophisticatedly
99 |
100 | @ti.func
101 | def compute_boundary_volume_task(self, p_i, p_j, delta: ti.template()):
102 | if self.ps.material[p_j] == self.ps.material_solid:
103 | delta += self.cubic_kernel((self.ps.x[p_i] - self.ps.x[p_j]).norm())
104 |
105 |
106 | @ti.kernel
107 | def compute_moving_boundary_volume(self):
108 | for p_i in ti.grouped(self.ps.x):
109 | if not self.ps.is_dynamic_rigid_body(p_i):
110 | continue
111 | delta = self.cubic_kernel(0.0)
112 | self.ps.for_all_neighbors(p_i, self.compute_boundary_volume_task, delta)
113 | self.ps.m_V[p_i] = 1.0 / delta * 3.0 # TODO: the 3.0 here is a coefficient for missing particles by trail and error... need to figure out how to determine it sophisticatedly
114 |
115 | def substep(self):
116 | pass
117 |
118 | @ti.func
119 | def simulate_collisions(self, p_i, vec):
120 | # Collision factor, assume roughly (1-c_f)*velocity loss after collision
121 | c_f = 0.5
122 | self.ps.v[p_i] -= (
123 | 1.0 + c_f) * self.ps.v[p_i].dot(vec) * vec
124 |
125 | @ti.kernel
126 | def enforce_boundary_2D(self, particle_type:int):
127 | for p_i in ti.grouped(self.ps.x):
128 | if self.ps.material[p_i] == particle_type and self.ps.is_dynamic[p_i]:
129 | pos = self.ps.x[p_i]
130 | collision_normal = ti.Vector([0.0, 0.0])
131 | if pos[0] > self.ps.domain_size[0] - self.ps.padding:
132 | collision_normal[0] += 1.0
133 | self.ps.x[p_i][0] = self.ps.domain_size[0] - self.ps.padding
134 | if pos[0] <= self.ps.padding:
135 | collision_normal[0] += -1.0
136 | self.ps.x[p_i][0] = self.ps.padding
137 |
138 | if pos[1] > self.ps.domain_size[1] - self.ps.padding:
139 | collision_normal[1] += 1.0
140 | self.ps.x[p_i][1] = self.ps.domain_size[1] - self.ps.padding
141 | if pos[1] <= self.ps.padding:
142 | collision_normal[1] += -1.0
143 | self.ps.x[p_i][1] = self.ps.padding
144 | collision_normal_length = collision_normal.norm()
145 | if collision_normal_length > 1e-6:
146 | self.simulate_collisions(
147 | p_i, collision_normal / collision_normal_length)
148 |
149 | @ti.kernel
150 | def enforce_boundary_3D(self, particle_type:int):
151 | for p_i in ti.grouped(self.ps.x):
152 | if self.ps.material[p_i] == particle_type and self.ps.is_dynamic[p_i]:
153 | pos = self.ps.x[p_i]
154 | collision_normal = ti.Vector([0.0, 0.0, 0.0])
155 | if pos[0] > self.ps.domain_size[0] - self.ps.padding:
156 | collision_normal[0] += 1.0
157 | self.ps.x[p_i][0] = self.ps.domain_size[0] - self.ps.padding
158 | if pos[0] <= self.ps.padding:
159 | collision_normal[0] += -1.0
160 | self.ps.x[p_i][0] = self.ps.padding
161 |
162 | if pos[1] > self.ps.domain_size[1] - self.ps.padding:
163 | collision_normal[1] += 1.0
164 | self.ps.x[p_i][1] = self.ps.domain_size[1] - self.ps.padding
165 | if pos[1] <= self.ps.padding:
166 | collision_normal[1] += -1.0
167 | self.ps.x[p_i][1] = self.ps.padding
168 |
169 | if pos[2] > self.ps.domain_size[2] - self.ps.padding:
170 | collision_normal[2] += 1.0
171 | self.ps.x[p_i][2] = self.ps.domain_size[2] - self.ps.padding
172 | if pos[2] <= self.ps.padding:
173 | collision_normal[2] += -1.0
174 | self.ps.x[p_i][2] = self.ps.padding
175 |
176 | collision_normal_length = collision_normal.norm()
177 | if collision_normal_length > 1e-6:
178 | self.simulate_collisions(
179 | p_i, collision_normal / collision_normal_length)
180 |
181 |
182 | @ti.func
183 | def compute_com(self, object_id):
184 | sum_m = 0.0
185 | cm = ti.Vector([0.0, 0.0, 0.0])
186 | for p_i in range(self.ps.particle_num[None]):
187 | if self.ps.is_dynamic_rigid_body(p_i) and self.ps.object_id[p_i] == object_id:
188 | mass = self.ps.m_V0 * self.ps.density[p_i]
189 | cm += mass * self.ps.x[p_i]
190 | sum_m += mass
191 | cm /= sum_m
192 | return cm
193 |
194 |
195 | @ti.kernel
196 | def compute_com_kernel(self, object_id: int)->ti.types.vector(3, float):
197 | return self.compute_com(object_id)
198 |
199 |
200 | @ti.kernel
201 | def solve_constraints(self, object_id: int) -> ti.types.matrix(3, 3, float):
202 | # compute center of mass
203 | cm = self.compute_com(object_id)
204 | # A
205 | A = ti.Matrix([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
206 | for p_i in range(self.ps.particle_num[None]):
207 | if self.ps.is_dynamic_rigid_body(p_i) and self.ps.object_id[p_i] == object_id:
208 | q = self.ps.x_0[p_i] - self.ps.rigid_rest_cm[object_id]
209 | p = self.ps.x[p_i] - cm
210 | A += self.ps.m_V0 * self.ps.density[p_i] * p.outer_product(q)
211 |
212 | R, S = ti.polar_decompose(A)
213 |
214 | if all(abs(R) < 1e-6):
215 | R = ti.Matrix.identity(ti.f32, 3)
216 |
217 | for p_i in range(self.ps.particle_num[None]):
218 | if self.ps.is_dynamic_rigid_body(p_i) and self.ps.object_id[p_i] == object_id:
219 | goal = cm + R @ (self.ps.x_0[p_i] - self.ps.rigid_rest_cm[object_id])
220 | corr = (goal - self.ps.x[p_i]) * 1.0
221 | self.ps.x[p_i] += corr
222 | return R
223 |
224 |
225 | # @ti.kernel
226 | # def compute_rigid_collision(self):
227 | # # FIXME: This is a workaround, rigid collision failure in some cases is expected
228 | # for p_i in range(self.ps.particle_num[None]):
229 | # if not self.ps.is_dynamic_rigid_body(p_i):
230 | # continue
231 | # cnt = 0
232 | # x_delta = ti.Vector([0.0 for i in range(self.ps.dim)])
233 | # for j in range(self.ps.solid_neighbors_num[p_i]):
234 | # p_j = self.ps.solid_neighbors[p_i, j]
235 |
236 | # if self.ps.is_static_rigid_body(p_i):
237 | # cnt += 1
238 | # x_j = self.ps.x[p_j]
239 | # r = self.ps.x[p_i] - x_j
240 | # if r.norm() < self.ps.particle_diameter:
241 | # x_delta += (r.norm() - self.ps.particle_diameter) * r.normalized()
242 | # if cnt > 0:
243 | # self.ps.x[p_i] += 2.0 * x_delta # / cnt
244 |
245 |
246 |
247 | def solve_rigid_body(self):
248 | for i in range(1):
249 | for r_obj_id in self.ps.object_id_rigid_body:
250 | if self.ps.object_collection[r_obj_id]["isDynamic"]:
251 | R = self.solve_constraints(r_obj_id)
252 |
253 | if self.ps.cfg.get_cfg("exportObj"):
254 | # For output obj only: update the mesh
255 | cm = self.compute_com_kernel(r_obj_id)
256 | ret = R.to_numpy() @ (self.ps.object_collection[r_obj_id]["restPosition"] - self.ps.object_collection[r_obj_id]["restCenterOfMass"]).T
257 | self.ps.object_collection[r_obj_id]["mesh"].vertices = cm.to_numpy() + ret.T
258 |
259 | # self.compute_rigid_collision()
260 | self.enforce_boundary_3D(self.ps.material_solid)
261 |
262 |
263 | def step(self):
264 | self.ps.initialize_particle_system()
265 | self.compute_moving_boundary_volume()
266 | self.substep()
267 | self.solve_rigid_body()
268 | if self.ps.dim == 2:
269 | self.enforce_boundary_2D(self.ps.material_fluid)
270 | elif self.ps.dim == 3:
271 | self.enforce_boundary_3D(self.ps.material_fluid)
272 |
--------------------------------------------------------------------------------