···11-# pytorch adaptation of the method described here:
22-# https://www.dgp.toronto.edu/public_user/stam/reality/Research/pdf/GDC03.pdf
11+# pytorch adaptation of the methods described here:
22+# https://www.dgp.toronto.edu/public_user/stam/reality/Research/pdf/GDC03.pdf (overall design)
33+# https://www.karlsims.com/fluid-flow.html (divergence clearing step)
3445# only dependencies are pillow and pytorch
56···1819Path(result_directory + "/velocities").mkdir(parents=True, exist_ok=True)
1920Path(result_directory + "/densities").mkdir(parents=True, exist_ok=True)
20212121-scale = 2
2222-w = 1920 // scale
2323-h = 1080 // scale
2222+scale = 10
2323+w = 2**scale
2424+h = 2**scale
24252526max_iterations = 100000
2626-save_every = 60
2727+save_every = 1
27282828-velocity_diffusion_rate = 10#100
2929-density_diffusion_rate = 1
2929+velocity_diffusion_rate = 20
3030+density_diffusion_rate = None
3031density_timescale = 0.005
3131-timescale = 0.005
3232+timescale = 0.01
32333334torch.set_default_device("cuda")
34353535-diffusion_solver_steps = 1
3636-conservation_solver_steps = 50
3636+diffusion_solver_steps = 10
3737+divergence_clearing_steps = 20
373838393940# save images
···6768# skipping points where the window would hang off the edge of the field. thus, we end up with
6869# a result that's slightly smaller than the field
69707070-# the paper this was based on doesn't mention convolution, but many of its deeply nested for loops are doing just that
7171-7271def convolve(field, kernel):
7372 # conv2d works in batches & we only ever need to do a 1-element batch
7473 # "unsqueeze" is pytorch's absolutely bizarre term for wrapping a tensor with another dimension. like going from [1,2] to [[1,2]]
···8382 [0, 1, 0]
8483]]], dtype=torch.float)
85848686-# difference of values of horizontal neighbors
8787-horizontal_projection_kernel = torch.tensor([[[
8888- [0, 0, 0],
8989- [1, 0, -1],
9090- [0, 0, 0]
8585+# gradient of divergence in the x direction
8686+x_div_kernel = torch.tensor([[[
8787+ [ 1, 2, 1],
8888+ [-2,-4,-2],
8989+ [ 1, 2, 1]
9190]]], dtype=torch.float)
92919393-# difference of values of vertical neighbors
9494-vertical_projection_kernel = torch.tensor([[[
9595- [0, 1, 0],
9696- [0, 0, 0],
9797- [0, -1, 0]
9292+div_opp_kernel = torch.tensor([[[
9393+ [ 1, 0,-1],
9494+ [ 0, 0, 0],
9595+ [-1, 0, 1]
9896]]], dtype=torch.float)
9997100100-101101-102102-9898+# gradient of divergence in the y direction
9999+y_div_kernel = torch.tensor([[[
100100+ [ 1,-2, 1],
101101+ [ 2,-4, 2],
102102+ [ 1,-2, 1]
103103+]]], dtype=torch.float)
103104104105# various boundary conditions
105106···157158158159159160# advection
160160-# this is where i deviate from the original paper.
161161+# this is a point where i deviate from the original paper.
161162# i take the velocity at every point, and add a scaled version of that to the index at each point
162163# to find where that velocity will carry whatever is being advected along it in one timestep.
163164# that will be a point that i decompose into the integer component & fractional component. like (3.4, 1.2) -> (3,1) + (0.4, 0.2)
···193194 opposed_vertical_boundary(res[0])
194195 return res
195196196196-197197-198198-def enforce_conservation(field):
199199- divergence = convolve(field[1], horizontal_projection_kernel)
200200- divergence += convolve(field[0], vertical_projection_kernel)
201201- divergence *= 0.5
202202- continuous_boundary(divergence)
203203- p = torch.zeros_like(field[0])
204204- for i in range(conservation_solver_steps):
205205- p[1:h-1,1:w-1] = (divergence + convolve(p, diffusion_kernel)) / 4
206206- continuous_boundary(p)
207207- field[1,1:h-1,1:w-1] += 0.5 * convolve(p, horizontal_projection_kernel)
208208- field[0,1:h-1,1:w-1] += 0.5 * convolve(p, vertical_projection_kernel)
209209- #field[1,1:h-1,1:w-1] += 0.5 * convolve(field[1], horizontal_projection_kernel)
210210- #field[0,1:h-1,1:w-1] += 0.5 * convolve(field[0], vertical_projection_kernel)
211211- opposed_horizontal_boundary(field[1])
212212- opposed_vertical_boundary(field[0])
213213-197197+def clear_divergence(field):
198198+ opposed_horizontal_boundary(field[0])
199199+ opposed_vertical_boundary(field[1])
200200+ for i in range(divergence_clearing_steps):
201201+ x_op = convolve(field[0], div_opp_kernel)
202202+ y_op = convolve(field[1], div_opp_kernel)
203203+ field[1,1:h-1,1:w-1] += (convolve(field[1], y_div_kernel) + x_op) / 8
204204+ field[0,1:h-1,1:w-1] += (convolve(field[0], x_div_kernel) + y_op) / 8
205205+ opposed_horizontal_boundary(field[0])
206206+ opposed_vertical_boundary(field[1])
214207215208216209···218211# initialize a small field of random velocities, then upscale it interpolating between those velocities,
219212# so that the variation in velocity is somewhat smooth instead of pure per-pixel noise,
220213# which would lead to a less interesting simulation
221221-velocities = torch.randn([2, h//128, w//128]) * 30
214214+velocities = torch.randn([2, h//32, w//32]) * 120
222215upsample = torch.nn.Upsample(size=[h, w], mode='bilinear')
223216velocities = upsample(velocities.unsqueeze(0))[0]
224217225225-# initialize "dye" densities to a uniform field of 0.1
226226-densities = torch.ones([1, h, w]) * 0.1
218218+# initialize "dye" densities to a uniform field
219219+densities = torch.ones([1, h, w]) * 0.3
227220# add lines
228228-for x in range(20):
229229- densities[0,:,x*w//20] += 0.8
230230-for y in range(20):
231231- densities[0,y*h//20,:] += 0.8
221221+#for x in range(20):
222222+# densities[0,:,x*w//20] += 0.8
223223+#for y in range(20):
224224+# densities[0,y*h//20,:] += 0.8
232225233226frame_index = 0
234227235228last_frame = time.perf_counter()
236229237237-limit = 1 / (math.sqrt(2) * timescale)
230230+limit = 1 / (timescale * 2 * math.sqrt(2))
238231239232# core loop of the simulation
240233for iteration in range(max_iterations):
241234 # add a tiny bit of "dye" to the fluid at all points
242242- densities += 0.003
243243- if iteration % (30 * save_every) == 0:
244244- for x in range(20):
245245- densities[0,:,x*w//20] += 0.5
246246- for y in range(20):
247247- densities[0,y*h//20,:] += 0.5
235235+ densities.add_(0.003)
236236+ #if iteration % (30 * save_every) == 0:
237237+ # for x in range(20):
238238+ # densities[0,:,x*w//20] += 0.3
239239+ # for y in range(20):
240240+ # densities[0,y*h//20,:] += 0.3
248241249242250250- velocities[1,h//12+h//3:h//12+2*h//3,w//8] += 50 + 50 * math.sin(iteration * 0.005)
243243+ #velocities[1,h//12+h//3:h//12+2*h//3,w//8] += 50 + 50 * math.sin(iteration * 0.005)
251244 #velocities[1,h//3-h//12:2*h//3-h//12,7*w//8] -= 20 + 70 * math.sin(iteration * 0.01)
252245 #velocities[0,7*h//8,4*w//6:5*w//6] -= 60 + 50 * math.sin(iteration * 0.03)
253246254247 # diffuse the velocities in both directions, enforcing a boundary
255248 # condition that maintains a constant inward velocity matching the outward velocity along the edges. that is, a wall
256256- velocities.clamp_(-limit, limit)
257249 velocities[0] = diffuse(velocities[0], velocity_diffusion_rate, opposed_horizontal_boundary, timescale)
258250 velocities[1] = diffuse(velocities[1], velocity_diffusion_rate, opposed_vertical_boundary, timescale)
259259- enforce_conservation(velocities)
251251+ clear_divergence(velocities)
252252+ velocities.clamp_(-limit, limit)
260253261254 # let the velocity field flow along itself
262255 velocities = advect(velocities, velocities, timescale)
263263- enforce_conservation(velocities)
256256+ clear_divergence(velocities)
264257265258 # diffuse the densities & let them flow along the velocity field
266266- # in the source paper this is done at the same timescale as the fluid simulation itself, but i got better results
267267- # with a separate timescale
268268- #densities = diffuse(densities[0], density_diffusion_rate, continuous_boundary, density_timescale).unsqueeze(0)
259259+ if density_diffusion_rate is not None:
260260+ densities = diffuse(densities[0], density_diffusion_rate, continuous_boundary, density_timescale).unsqueeze(0)
269261 densities = advect(densities, velocities, density_timescale)
270262271263 # remove a little density
272272- densities -= 0.003
264264+ densities.sub_(0.003)
273265 densities.clamp_(0,1)
274266275267 if iteration % save_every == 0:
276268 frame_index += 1
277269 frame_time = time.perf_counter() - last_frame
278270 image = torch.cat((0.5 + 0.5 * velocities / torch.sqrt(velocities[0]**2 + velocities[1]**2), torch.zeros((1,h,w))), dim=0)
279279- #save(image, f"velocities/{iteration}")
280271 save(image, f"velocities/{frame_index:06d}")
281281- #monochrome_save(densities[0], f"densities/{iteration}")
282272 monochrome_save(densities[0], f"densities/{frame_index:06d}")
283273284274 print(f"frame {frame_index:06d}: {frame_time:06f}")