This commit is contained in:
kijai 2023-10-11 00:32:24 +03:00
commit 510d1e237e
3 changed files with 174 additions and 0 deletions

67
fluid.py Normal file
View File

@ -0,0 +1,67 @@
import numpy as np
from scipy.ndimage import map_coordinates, spline_filter
from scipy.sparse.linalg import factorized
from .numerical import difference, operator
class Fluid:
def __init__(self, shape, *quantities, pressure_order=1, advect_order=3):
self.shape = shape
self.dimensions = len(shape)
# Prototyping is simplified by dynamically
# creating advected quantities as needed.
self.quantities = quantities
for q in quantities:
setattr(self, q, np.zeros(shape))
self.indices = np.indices(shape)
self.velocity = np.zeros((self.dimensions, *shape))
laplacian = operator(shape, difference(2, pressure_order))
self.pressure_solver = factorized(laplacian)
self.advect_order = advect_order
def step(self):
# Advection is computed backwards in time as described in Stable Fluids.
advection_map = self.indices - self.velocity
# SciPy's spline filter introduces checkerboard divergence.
# A linear blend of the filtered and unfiltered fields based
# on some value epsilon eliminates this error.
def advect(field, filter_epsilon=10e-2, mode='constant'):
filtered = spline_filter(field, order=self.advect_order, mode=mode)
field = filtered * (1 - filter_epsilon) + field * filter_epsilon
return map_coordinates(field, advection_map, prefilter=False, order=self.advect_order, mode=mode)
# Apply advection to each axis of the
# velocity field and each user-defined quantity.
for d in range(self.dimensions):
self.velocity[d] = advect(self.velocity[d])
for q in self.quantities:
setattr(self, q, advect(getattr(self, q)))
# Compute the jacobian at each point in the
# velocity field to extract curl and divergence.
jacobian_shape = (self.dimensions,) * 2
partials = tuple(np.gradient(d) for d in self.velocity)
jacobian = np.stack(partials).reshape(*jacobian_shape, *self.shape)
divergence = jacobian.trace()
# If this curl calculation is extended to 3D, the y-axis value must be negated.
# This corresponds to the coefficients of the levi-civita symbol in that dimension.
# Higher dimensions do not have a vector -> scalar, or vector -> vector,
# correspondence between velocity and curl due to differing isomorphisms
# between exterior powers in dimensions != 2 or 3 respectively.
curl_mask = np.triu(np.ones(jacobian_shape, dtype=bool), k=1)
curl = (jacobian[curl_mask] - jacobian[curl_mask.T]).squeeze()
# Apply the pressure correction to the fluid's velocity field.
pressure = self.pressure_solver(divergence.flatten()).reshape(self.shape)
self.velocity -= np.gradient(pressure)
return divergence, curl, pressure

View File

@ -6,6 +6,8 @@ import numpy as np
from PIL import ImageColor, Image, ImageDraw, ImageFont from PIL import ImageColor, Image, ImageDraw, ImageFont
import os import os
import librosa import librosa
from scipy.special import erf
from .fluid import Fluid
from nodes import MAX_RESOLUTION from nodes import MAX_RESOLUTION
@ -33,6 +35,84 @@ def gaussian_kernel(kernel_size: int, sigma: float, device=None):
g = torch.exp(-(d * d) / (2.0 * sigma * sigma)) g = torch.exp(-(d * d) / (2.0 * sigma * sigma))
return g / g.sum() return g / g.sum()
class CreateFluidMask:
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "createfluidmask"
CATEGORY = "KJNodes"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"invert": ("BOOLEAN", {"default": False}),
"frames": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}),
"width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
"height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
"inflow_count": ("INT", {"default": 3,"min": 0, "max": 255, "step": 1}),
"inflow_velocity": ("INT", {"default": 1,"min": 0, "max": 255, "step": 1}),
"inflow_radius": ("INT", {"default": 8,"min": 0, "max": 255, "step": 1}),
"inflow_padding": ("INT", {"default": 50,"min": 0, "max": 255, "step": 1}),
"inflow_duration": ("INT", {"default": 60,"min": 0, "max": 255, "step": 1}),
},
}
#using code from https://github.com/GregTJ/stable-fluids
def createfluidmask(self, frames, width, height, invert, inflow_count, inflow_velocity, inflow_radius, inflow_padding, inflow_duration):
out = []
masks = []
print(frames)
RESOLUTION = width, height
DURATION = frames
INFLOW_PADDING = inflow_padding
INFLOW_DURATION = inflow_duration
INFLOW_RADIUS = inflow_radius
INFLOW_VELOCITY = inflow_velocity
INFLOW_COUNT = inflow_count
print('Generating fluid solver, this may take some time.')
fluid = Fluid(RESOLUTION, 'dye')
center = np.floor_divide(RESOLUTION, 2)
r = np.min(center) - INFLOW_PADDING
points = np.linspace(-np.pi, np.pi, INFLOW_COUNT, endpoint=False)
points = tuple(np.array((np.cos(p), np.sin(p))) for p in points)
normals = tuple(-p for p in points)
points = tuple(r * p + center for p in points)
inflow_velocity = np.zeros_like(fluid.velocity)
inflow_dye = np.zeros(fluid.shape)
for p, n in zip(points, normals):
mask = np.linalg.norm(fluid.indices - p[:, None, None], axis=0) <= INFLOW_RADIUS
inflow_velocity[:, mask] += n[:, None] * INFLOW_VELOCITY
inflow_dye[mask] = 1
for f in range(DURATION):
print(f'Computing frame {f + 1} of {DURATION}.')
if f <= INFLOW_DURATION:
fluid.velocity += inflow_velocity
fluid.dye += inflow_dye
curl = fluid.step()[1]
# Using the error function to make the contrast a bit higher.
# Any other sigmoid function e.g. smoothstep would work.
curl = (erf(curl * 2) + 1) / 4
color = np.dstack((curl, np.ones(fluid.shape), fluid.dye))
color = (np.clip(color, 0, 1) * 255).astype('uint8')
image = np.array(color).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
mask = image[:, :, :, 0]
masks.append(mask)
out.append(image)
if invert:
return (1.0 - torch.cat(out, dim=0),1.0 - torch.cat(masks, dim=0),)
return (torch.cat(out, dim=0),torch.cat(masks, dim=0),)
class CreateAudioMask: class CreateAudioMask:
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
@ -548,6 +628,7 @@ NODE_CLASS_MAPPINGS = {
"CreateTextMask": CreateTextMask, "CreateTextMask": CreateTextMask,
"CreateAudioMask": CreateAudioMask, "CreateAudioMask": CreateAudioMask,
"CreateFadeMask": CreateFadeMask, "CreateFadeMask": CreateFadeMask,
"CreateFluidMask" :CreateFluidMask,
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"INTConstant": "INT Constant", "INTConstant": "INT Constant",
@ -559,4 +640,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"CreateGradientMask": "CreateGradientMask", "CreateGradientMask": "CreateGradientMask",
"CreateTextMask" : "CreateTextMask", "CreateTextMask" : "CreateTextMask",
"CreateFadeMask" : "CreateFadeMask", "CreateFadeMask" : "CreateFadeMask",
"CreateFluidMask" : "CreateFluidMask",
} }

25
numerical.py Normal file
View File

@ -0,0 +1,25 @@
from functools import reduce
from itertools import cycle
from math import factorial
import numpy as np
import scipy.sparse as sp
def difference(derivative, accuracy=1):
# Central differences implemented based on the article here:
# http://web.media.mit.edu/~crtaylor/calculator.html
derivative += 1
radius = accuracy + derivative // 2 - 1
points = range(-radius, radius + 1)
coefficients = np.linalg.inv(np.vander(points))
return coefficients[-derivative] * factorial(derivative - 1), points
def operator(shape, *differences):
# Credit to Philip Zucker for figuring out
# that kronsum's argument order is reversed.
# Without that bit of wisdom I'd have lost it.
differences = zip(shape, cycle(differences))
factors = (sp.diags(*diff, shape=(dim,) * 2) for dim, diff in differences)
return reduce(lambda a, f: sp.kronsum(f, a, format='csc'), factors)