Files
mcDandy-more_math/more_math/noise_utils.py
T

196 lines
6.9 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:
"""GPU-accelerated Perlin noise generation with support for arbitrary dimensions."""
@staticmethod
def _hash_nd(coords, seed=0):
"""
Hash function for N-dimensional coordinates.
coords: tuple or list of coordinate tensors
Returns: hash value in [0, 1)
"""
h = seed
# Combine all coordinates into hash
for i, coord in enumerate(coords):
prime = [73856093, 19349663, 83492791, 39916801, 56962349, 76978801][i % 6]
h ^= (torch.floor(coord).long() * prime)
h = h ^ (h >> 13)
h = (h * 377119761) & 0x7fffffff
return (h.float() / 0x7fffffff).clamp(0, 0.9999)
@staticmethod
def _fade(t):
"""Perlin fade curve: 6t^5 - 15t^4 + 10t^3"""
return t**3 * (t * (t * 6 - 15) + 10)
@staticmethod
def _build_grid_samples(coords_frac, coords_floor):
"""
Build all 2^n corner samples for n-dimensional Perlin noise.
coords_frac: tuple of fractional parts (for fading)
coords_floor: tuple of integer parts (for grid corners)
Returns: list of (corner_coords, fade_values) tuples
"""
ndim = len(coords_floor)
samples = []
# Generate all 2^ndim corners
for i in range(1 << ndim):
corner_coords = []
for d in range(ndim):
# Extract bit d from i to determine if we add 1 to this dimension
if (i >> d) & 1:
corner_coords.append(coords_floor[d] + 1)
else:
corner_coords.append(coords_floor[d])
samples.append(tuple(corner_coords))
return samples
@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.
coords_input: tuple of coordinate tensors
Returns: distance to nearest feature point in [0, 1]
"""
ndim = len(coords_input)
# Scale coordinates
coords = tuple(c / scale for c in coords_input)
# Integer grid cell and position within cell
coords_cell = tuple(torch.floor(c).long() for c in coords)
coords_frac = tuple(c - torch.floor(c) for c in coords)
# Search neighborhood (3^ndim cells)
min_dist = torch.full_like(coords[0], float('inf'))
def generate_offsets(d):
"""Generate all offsets for neighborhood search"""
if d == 0:
return [[]]
return [[i] + offset for i in [-1, 0, 1] for offset in generate_offsets(d - 1)]
offsets = generate_offsets(ndim)
for offset in offsets:
# Neighbor cell coordinates
neighbor_coords = tuple(coords_cell[i] + offset[i] for i in range(ndim))
# Hash-based feature point in neighbor cell
hash_vals = []
for seed_mult in range(ndim):
h = NoiseUtils._hash_nd(neighbor_coords, seed * (seed_mult + 2))
hash_vals.append(h * jitter)
# Feature point position (cell + jitter)
feature_pos = tuple(
offset[i] + hash_vals[i]
for i in range(ndim)
)
# Distance to feature point
dist_sq = sum((coords_frac[i] - feature_pos[i])**2 for i in range(ndim))
dist = torch.sqrt(dist_sq)
min_dist = torch.minimum(min_dist, dist)
return min_dist+0.5
@staticmethod
def plasma_noise_nd(coords_input, scale=1.0, seed=0, device=None):
"""
N-dimensional Plasma/Turbulence noise - high-frequency chaotic patterns.
Uses interpolated noise for smooth results.
coords_input: tuple of coordinate tensors
"""
ndim = len(coords_input)
result = torch.zeros_like(coords_input[0])
amplitude = 1.0
frequency = 1.0
max_amplitude = 0.0
for octave in range(4):
# Scale coordinates by frequency and base scale
scaled_coords = tuple((c / scale) * frequency for c in coords_input)
# Use Perlin-like interpolation for smooth plasma
noise_octave = NoiseUtils.perlin_noise_nd(
tuple((c / scale) * frequency for c in coords_input),
1.0, # scale already applied above
seed + octave * 1000,
device
)
# Add this octave
result = result + noise_octave * amplitude
max_amplitude += amplitude
# Update for next octave
amplitude *= 0.5
frequency *= 2.0
result = result / max_amplitude
return (result*2)+0.5