187 lines
7.1 KiB
Python
187 lines
7.1 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 torch
|
|
|
|
class NoiseUtils:
|
|
@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 float tensoru se seedem
|
|
dt = torch.full_like(coords[0], float(seed), dtype=torch.float32)
|
|
|
|
# Lineární kombinace souřadnic s prvočíselnými vahami
|
|
for i, coord in enumerate(coords):
|
|
# Převod na float32 je klíčový pro rychlé matematické operace
|
|
c_float = coord.to(torch.float32)
|
|
dt = dt + c_float * constants[i % len(constants)]
|
|
|
|
# Velmi rychlý a osvědčený hash z GPU shaderů: fract(sin(x) * 43758.5453)
|
|
# torch.sin a následné odtržení celé části je na CPU extrémně rychlé a vektorizované
|
|
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
|
|
coords_floor = tuple(torch.floor(c).long() for c in coords)
|
|
coords_frac = tuple(c - torch.floor(c) for c in 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)
|
|
grad = torch.stack(grad_components, dim=0)
|
|
grad_norm = torch.linalg.norm(grad, dim=0, keepdim=True)
|
|
grad = grad / torch.where(grad_norm == 0, torch.ones_like(grad_norm), grad_norm)
|
|
|
|
dist_components = [coords_frac[d] - corner_offsets[d] for d in range(ndim)]
|
|
dist = torch.stack(dist_components, dim=0)
|
|
dot = torch.sum(grad * dist, dim=0)
|
|
|
|
weight = 1.0
|
|
for d in range(ndim):
|
|
if corner_offsets[d]:
|
|
weight = weight * fades[d]
|
|
else:
|
|
weight = weight * (1 - fades[d])
|
|
|
|
result = result + dot * weight
|
|
|
|
return torch.clamp(result, -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)
|
|
coords_cell = tuple(torch.floor(c) 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))
|
|
|
|
# Inicializace dist_sq podle prvního tensoru v mřížce
|
|
dist_sq = torch.zeros_like(coords_input[0], dtype=torch.float32, device=device)
|
|
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 = dist_sq + diff * diff
|
|
|
|
dist = torch.sqrt(dist_sq)
|
|
min_dist = torch.minimum(min_dist, dist)
|
|
|
|
return min_dist
|
|
|
|
|
|
|
|
@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*2)+0.5
|