upgrade light leaks with seed-driven spatial randomness, organic cloud shapes, and new styles/locations
This commit is contained in:
+61
-20
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user