From 1d0db397e698715461ddcf9d2fe9e726c07d863c Mon Sep 17 00:00:00 2001 From: Tom Maiaroto Date: Sat, 15 Aug 2026 22:55:23 -0700 Subject: [PATCH] upgrade light leaks with seed-driven spatial randomness, organic cloud shapes, and new styles/locations --- film_optics_artifacts.py | 81 ++++++++++++++++++++++++++++++---------- test_film_nodes.py | 7 ++++ 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/film_optics_artifacts.py b/film_optics_artifacts.py index 3eab229..f81fc23 100644 --- a/film_optics_artifacts.py +++ b/film_optics_artifacts.py @@ -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 diff --git a/test_film_nodes.py b/test_film_nodes.py index e992973..889bb39 100644 --- a/test_film_nodes.py +++ b/test_film_nodes.py @@ -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