replace noise with tileable perlin; use pixel-space sampling for fixed scale
This commit is contained in:
parent
9adba9d6c2
commit
d636d0f847
4 changed files with 170 additions and 1 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 7.4 KiB |
153
generate_noise.py
Normal file
153
generate_noise.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/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 ✓)")
|
||||
|
|
@ -31,9 +31,20 @@ func get_module_title() -> String:
|
|||
func _initialize() -> void:
|
||||
_initialized = true
|
||||
initialize()
|
||||
resized.connect(_update_noise_size)
|
||||
_update_noise_size()
|
||||
module_ready.emit()
|
||||
|
||||
|
||||
func _update_noise_size() -> void:
|
||||
var vial: ColorRect = find_child("VialFill", true, false) as ColorRect
|
||||
if vial == null:
|
||||
return
|
||||
var mat := vial.material as ShaderMaterial
|
||||
if mat and size.x > 0 and size.y > 0:
|
||||
mat.set_shader_parameter("noise_module_size", size)
|
||||
|
||||
|
||||
func _set_mouse_ignore(node: Node) -> void:
|
||||
if node is Control:
|
||||
node.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ uniform float edge_pulse : hint_range(0.0, 2.0) = 1.0;
|
|||
// --- Distortion ---
|
||||
uniform sampler2D noise_tex : repeat_enable;
|
||||
uniform float noise_scale : hint_range(0.0, 5.0) = 1.0;
|
||||
uniform vec2 noise_module_size = vec2(300.0, 240.0);
|
||||
uniform float swirl_strength : hint_range(-2.0, 2.0) = 0.5;
|
||||
uniform float hue_shift_speed : hint_range(0.0, 5.0) = 0.0;
|
||||
|
||||
|
|
@ -94,7 +95,11 @@ void fragment() {
|
|||
|
||||
// --- build effect colour for the liquid region ---
|
||||
vec2 uv = UV;
|
||||
float noise_val = texture(noise_tex, uv * noise_scale + t * 0.05).r;
|
||||
// Pixel-space noise coordinate: same pixel distance = same noise delta,
|
||||
// keeping the pattern fixed-size regardless of module aspect or window size.
|
||||
// The 100.0 constant matches the noise texture size (100×100).
|
||||
vec2 noise_uv = uv * noise_module_size / 100.0 * noise_scale + t * 0.05;
|
||||
float noise_val = texture(noise_tex, noise_uv).r;
|
||||
|
||||
// wave distortion
|
||||
uv.y += sin(uv.x * 10.0 + t) * wave_strength * noise_val;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue