Support multiple vortices in latent swirl noise

This commit is contained in:
Ken Simpson
2025-10-03 23:38:30 -07:00
parent 5a8b37de4a
commit c0ae7b0271
2 changed files with 40 additions and 20 deletions
+2 -2
View File
@@ -38,7 +38,7 @@ Generates smooth octave-based Perlin noise and adds it to the latent. Tune the b
### Latent Swirl Noise
Warps the latent around a seeded vortex using `grid_sample`. Control the maximum rotation (in radians), the falloff radius, centre jitter, and blend strength to inject painterly whirlpools or gentle twisting motion into the underlying features.
Warps the latent around one or more seeded vortices using `grid_sample`. Control the vortex count, maximum rotation (in radians), falloff radius, centre jitter, and blend strength to inject painterly whirlpools or gentle twisting motion into the underlying features.
### Conditioning (Add Noise)
@@ -82,7 +82,7 @@ Image exploration rarely means chasing a single "correct" answer. These nodes ar
- Follow the noise with `Conditioning (Gaussian Blur)` to soften abrupt emphasis changes or to merge multi-prompt blends into a single vibe. Blurring after noise often produces dreamlike, painterly shifts instead of chaotic drift.
- Use `Conditioning (Scale)` as a volume knob while iterating: dial the factor down to 0.3–0.5 when you want the image to respond mostly to the recovered latent, then crank it above 1.5 when the textual guidance should take the lead.
- Pair `Latent Gaussian Blur` with `Conditioning (Gaussian Blur)` for holistic smoothing—latent blur calms texture while conditioning blur calms prompt pacing. Reintroduce energy by sprinkling in `Add Latent Noise` or ramping the conditioning scale afterwards.
- Sculpt structured detail by layering `Latent Perlin Fractal Noise` (shared mode for broad waves, per-channel for colour shifts) before blending back with `Latent Swirl Noise` to twist the resulting features into fluid patterns.
- Sculpt structured detail by layering `Latent Perlin Fractal Noise` (shared mode for broad waves, per-channel for colour shifts) before blending back with `Latent Swirl Noise`—increase `vortices` for multi-spiral motion, or keep it at 1 for a single focal swirl.
- Split a latent with `Latent Frequency Split`, sharpen or noise the high band while blurring the low band, then recombine via `Latent Mixer`/`Add` nodes to weave sharp detail onto soft composition.
- Carve prompts with `Conditioning (Frequency Split)` and return them through `Conditioning (Frequency Merge)`: keep low-band text steady for the story while remapping the high band through noise, blur, or scale to emphasize only certain fragments.
- Swap seeds between the inverter, forward diffusion, and noise nodes as you iterate. Small tweaks here can shift the interplay between latent detail and prompt guidance, rewarding playful experimentation.
+38 -18
View File
@@ -216,17 +216,26 @@ def _normalized_meshgrid(height, width, device, dtype):
return torch.meshgrid(ys, xs, indexing="ij")
def _swirl_grid(base_y, base_x, center_x, center_y, strength, radius, direction):
radius = max(radius, 1e-3)
dx = base_x - center_x
dy = base_y - center_y
r = torch.sqrt(dx * dx + dy * dy)
falloff = torch.exp(-torch.square(r / radius))
angle = direction * strength * falloff
sin_a = torch.sin(angle)
cos_a = torch.cos(angle)
x_new = dx * cos_a - dy * sin_a + center_x
y_new = dx * sin_a + dy * cos_a + center_y
def _swirl_grid(base_y, base_x, vortices):
total_dx = torch.zeros_like(base_x)
total_dy = torch.zeros_like(base_y)
for center_x, center_y, strength, radius, direction in vortices:
radius = max(radius, 1e-3)
dx = base_x - center_x
dy = base_y - center_y
r = torch.sqrt(dx * dx + dy * dy)
falloff = torch.exp(-torch.square(r / radius))
angle = direction * strength * falloff
sin_a = torch.sin(angle)
cos_a = torch.cos(angle)
rotated_dx = dx * cos_a - dy * sin_a
rotated_dy = dx * sin_a + dy * cos_a
total_dx = total_dx + (rotated_dx - dx)
total_dy = total_dy + (rotated_dy - dy)
x_new = base_x + total_dx
y_new = base_y + total_dy
grid = torch.stack((x_new, y_new), dim=-1)
return torch.clamp(grid, -1.0, 1.0)
@@ -758,6 +767,12 @@ class LatentSwirlNoise:
"required": {
"latent": ("LATENT",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"vortices": ("INT", {
"default": 1,
"min": 1,
"max": 16,
"tooltip": "Number of independent vortex centres to spawn per latent."
}),
"strength": ("FLOAT", {
"default": 0.75,
"min": 0.0,
@@ -800,7 +815,7 @@ class LatentSwirlNoise:
FUNCTION = "add_swirl_noise"
CATEGORY = "Latent/Noise"
def add_swirl_noise(self, latent, seed, strength, radius, center_spread, direction_bias, mix):
def add_swirl_noise(self, latent, seed, vortices, strength, radius, center_spread, direction_bias, mix):
if strength == 0.0 or mix == 0.0:
return (latent,)
@@ -823,19 +838,24 @@ class LatentSwirlNoise:
center_spread = max(0.0, min(center_spread, 1.0))
mix = max(0.0, min(mix, 1.0))
bias_threshold = max(0.0, min(1.0, (direction_bias + 1.0) * 0.5))
vortex_count = max(1, int(vortices))
cpu_generator = torch.Generator(device="cpu").manual_seed(seed)
total = working.shape[0]
for idx in range(total):
rand_vals = torch.rand(3, generator=cpu_generator)
offsets = (rand_vals[:2] * 2.0 - 1.0) * center_spread
center_y = offsets[0].item()
center_x = offsets[1].item()
direction = 1.0 if rand_vals[2].item() < bias_threshold else -1.0
rand_vals = torch.rand((vortex_count, 3), generator=cpu_generator)
vortex_params = []
grid = _swirl_grid(base_y, base_x, center_x, center_y, strength, radius, direction).unsqueeze(0)
for vortex_idx in range(vortex_count):
offsets = (rand_vals[vortex_idx, :2] * 2.0 - 1.0) * center_spread
center_y = offsets[0].item()
center_x = offsets[1].item()
direction = 1.0 if rand_vals[vortex_idx, 2].item() < bias_threshold else -1.0
vortex_params.append((center_x, center_y, float(strength), float(radius), direction))
grid = _swirl_grid(base_y, base_x, vortex_params).unsqueeze(0)
sample = working[idx:idx + 1]
warped = F.grid_sample(