upgrade light leaks with seed-driven spatial randomness, organic cloud shapes, and new styles/locations

This commit is contained in:
Tom Maiaroto
2026-08-15 22:55:23 -07:00
parent e8bf6caf4f
commit 1d0db397e6
2 changed files with 68 additions and 20 deletions
+61 -20
View File
@@ -22,12 +22,12 @@ class AfterDarkFilmOpticsArtifacts:
"required": {
"image": ("IMAGE",),
"light_leak_style": (
["None", "C-41 Orange Flare", "Tungsten Blue Burn", "Vintage Magenta Leak", "Random Light Leak"],
["None", "C-41 Orange Flare", "Tungsten Blue Burn", "Vintage Magenta Leak", "Sunburst Golden Flare", "Rainbow Prism Flare", "Random Organic Leak"],
{"default": "C-41 Orange Flare"}
),
"leak_location": (
["Top Left Corner", "Top Right Corner", "Bottom Left Corner", "Bottom Right Corner", "Left Edge Strip", "Right Edge Strip"],
{"default": "Top Right Corner"}
["Random / Scattered", "Top Left Corner", "Top Right Corner", "Bottom Left Corner", "Bottom Right Corner", "Left Edge Strip", "Right Edge Strip", "Center Specular Flare", "Diagonal Streak", "Sprocket Hole Leaks"],
{"default": "Random / Scattered"}
),
"leak_intensity": ("FLOAT", {
"default": 0.30,
@@ -72,7 +72,7 @@ class AfterDarkFilmOpticsArtifacts:
self,
image,
light_leak_style="C-41 Orange Flare",
leak_location="Top Right Corner",
leak_location="Random / Scattered",
leak_intensity=0.30,
vignette_amount=0.25,
vignette_falloff=1.5,
@@ -84,7 +84,7 @@ class AfterDarkFilmOpticsArtifacts:
device = out_image.device
dtype = out_image.dtype
# 1. Procedural Light Leak Injection
# 1. Procedural Organic Light Leak Injection
if light_leak_style != "None" and leak_intensity > 0.0 and C >= 3:
generator = None
if seed != 0:
@@ -99,7 +99,9 @@ class AfterDarkFilmOpticsArtifacts:
color_tint = torch.tensor([0.20, 0.55, 1.0], device=device, dtype=dtype)
elif light_leak_style == "Vintage Magenta Leak":
color_tint = torch.tensor([0.95, 0.20, 0.65], device=device, dtype=dtype)
elif light_leak_style == "Random Light Leak":
elif light_leak_style == "Sunburst Golden Flare":
color_tint = torch.tensor([1.0, 0.75, 0.20], device=device, dtype=dtype)
elif light_leak_style in ("Random Organic Leak", "Rainbow Prism Flare"):
if generator is not None:
rand_color = torch.rand((3,), device=device, dtype=dtype, generator=generator)
else:
@@ -108,32 +110,72 @@ class AfterDarkFilmOpticsArtifacts:
else:
color_tint = torch.tensor([1.0, 0.45, 0.10], device=device, dtype=dtype)
# Generate spatial coordinate grid
# Seed-driven spatial perturbations (center jitter & ellipse scaling)
if generator is not None:
jitter_x = (torch.rand((1,), device=device, dtype=dtype, generator=generator).item() - 0.5) * 0.35
jitter_y = (torch.rand((1,), device=device, dtype=dtype, generator=generator).item() - 0.5) * 0.35
scale_x = 0.7 + torch.rand((1,), device=device, dtype=dtype, generator=generator).item() * 0.8
scale_y = 0.7 + torch.rand((1,), device=device, dtype=dtype, generator=generator).item() * 0.8
else:
jitter_x, jitter_y = 0.0, 0.0
scale_x, scale_y = 1.0, 1.0
# Spatial coordinate meshgrid
y_coords = torch.linspace(0, 1, H, device=device, dtype=dtype)
x_coords = torch.linspace(0, 1, W, device=device, dtype=dtype)
grid_y, grid_x = torch.meshgrid(y_coords, x_coords, indexing="ij")
# Determine distance mask based on leak location
if leak_location == "Top Left Corner":
dist = torch.sqrt(grid_x**2 + grid_y**2)
# Determine distance map based on location
if leak_location == "Random / Scattered":
cx = 0.5 + jitter_x
cy = 0.5 + jitter_y
dist = torch.sqrt(((grid_x - cx) / scale_x)**2 + ((grid_y - cy) / scale_y)**2)
elif leak_location == "Top Left Corner":
cx, cy = 0.0 + jitter_x, 0.0 + jitter_y
dist = torch.sqrt(((grid_x - cx) / scale_x)**2 + ((grid_y - cy) / scale_y)**2)
elif leak_location == "Top Right Corner":
dist = torch.sqrt((1.0 - grid_x)**2 + grid_y**2)
cx, cy = 1.0 + jitter_x, 0.0 + jitter_y
dist = torch.sqrt(((grid_x - cx) / scale_x)**2 + ((grid_y - cy) / scale_y)**2)
elif leak_location == "Bottom Left Corner":
dist = torch.sqrt(grid_x**2 + (1.0 - grid_y)**2)
cx, cy = 0.0 + jitter_x, 1.0 + jitter_y
dist = torch.sqrt(((grid_x - cx) / scale_x)**2 + ((grid_y - cy) / scale_y)**2)
elif leak_location == "Bottom Right Corner":
dist = torch.sqrt((1.0 - grid_x)**2 + (1.0 - grid_y)**2)
cx, cy = 1.0 + jitter_x, 1.0 + jitter_y
dist = torch.sqrt(((grid_x - cx) / scale_x)**2 + ((grid_y - cy) / scale_y)**2)
elif leak_location == "Left Edge Strip":
dist = grid_x
dist = torch.abs(grid_x - (0.0 + jitter_x)) / scale_x
elif leak_location == "Right Edge Strip":
dist = 1.0 - grid_x
dist = torch.abs(grid_x - (1.0 + jitter_x)) / scale_x
elif leak_location == "Center Specular Flare":
dist = torch.sqrt(((grid_x - 0.5) / scale_x)**2 + ((grid_y - 0.5) / scale_y)**2)
elif leak_location == "Diagonal Streak":
dist = torch.abs((grid_x + grid_y) / 1.414 - (0.7 + jitter_x)) / scale_x
elif leak_location == "Sprocket Hole Leaks":
sprocket_freq = torch.sin(grid_y * 3.14159 * 12.0) ** 4
dist = torch.clamp((torch.min(grid_x, 1.0 - grid_x) * 4.0) + (1.0 - sprocket_freq) * 0.5, 0.0, 2.0)
else:
dist = torch.sqrt((1.0 - grid_x)**2 + grid_y**2)
leak_mask = torch.clamp(1.0 - dist / 0.75, 0.0, 1.0) ** 1.8
leak_mask = leak_mask.unsqueeze(-1).unsqueeze(0) # [1, H, W, 1]
# Create base smooth spatial falloff mask
base_mask = torch.clamp(1.0 - dist / 0.85, 0.0, 1.0) ** 1.8
# Screen blending mode: Out = 1 - (1 - Image) * (1 - Leak * Intensity)
leak_tensor = leak_mask * color_tint.view(1, 1, 1, 3) * leak_intensity
# Seed-driven organic cloud modulation map (creates natural non-uniform burn shapes)
low_noise = torch.rand((1, 1, max(4, H // 16), max(4, W // 16)), device=device, dtype=dtype, generator=generator)
noise_cloud = F.interpolate(low_noise, size=(H, W), mode="bilinear", align_corners=False).squeeze()
organic_mask = base_mask * (0.60 + 0.40 * noise_cloud)
# Rainbow Prism spectral gradient option
if light_leak_style == "Rainbow Prism Flare":
rainbow_tint = torch.stack([
0.5 + 0.5 * torch.sin(dist * 6.28 + 0.0),
0.5 + 0.5 * torch.sin(dist * 6.28 + 2.09),
0.5 + 0.5 * torch.sin(dist * 6.28 + 4.18),
], dim=-1) # [H, W, 3]
leak_tensor = organic_mask.unsqueeze(-1).unsqueeze(0) * rainbow_tint.unsqueeze(0) * leak_intensity
else:
leak_tensor = organic_mask.unsqueeze(-1).unsqueeze(0) * color_tint.view(1, 1, 1, 3) * leak_intensity
# Screen blend mode: Out = 1 - (1 - Image) * (1 - Leak * Intensity)
out_image[..., :3] = 1.0 - (1.0 - out_image[..., :3]) * (1.0 - leak_tensor)
# 2. Physical Lens Vignetting (Cos^4 theta optical law)
@@ -154,7 +196,6 @@ class AfterDarkFilmOpticsArtifacts:
border_w = max(1, int(W * gate_border))
gate_mask = torch.ones((H, W), device=device, dtype=dtype)
# Create soft border falloff
for i in range(border_h):
alpha = i / float(border_h)
gate_mask[i, :] *= alpha
+7
View File
@@ -23,6 +23,13 @@ class TestNewFilmNodes(unittest.TestCase):
self.assertEqual(out.shape, self.input_image.shape)
self.assertTrue((out >= 0.0).all() and (out <= 1.0).all())
def test_optics_seed_variations(self):
node = AfterDarkFilmOpticsArtifacts()
(out1,) = node.apply_artifacts(self.input_image, light_leak_style="Rainbow Prism Flare", leak_location="Random / Scattered", seed=101)
(out2,) = node.apply_artifacts(self.input_image, light_leak_style="Rainbow Prism Flare", leak_location="Random / Scattered", seed=202)
# Verify that changing seed produces different spatial patterns
self.assertFalse(torch.allclose(out1, out2))
def test_film_halation(self):
node = AfterDarkFilmHalation()
# Create image with bright specular highlight in center