Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
85 lines
3.6 KiB
Python
85 lines
3.6 KiB
Python
"""
|
|
Two FBMs turned into fringes.
|
|
|
|
Wave interference is the superposition of two fields: where they agree the
|
|
result reinforces, where they disagree it cancels. Here the two are FBMs at
|
|
different scales and seeds, each passed through a cosine or a sine so that its
|
|
level sets become fringes, and the fringes of one cross the fringes of the
|
|
other. The result is banded and moire-like, with a bimodal value distribution
|
|
instead of the bell of a plain FBM, and standardising keeps that shape -- it is
|
|
the character of the type.
|
|
|
|
The matrix's formula reads cos(pi*f1) + sin(pi*f2); an FBM normalised to
|
|
[-1, 1] rarely leaves [-0.5, 0.5], so straight into a cosine it stays near 1
|
|
and shows no fringes. A gain in front of it fixes that, and warp_strength is
|
|
that gain: fringe density, from soft folds to dense moire. phase_shift is the
|
|
relative frequency of the second field. On a clip both FBMs are evaluated in 3D.
|
|
"""
|
|
import math
|
|
|
|
import torch
|
|
|
|
from .base import BaseNoiseGenerator
|
|
from .fbm import draw_channels, fbm, time_is_an_axis
|
|
from .registry import shader_generator
|
|
from ..core.constants import DEFAULT_CHANNELS
|
|
from ..core.params import ShaderParams
|
|
from ..utils.noise_utils import create_coordinate_grid
|
|
|
|
# Simplex cells across the frame at noise_scale 1.
|
|
CELLS_PER_UNIT = 2.0
|
|
# The second field: its scale against the first, and where in the noise it sits.
|
|
SECOND_SCALE = 1.5
|
|
SECOND_OFFSET = 17.3
|
|
# Half-turns of fringe per unit of field at warp_strength 0, and per unit of
|
|
# warp_strength. Calibrated on a 64x64 draw: at 0 the fringes are still coarser
|
|
# than four pixels (lag-4 correlation 0.51 against fractal's 0.69), at 5 they
|
|
# are dense moire. Twice this made the default field white within four pixels.
|
|
BASE_GAIN = 0.5
|
|
GAIN_PER_WARP = 0.5
|
|
|
|
|
|
@shader_generator("interference", metadata={
|
|
"description": "Two FBMs turned into crossing cos/sin fringes: banded, moire-like",
|
|
"supports_temporal": True,
|
|
})
|
|
class InterferenceNoiseGenerator(BaseNoiseGenerator):
|
|
"""Crossing fringes from two fractal fields."""
|
|
|
|
@staticmethod
|
|
def generate(
|
|
batch_size: int,
|
|
height: int,
|
|
width: int,
|
|
params: ShaderParams,
|
|
device: torch.device,
|
|
seed: int = 0,
|
|
target_channels: int = DEFAULT_CHANNELS,
|
|
) -> torch.Tensor:
|
|
scale = float(params.scale)
|
|
octaves = int(params.octaves)
|
|
gain = math.pi * (BASE_GAIN + GAIN_PER_WARP * float(params.warp_strength))
|
|
second_rate = 1.0 + 0.2 * float(params.phase_shift)
|
|
time = float(params.time)
|
|
base_seed = params.get("base_seed", seed)
|
|
current_seed = base_seed if params.use_temporal_coherence else seed
|
|
|
|
coords = create_coordinate_grid(batch_size, height, width, device)
|
|
p = coords * (scale * CELLS_PER_UNIT)
|
|
z = time if time_is_an_axis(params.get("time_axis", None), time) else None
|
|
|
|
def field(channel_seed):
|
|
first = fbm(p, octaves, channel_seed, time=z)
|
|
second = fbm(p * SECOND_SCALE + SECOND_OFFSET, max(1, octaves - 1), channel_seed + 10,
|
|
persistence=0.6, lacunarity=1.8, time=z)
|
|
return torch.cos(gain * first) + torch.sin(gain * second_rate * second)
|
|
|
|
return draw_channels(field, coords, params, current_seed, target_channels)
|
|
|
|
|
|
def generate_interference_tensor(batch_size, height, width, params, device, seed=0,
|
|
target_channels=DEFAULT_CHANNELS):
|
|
"""Function form, matching the other generators' module-level entry points."""
|
|
return InterferenceNoiseGenerator.generate(
|
|
batch_size, height, width, params, device, seed, target_channels)
|