Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
91 lines
3.8 KiB
Python
91 lines
3.8 KiB
Python
"""
|
|
Plane waves, summed.
|
|
|
|
octaves seeded sine waves, each with its own direction, frequency, phase and
|
|
drift rate drawn from lattice_hash, so there is no RNG and a tensor of seeds
|
|
draws the whole channel axis in one call. Their sum is an interference pattern
|
|
of straight wavefronts: ripples at one wave, moire at several. A fractional
|
|
octave count blends the sum with and without the next wave, which fades it in.
|
|
|
|
warp_strength bends the wavefronts with a low-frequency simplex field; at 0
|
|
they are straight. phase_shift shifts each wave's phase by a different amount,
|
|
so it is a different interference pattern from the same waves. time advances
|
|
each wave at its own rate, so holding the seed and advancing it moves the
|
|
pattern rather than redrawing it, the way spectral's modes drift.
|
|
"""
|
|
import math
|
|
|
|
import torch
|
|
|
|
from .base import BaseNoiseGenerator
|
|
from .fbm import MAX_OCTAVES, draw_channels, simplex_warp
|
|
from .registry import shader_generator
|
|
from .simplex import lattice_hash
|
|
from ..core.constants import DEFAULT_CHANNELS
|
|
from ..core.params import ShaderParams
|
|
from ..utils.noise_utils import create_coordinate_grid
|
|
|
|
# Cycles of the first wave across the frame at noise_scale 1.
|
|
WAVES_PER_UNIT = 3.0
|
|
# The warp field's frequency and how much of warp_strength reaches it: a
|
|
# little bends straight wavefronts a lot.
|
|
WARP_FREQUENCY = 1.0
|
|
WARP_GAIN = 0.15
|
|
# Cycles each wave drifts over a clip, before its own rate scales it.
|
|
DRIFT_PER_TIME = 0.25
|
|
|
|
|
|
@shader_generator("waves", metadata={
|
|
"description": "octaves seeded plane waves summed; straight at warp 0, bent above it",
|
|
"supports_temporal": True,
|
|
})
|
|
class WavesNoiseGenerator(BaseNoiseGenerator):
|
|
"""A sum of seeded plane waves."""
|
|
|
|
@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)
|
|
count = min(max(int(params.octaves), 1), MAX_OCTAVES)
|
|
warp_strength = float(params.warp_strength)
|
|
phase_shift = 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
|
|
|
|
def field(channel_seed):
|
|
q = simplex_warp(p, channel_seed, warp_strength * WARP_GAIN, WARP_FREQUENCY)
|
|
total = None
|
|
for i in range(count):
|
|
# One hash per wave parameter, at lattice point (wave, slot).
|
|
def draw(slot):
|
|
return lattice_hash(torch.tensor(i, device=device), torch.tensor(slot, device=device),
|
|
channel_seed)
|
|
angle = draw(1) * math.pi
|
|
frequency = WAVES_PER_UNIT * (1.0 + 0.5 * i + 0.5 * draw(2))
|
|
phase = draw(3) * (2.0 * math.pi) + phase_shift * math.pi * (i + 1)
|
|
rate = DRIFT_PER_TIME * (0.5 + draw(4))
|
|
along = q[..., 0:1] * torch.cos(angle) + q[..., 1:2] * torch.sin(angle)
|
|
wave = torch.sin(2.0 * math.pi * (frequency * along + rate * time) + phase)
|
|
total = wave if total is None else total + wave
|
|
return total / math.sqrt(count)
|
|
|
|
return draw_channels(field, coords, params, current_seed, target_channels)
|
|
|
|
|
|
def generate_waves_tensor(batch_size, height, width, params, device, seed=0,
|
|
target_channels=DEFAULT_CHANNELS):
|
|
"""Function form, matching the other generators' module-level entry points."""
|
|
return WavesNoiseGenerator.generate(
|
|
batch_size, height, width, params, device, seed, target_channels)
|