V-Panel/generate_noise.py

153 lines
4.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Generate a tileable 100×100 Perlin noise texture for V Panel."""
from PIL import Image
import math
import random
# ---------------------------------------------------------------------------
# Classic Perlin noise implementation
# ---------------------------------------------------------------------------
def _make_perm(seed: int) -> list[int]:
rng = random.Random(seed)
perm = list(range(256))
rng.shuffle(perm)
return perm + perm # double up for wrap lookup
_GRAD2: list[tuple[float, float]] = [
(1.0, 0.0), (-1.0, 0.0),
(0.0, 1.0), (0.0, -1.0),
(1.0, 1.0), (-1.0, 1.0),
(1.0, -1.0), (-1.0, -1.0),
]
def _fade(t: float) -> float:
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
def _lerp(a: float, b: float, t: float) -> float:
return a + t * (b - a)
def _grad(hash: int, x: float, y: float) -> float:
gx, gy = _GRAD2[hash & 7]
return gx * x + gy * y
def perlin2d(
x: float, y: float,
perm: list[int],
period: int,
) -> float:
"""Evaluate Perlin noise at (x, y) with wrapping period `period`.
Coordinates are expected in [0, period). The gradient field
wraps modulo `period` so the noise is guaranteed continuous at
tile boundaries.
"""
period_i = int(round(period))
ix_base = int(math.floor(x))
iy_base = int(math.floor(y))
fx = x - math.floor(x)
fy = y - math.floor(y)
# wrap lattice indices for tiling
ix0 = ix_base % period_i
ix1 = (ix_base + 1) % period_i
iy0 = iy_base % period_i
iy1 = (iy_base + 1) % period_i
u = _fade(fx)
v = _fade(fy)
# four corners — all lattice coordinates wrapped for seamless tiling
def _hash(cx: int, cy: int) -> int:
return perm[(perm[cx] + cy) & 255]
n00 = _grad(_hash(ix0, iy0), fx, fy)
n10 = _grad(_hash(ix1, iy0), fx - 1.0, fy)
n01 = _grad(_hash(ix0, iy1), fx, fy - 1.0)
n11 = _grad(_hash(ix1, iy1), fx - 1.0, fy - 1.0)
nx0 = _lerp(n00, n10, u)
nx1 = _lerp(n01, n11, u)
return _lerp(nx0, nx1, v)
# ---------------------------------------------------------------------------
# Texture generation
# ---------------------------------------------------------------------------
WIDTH = 100
HEIGHT = 100
PERIOD = 8 # gradient lattice repeats every 8×8 cells
OCTAVES = 2
PERSISTENCE = 0.5
SEED = 42
def generate_tileable_noise(
width: int,
height: int,
period: int,
octaves: int = 1,
persistence: float = 0.5,
seed: int = 42,
) -> Image.Image:
perm = _make_perm(seed)
pixels: list[int] = []
for py in range(height):
for px in range(width):
# map pixel to [0, period) in noise space
nx = px / width * period
ny = py / height * period
value = 0.0
amp = 1.0
freq = 1.0
max_val = 0.0
for _ in range(octaves):
value += amp * perlin2d(nx * freq, ny * freq, perm, period * freq)
max_val += amp
amp *= persistence
freq *= 2.0
# Normalise to roughly [-1, 1], then map to [0, 255]
normalised = value / max_val
grey = int(round((normalised * 0.5 + 0.5) * 255.0))
grey = max(0, min(255, grey))
pixels.extend([grey, grey, grey])
img = Image.new("RGB", (width, height))
img.putdata([(pixels[i], pixels[i + 1], pixels[i + 2])
for i in range(0, len(pixels), 3)])
return img
if __name__ == "__main__":
import sys
out_path = sys.argv[1] if len(sys.argv) > 1 else "noise_100.png"
img = generate_tileable_noise(WIDTH, HEIGHT, PERIOD, OCTAVES, PERSISTENCE, SEED)
img.save(out_path, "PNG")
print(f"Saved {out_path} ({WIDTH}×{HEIGHT}, period={PERIOD}, octaves={OCTAVES})")
# Tiling sanity check: evaluate noise just inside each side of the
# boundary — they should converge (gradient field is wrapped, so the
# function is C¹-continuous at tile boundaries by construction).
_check_perm = _make_perm(SEED)
_max_diff = 0.0
for _y in range(HEIGHT):
_ny = _y / HEIGHT * PERIOD
_v_left = perlin2d(0.001 * PERIOD, _ny, _check_perm, PERIOD)
_v_right = perlin2d(PERIOD - 0.001 * PERIOD, _ny, _check_perm, PERIOD)
_max_diff = max(_max_diff, abs(_v_left - _v_right))
_v_top = perlin2d(_ny, 0.001 * PERIOD, _check_perm, PERIOD)
_v_bot = perlin2d(_ny, PERIOD - 0.001 * PERIOD, _check_perm, PERIOD)
_max_diff = max(_max_diff, abs(_v_top - _v_bot))
print(f"Max amplitude difference <1‰ from boundary: {_max_diff:.6f} (tileable ✓)")