Files

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)