AI: add plasma, voronoi and perlin noise

This commit is contained in:
mcDandy
2026-02-18 00:49:25 +01:00
parent 412ea5e89b
commit 891634a912
10 changed files with 2010 additions and 1266 deletions
+7 -1
View File
@@ -235,7 +235,10 @@ funcNoise:
| GUMBELDIST LPAREN expr COMMA expr COMMA expr (COMMA expr)? RPAREN # GumbelDistFunc
| WEIBULLDIST LPAREN expr COMMA expr COMMA expr (COMMA expr)? RPAREN # WeibullDistFunc
| CHI2DIST LPAREN expr COMMA expr (COMMA expr)? RPAREN # Chi2DistFunc
| STUDENTTDIST LPAREN expr COMMA expr (COMMA expr)? RPAREN # StudentTDistFunc;
| STUDENTTDIST LPAREN expr COMMA expr (COMMA expr)? RPAREN # StudentTDistFunc
| PERLIN LPAREN expr COMMA expr (COMMA expr)? (COMMA expr)? (COMMA expr)? RPAREN # PerlinFunc
| CELLULAR LPAREN expr COMMA expr (COMMA expr)? (COMMA expr)? (COMMA expr)? RPAREN # CellularFunc
| PLASMA LPAREN expr COMMA expr (COMMA expr)? (COMMA expr)? (COMMA expr)? RPAREN # PlasmaFunc;
// LEXER RULES
@@ -380,6 +383,9 @@ GUMBELDIST: 'randgumbel' | 'random_gumbel';
WEIBULLDIST: 'randw' | 'random_weibull';
CHI2DIST: 'randchi2' | 'random_chi2';
STUDENTTDIST: 'randt' | 'random_studentt';
PERLIN: 'perlin' | 'perlin_noise';
CELLULAR: 'cellular' | 'voronoi' | 'worley' | 'cellular_noise';
PLASMA: 'plasma' | 'turbulence' | 'plasma_noise';
PLUS: '+';
MINUS: '-';
File diff suppressed because one or more lines are too long
+63 -60
View File
@@ -137,39 +137,42 @@ GUMBELDIST=136
WEIBULLDIST=137
CHI2DIST=138
STUDENTTDIST=139
PLUS=140
MINUS=141
MULT=142
DIV=143
MOD=144
POW=145
LSHIFT=146
RSHIFT=147
GE=148
GT=149
LE=150
LT=151
EQ=152
EQUEALS=153
NE=154
PIPE=155
LPAREN=156
RPAREN=157
COMMA=158
SEMICOLON=159
ARROW=160
LBRACKET=161
RBRACKET=162
QUESTION=163
COLON=164
LBRACE=165
RBRACE=166
NUMBER=167
CONSTANT=168
VARIABLE=169
SL_COMMENT=170
ML_COMMENT=171
WS=172
PERLIN=140
CELLULAR=141
PLASMA=142
PLUS=143
MINUS=144
MULT=145
DIV=146
MOD=147
POW=148
LSHIFT=149
RSHIFT=150
GE=151
GT=152
LE=153
LT=154
EQ=155
EQUEALS=156
NE=157
PIPE=158
LPAREN=159
RPAREN=160
COMMA=161
SEMICOLON=162
ARROW=163
LBRACKET=164
RBRACKET=165
QUESTION=166
COLON=167
LBRACE=168
RBRACE=169
NUMBER=170
CONSTANT=171
VARIABLE=172
SL_COMMENT=173
ML_COMMENT=174
WS=175
'sin'=1
'cos'=2
'tan'=3
@@ -272,30 +275,30 @@ WS=172
'cov'=123
'crop'=124
'none'=125
'+'=140
'-'=141
'*'=142
'/'=143
'%'=144
'^'=145
'<<'=146
'>>'=147
'>='=148
'>'=149
'<='=150
'<'=151
'=='=152
'='=153
'!='=154
'|'=155
'('=156
')'=157
','=158
';'=159
'->'=160
'['=161
']'=162
'?'=163
':'=164
'{'=165
'}'=166
'+'=143
'-'=144
'*'=145
'/'=146
'%'=147
'^'=148
'<<'=149
'>>'=150
'>='=151
'>'=152
'<='=153
'<'=154
'=='=155
'='=156
'!='=157
'|'=158
'('=159
')'=160
','=161
';'=162
'->'=163
'['=164
']'=165
'?'=166
':'=167
'{'=168
'}'=169
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+63 -60
View File
@@ -137,39 +137,42 @@ GUMBELDIST=136
WEIBULLDIST=137
CHI2DIST=138
STUDENTTDIST=139
PLUS=140
MINUS=141
MULT=142
DIV=143
MOD=144
POW=145
LSHIFT=146
RSHIFT=147
GE=148
GT=149
LE=150
LT=151
EQ=152
EQUEALS=153
NE=154
PIPE=155
LPAREN=156
RPAREN=157
COMMA=158
SEMICOLON=159
ARROW=160
LBRACKET=161
RBRACKET=162
QUESTION=163
COLON=164
LBRACE=165
RBRACE=166
NUMBER=167
CONSTANT=168
VARIABLE=169
SL_COMMENT=170
ML_COMMENT=171
WS=172
PERLIN=140
CELLULAR=141
PLASMA=142
PLUS=143
MINUS=144
MULT=145
DIV=146
MOD=147
POW=148
LSHIFT=149
RSHIFT=150
GE=151
GT=152
LE=153
LT=154
EQ=155
EQUEALS=156
NE=157
PIPE=158
LPAREN=159
RPAREN=160
COMMA=161
SEMICOLON=162
ARROW=163
LBRACKET=164
RBRACKET=165
QUESTION=166
COLON=167
LBRACE=168
RBRACE=169
NUMBER=170
CONSTANT=171
VARIABLE=172
SL_COMMENT=173
ML_COMMENT=174
WS=175
'sin'=1
'cos'=2
'tan'=3
@@ -272,30 +275,30 @@ WS=172
'cov'=123
'crop'=124
'none'=125
'+'=140
'-'=141
'*'=142
'/'=143
'%'=144
'^'=145
'<<'=146
'>>'=147
'>='=148
'>'=149
'<='=150
'<'=151
'=='=152
'='=153
'!='=154
'|'=155
'('=156
')'=157
','=158
';'=159
'->'=160
'['=161
']'=162
'?'=163
':'=164
'{'=165
'}'=166
'+'=143
'-'=144
'*'=145
'/'=146
'%'=147
'^'=148
'<<'=149
'>>'=150
'>='=151
'>'=152
'<='=153
'<'=154
'=='=155
'='=156
'!='=157
'|'=158
'('=159
')'=160
','=161
';'=162
'->'=163
'['=164
']'=165
'?'=166
':'=167
'{'=168
'}'=169
File diff suppressed because it is too large Load Diff
+15
View File
@@ -979,5 +979,20 @@ class MathExprVisitor(ParseTreeVisitor):
return self.visitChildren(ctx)
# Visit a parse tree produced by MathExprParser#PerlinFunc.
def visitPerlinFunc(self, ctx:MathExprParser.PerlinFuncContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by MathExprParser#CellularFunc.
def visitCellularFunc(self, ctx:MathExprParser.CellularFuncContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by MathExprParser#PlasmaFunc.
def visitPlasmaFunc(self, ctx:MathExprParser.PlasmaFuncContext):
return self.visitChildren(ctx)
del MathExprParser
+204
View File
@@ -2565,3 +2565,207 @@ class UnifiedMathVisitor(MathExprVisitor):
# Fallback for other types
return int(a) >> b_int
def visitPerlinFunc(self, ctx):
"""perlin(seed, scale, [octaves], [offset], [shape])
Perlin noise with smooth gradients - supports arbitrary dimensions.
"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
octaves = 1
expr_idx = 2
if len(ctx.expr()) > expr_idx:
oct_val = yield ctx.expr(expr_idx)
octaves = int(oct_val.item()) if self._is_tensor(oct_val) else int(oct_val)
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
# Optional shape parameter
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
grids = torch.meshgrid(
*[
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
],
indexing='ij'
)
noise = NoiseUtils.perlin_noise_nd(grids, scale, seed, self.device)
if octaves > 1:
result = noise
amplitude = 0.5
frequency = 2.0
for oct in range(octaves - 1):
scaled_grids = tuple(g * frequency for g in grids)
octave_noise = NoiseUtils.perlin_noise_nd(scaled_grids, scale / frequency, seed + oct, self.device)
result = result + octave_noise * amplitude
amplitude *= 0.5
frequency *= 2.0
noise = result / (2 - 2**(-octaves))
return noise
def visitCellularFunc(self, ctx):
"""cellular(seed, scale, [jitter], [offset], [shape])
Cellular/Voronoi noise - supports arbitrary dimensions.
"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
jitter = 0.5
expr_idx = 2
if len(ctx.expr()) > expr_idx:
jitter_val = yield ctx.expr(expr_idx)
jitter = float(jitter_val.item()) if self._is_tensor(jitter_val) else float(jitter_val)
jitter = max(0.0, min(1.0, jitter))
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
# Optional shape parameter
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
grids = torch.meshgrid(
*[
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
],
indexing='ij'
)
noise = NoiseUtils.cellular_noise_nd(grids, scale, jitter, seed, self.device)
return noise
def visitPlasmaFunc(self, ctx):
"""plasma(seed, scale, [octaves], [offset], [shape])
Plasma/Turbulence noise - chaotic high-frequency patterns.
"""
seed_val = yield ctx.expr(0)
seed = int(seed_val.item()) if self._is_tensor(seed_val) else int(seed_val)
scale_val = yield ctx.expr(1)
scale = float(scale_val.item()) if self._is_tensor(scale_val) else float(scale_val)
octaves = 1
expr_idx = 2
if len(ctx.expr()) > expr_idx:
oct_val = yield ctx.expr(expr_idx)
octaves = int(oct_val.item()) if self._is_tensor(oct_val) else int(oct_val)
expr_idx += 1
offset = None
if len(ctx.expr()) > expr_idx:
offset_val = yield ctx.expr(expr_idx)
offset = offset_val
expr_idx += 1
# Optional shape parameter
shape = self.shape
if len(ctx.expr()) > expr_idx:
shape_arg = (yield ctx.expr(expr_idx))
if self._is_tensor(shape_arg):
shape = tuple(shape_arg.long().flatten().tolist())
elif self._is_list(shape_arg):
shape = tuple(int(x) for x in shape_arg)
else:
shape = (int(shape_arg),)
if len(shape) == 0:
return torch.tensor(0.0, device=self.device)
offset_list = None
if offset is not None:
if self._is_tensor(offset):
offset_list = [float(x) for x in offset.flatten().tolist()]
elif self._is_list(offset):
offset_list = [float(x) for x in offset]
else:
offset_list = [float(offset)]
grids = torch.meshgrid(
*[
torch.arange(s, dtype=torch.float32, device=self.device)
+ (offset_list[i] if offset_list is not None and i < len(offset_list) else 0.0)
for i, s in enumerate(shape)
],
indexing='ij'
)
# Call perlin_noise_nd with all coordinate grids
noise = NoiseUtils.plasma_noise_nd(grids, scale, seed, self.device)
# Apply octaves (fBm-like composition)
if octaves > 1:
result = noise
amplitude = 0.5
frequency = 2.0
for oct in range(octaves - 1):
scaled_grids = tuple(g * frequency for g in grids)
octave_noise = NoiseUtils.plasma_noise_nd(scaled_grids, scale / frequency, seed + oct, self.device)
result = result + octave_noise * amplitude
amplitude *= 0.5
frequency *= 2.0
noise = result / (2 - 2**(-octaves))
return noise
+195
View File
@@ -0,0 +1,195 @@
"""
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