Files

293 lines
11 KiB
Python

"""
GPU-accelerated noise generation utilities for arbitrary ND tensors.
Pure PyTorch implementation with deterministic seeding.
True Perlin noise with gradient interpolation - arbitrary dimensions.
"""
import math
import torch
class NoiseUtils:
@staticmethod
def colored_noise(shape, seed, device, beta):
generator = torch.Generator(device=device).manual_seed(seed)
white = torch.randn(tuple(shape), generator=generator, device=device)
if white.ndim == 0 or white.shape[-1] < 2 or white.numel() == 0:
return white
spectrum = torch.fft.rfft(white, dim=-1)
bin_count = spectrum.shape[-1]
frequencies = torch.arange(1, bin_count, dtype=torch.float32, device=device)
weights = torch.zeros(bin_count, dtype=torch.float32, device=device)
if bin_count > 1:
weights[1:] = frequencies.pow(-beta / 2.0)
colored = torch.fft.irfft(spectrum * weights, n=white.shape[-1], dim=-1)
colored = colored - colored.mean(dim=-1, keepdim=True)
scale = colored.std(dim=-1, unbiased=False, keepdim=True)
return torch.where(scale > 0, colored / scale.clamp_min(1e-8), torch.zeros_like(colored))
@staticmethod
def _fade(t):
"""
Optimalizovaná Perlinova fade křivka.
Místo t**3 používáme t * t * t, což je v PyTorchi řádově rychlejší.
"""
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
@staticmethod
def _hash_nd(coords, seed=0):
"""
PLNĚ VEKTORIZOVANÁ matematická hashovací funkce.
Žádné bitové operace. Využívá pseudo-náhodnou sinusovou vlnu.
Čas výpočtu pro 25 mil. bodů klesne na milisekundy.
"""
# Použijeme stabilní deterministické koeficienty pro míchání dimenzí
constants = [12.9898, 78.233, 45.164, 92.411, 27.531, 53.892]
# Inicializace jako float pro implicitní broadcasting místo full_like
dt = float(seed)
# Lineární kombinace souřadnic s prvočíselnými vahami
for i, coord in enumerate(coords):
# Pokud už je to float32, to() je skoro no-op, ale můžeme rovnou přičítat
dt = dt + coord.to(torch.float32) * constants[i % len(constants)]
# Velmi rychlý a osvědčený hash z GPU shaderů: fract(sin(x) * 43758.5453)
st = torch.sin(dt) * 43758.5453
h = st - torch.floor(st)
# Vrátí hodnotu bezpečně v rozsahu [0, 0.9999] jako původní funkce
return h.clamp(0.0, 1.0)
@staticmethod
def perlin_noise_nd(coords_input, scale=1.0, seed=0, device=None):
"""
N-dimensional Perlin noise with proper gradient interpolation.
coords_input: tuple of coordinate tensors with same shape
scale: frequency scale
seed: random seed
Returns: noise tensor in same shape as input coordinates
"""
ndim = len(coords_input)
# Scale coordinates
coords = tuple(c / scale for c in coords_input)
# Split into integer and fractional parts
# Keep coords_floor as float32 to avoid expensive conversions to and from long
coords_floor = tuple(torch.floor(c).to(torch.float32) for c in coords)
coords_frac = tuple(c - coords_floor[d] for d, c in enumerate(coords))
# Fade curves for each dimension
fades = tuple(NoiseUtils._fade(f) for f in coords_frac)
# Accumulate weighted corner contributions
result = torch.zeros_like(coords_input[0])
prime_seeds = [1013, 1619, 3137, 5021, 7919, 10427]
for corner_idx in range(1 << ndim):
corner_offsets = tuple((corner_idx >> d) & 1 for d in range(ndim))
corner_coords = tuple(coords_floor[d] + corner_offsets[d] for d in range(ndim))
grad_components = []
for d in range(ndim):
h = NoiseUtils._hash_nd(corner_coords, seed + prime_seeds[d % len(prime_seeds)] * (d + 1))
grad_components.append(h * 2.0 - 1.0)
# Optimalizace: výpočet normy a dot produktu iterativně bez torch.stack
grad_norm_sq = grad_components[0] * grad_components[0]
for d in range(1, ndim):
grad_norm_sq += grad_components[d] * grad_components[d]
inv_norm = torch.where(grad_norm_sq == 0.0, torch.ones_like(grad_norm_sq), torch.rsqrt(grad_norm_sq))
dot = grad_components[0] * (coords_frac[0] - corner_offsets[0]) * inv_norm
for d in range(1, ndim):
dot += grad_components[d] * (coords_frac[d] - corner_offsets[d]) * inv_norm
weight = fades[0] if corner_offsets[0] else (1.0 - fades[0])
for d in range(1, ndim):
weight = weight * (fades[d] if corner_offsets[d] else (1.0 - fades[d]))
result += dot * weight
# Gradient noise with unit gradients peaks at ±sqrt(ndim)/2; rescale to [-1, 1].
return torch.clamp(result * (2.0 / math.sqrt(ndim)), -1, 1)
@staticmethod
def cellular_noise_nd(coords_input, scale=1.0, jitter=0.5, seed=0, device=None):
"""
N-dimensional Cellular (Voronoi) noise - OPRAVENÁ VERZE.
Správně odečítá jednotlivé dimenze tensorů.
"""
ndim = len(coords_input)
device = coords_input[0].device
# 1. Škálování a rozdělení souřadnic
coords = tuple(c / scale for c in coords_input)
# Optimalizace: uložit jako float32 pro rychlejší indexování a hash
coords_cell = tuple(torch.floor(c).to(torch.float32) for c in coords)
# OPRAVA: Musíme indexovat konkrétní prvek z tuple pomocí `coords_cell[i]`
coords_frac = tuple(coords[i] - coords_cell[i] for i in range(ndim))
# Pro inicializaci použijeme první tensor z mřížky, abychom dodrželi správný shape
min_dist = torch.full_like(coords_input[0], float('inf'), dtype=torch.float32, device=device)
# 2. Generování všech 3^ndim offsetů přímo v PyTorchi
base_range = torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float32, device=device)
offset_grids = torch.meshgrid(*[base_range] * ndim, indexing='ij')
gpu_offsets = torch.stack([g.flatten() for g in offset_grids], dim=-1)
num_neighbors = gpu_offsets.shape[0]
prime_seeds = [1013, 1619, 3137, 5021, 7919, 10427]
# 3. Plně akcelerovaný výpočet sousedství
for n in range(num_neighbors):
current_offset = gpu_offsets[n]
# Souřadnice sousední buňky složené ze všech dimenzí naráz pro správný hash
neighbor_coords = tuple(coords_cell[i] + current_offset[i] for i in range(ndim))
dist_sq = 0.0
for i in range(ndim):
# Generování unikátního seedu pro každou dimenzi aktuálního souseda
s = seed + prime_seeds[i % len(prime_seeds)] * (i + 2)
h = NoiseUtils._hash_nd(neighbor_coords, s)
# Pozice feature pointu: offset + hash * jitter
pos_i = current_offset[i] + h * jitter
# Vzdálenost pixelu od tohoto bodu
diff = coords_frac[i] - pos_i
dist_sq += diff * diff
dist = torch.sqrt(dist_sq)
min_dist = torch.minimum(min_dist, dist)
# High jitter can leave points more than one cell from any feature point.
return min_dist.clamp(0.0, 1.0)
@staticmethod
def plasma_noise_nd(
coords_input,
scale=1.0,
seed=0,
device=None,
octaves=4,
persistence=0.5,
lacunarity=2.0,
):
"""
N-dimensional Plasma/Turbulence noise in [0, 1].
Uses Perlin octaves with automatic octave limiting to reduce aliasing.
"""
result = torch.zeros_like(coords_input[0])
amplitude = 1.0
frequency = 1.0
max_amplitude = 0.0
for octave in range(octaves):
noise_octave = NoiseUtils.perlin_noise_nd(
tuple((c / scale) * frequency for c in coords_input),
1.0,
seed + octave * 1000,
device
)
result = result + noise_octave * amplitude
max_amplitude += amplitude
amplitude *= persistence
frequency *= lacunarity
# Normalizace zpět do [-1, 1] a potom do [0, 1]
result = result / max_amplitude
return result * 0.5 + 0.5
@staticmethod
def _accumulate_octaves_nd(sample_fn, octaves=1, persistence=0.5, lacunarity=2.0):
octaves = max(1, int(octaves))
result = None
amplitude = 1.0
frequency = 1.0
max_amplitude = 0.0
for octave in range(octaves):
value = sample_fn(frequency, octave)
if result is None:
result = torch.zeros_like(value, dtype=torch.float32)
result = result + value * amplitude
max_amplitude += amplitude
amplitude *= persistence
frequency *= lacunarity
if max_amplitude == 0.0:
return result
return result / max_amplitude
@staticmethod
def ridged_noise_nd(coords_input, scale=1.0, seed=0, device=None, octaves=4, persistence=0.5, lacunarity=2.0, ridge_power=2.0):
"""
Ridged multifractal noise in [0, 1].
"""
def sample(frequency, octave):
noise = NoiseUtils.perlin_noise_nd(
tuple((c / scale) * frequency for c in coords_input),
1.0,
seed + octave * 1000,
device,
)
ridged = (1.0 - noise.abs()).clamp(0.0, 1.0)
return ridged.pow(ridge_power)
return NoiseUtils._accumulate_octaves_nd(sample, octaves, persistence, lacunarity).clamp(0.0, 1.0)
@staticmethod
def domain_warp_noise_nd(
coords_input,
scale=1.0,
seed=0,
device=None,
warp_scale=1.0,
warp_strength=0.35,
octaves=4,
warp_octaves=2,
persistence=0.5,
lacunarity=2.0,
):
"""
Domain-warped Perlin noise in [0, 1].
"""
ndim = len(coords_input)
warp_fields = []
for dim in range(ndim):
def sample_warp(frequency, octave, dim=dim):
return NoiseUtils.perlin_noise_nd(
tuple((c / warp_scale) * frequency for c in coords_input),
1.0,
seed + 10000 + dim * 1000 + octave * 97,
device,
)
warp_fields.append(NoiseUtils._accumulate_octaves_nd(sample_warp, warp_octaves, persistence, lacunarity))
warped_coords = tuple(
(coords_input[dim] / scale) + warp_fields[dim] * warp_strength
for dim in range(ndim)
)
def sample_base(frequency, octave):
return NoiseUtils.perlin_noise_nd(
tuple(c * frequency for c in warped_coords),
1.0,
seed + 20000 + octave * 1000,
device,
)
base = NoiseUtils._accumulate_octaves_nd(sample_base, octaves, persistence, lacunarity)
return ((base + 1.0) * 0.5).clamp(0.0, 1.0)