diff --git a/solid_noise/solid_noise.py b/solid_noise/solid_noise.py index 48fd4a3..82680f9 100644 --- a/solid_noise/solid_noise.py +++ b/solid_noise/solid_noise.py @@ -1,306 +1,147 @@ """ Solid Noise Generator — Krita Plugin Generates Perlin noise at canvas resolution with configurable octaves (1-16). - -Speed strategy: compile a tiny C shared library at first use via gcc + ctypes. -This gives near-native performance (~0.1s at 1080p, 4 octaves) while requiring -zero third-party Python packages. If gcc is unavailable we fall back to a -pure-Python path automatically. """ -import array -import ctypes import math -import os import random -import subprocess -import tempfile -from krita import (Krita, Extension, DockWidget, - DockWidgetFactory, DockWidgetFactoryBase) +from krita import DockWidget, DockWidgetFactory, DockWidgetFactoryBase, Extension, Krita +from PyQt5.QtCore import QObject, Qt, QThread, pyqtSignal from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QSlider, QSpinBox, QGroupBox, QProgressBar, QDoubleSpinBox, - QCheckBox, QFrame, + QCheckBox, + QDoubleSpinBox, + QFrame, + QGroupBox, + QHBoxLayout, + QLabel, + QProgressBar, + QPushButton, + QSlider, + QSpinBox, + QVBoxLayout, + QWidget, ) -from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject - # --------------------------------------------------------------------------- -# C source for the noise kernel +# Noise generation (Pure Python) # --------------------------------------------------------------------------- -_C_SOURCE = r""" -#include -#include -static int perm[512]; - -void sn_set_perm(int *p) { - memcpy(perm, p, 512 * sizeof(int)); -} - -static float fade(float t) { return t*t*t*(t*(t*6.0f-15.0f)+10.0f); } - -static float grad(int h, float x, float y) { - h &= 3; - if (h == 0) return x + y; - if (h == 1) return -x + y; - if (h == 2) return x - y; - return -x - y; -} - -/* Fill `out` (length=width) with one row of Perlin noise at height-coord ny. */ -void sn_row(float *out, int width, float ny, float scale) { - int yi = (int)floorf(ny) & 255; - float yf = ny - floorf(ny); - float yf1 = yf - 1.0f; - float v = fade(yf); - - float inv_w = scale / (float)width; - for (int x = 0; x < width; x++) { - float nx = x * inv_w; - int xi = (int)floorf(nx) & 255; - float xf = nx - floorf(nx); - float xf1 = xf - 1.0f; - float u = fade(xf); - - int aa = perm[perm[xi ] + yi ]; - int ab = perm[perm[xi ] + yi+1]; - int ba = perm[perm[xi+1] + yi ]; - int bb = perm[perm[xi+1] + yi+1]; - - float x1 = grad(aa,xf,yf) + u*(grad(ba,xf1,yf) - grad(aa,xf,yf)); - float x2 = grad(ab,xf,yf1) + u*(grad(bb,xf1,yf1) - grad(ab,xf,yf1)); - out[x] = x1 + v*(x2 - x1); - } -} - -/* - * Generate the full canvas (height rows x width cols) for one octave and - * ADD the result * amplitude into acc[]. - */ -void sn_octave(float *acc, int width, int height, - float scale, float amplitude) { - float inv_h = scale / (float)height; - float *row = (float *)__builtin_alloca(width * sizeof(float)); - for (int y = 0; y < height; y++) { - float ny = y * inv_h; - sn_row(row, width, ny, scale); - int base = y * width; - for (int x = 0; x < width; x++) - acc[base + x] += row[x] * amplitude; - } -} - -/* - * Normalise acc[], stretch so darkest=0 and brightest=255, pack as BGRA. - * max_amp = sum of all octave amplitudes. - * invert = 0 or 1. - */ -void sn_pack_bgra(float *acc, unsigned char *out, - int npixels, float max_amp, int invert) { - float inv = 1.0f / max_amp; - - /* First pass: normalise to [0,1] and find actual min/max. */ - float mn = 1.0f, mx = 0.0f; - for (int i = 0; i < npixels; i++) { - float v = (acc[i] * inv + 1.0f) * 0.5f; - if (v < 0.0f) v = 0.0f; - else if (v > 1.0f) v = 1.0f; - acc[i] = v; - if (v < mn) mn = v; - if (v > mx) mx = v; - } - - /* Second pass: remap [mn,mx] -> [0,255] and pack. */ - float range = mx - mn; - float scale = (range > 1e-6f) ? (255.0f / range) : 0.0f; - for (int i = 0; i < npixels; i++) { - float v = (acc[i] - mn) * scale; - if (invert) v = 255.0f - v; - unsigned char bv = (unsigned char)(v + 0.5f); - int base = i * 4; - out[base ] = bv; - out[base+1] = bv; - out[base+2] = bv; - out[base+3] = 255; - } -} -""" - -# --------------------------------------------------------------------------- -# Build / load the shared library (once per session) -# --------------------------------------------------------------------------- - -_clib = None # ctypes.CDLL or None -_clib_dir = None # tempfile.TemporaryDirectory kept alive - -def _try_build_clib(): - global _clib, _clib_dir - if _clib is not None: - return True - try: - _clib_dir = tempfile.TemporaryDirectory(prefix="krita_solid_noise_") - src_path = os.path.join(_clib_dir.name, "sn.c") - lib_path = os.path.join(_clib_dir.name, "sn.so") - with open(src_path, "w") as f: - f.write(_C_SOURCE) - result = subprocess.run( - ["gcc", "-O2", "-shared", "-fPIC", "-o", lib_path, src_path, "-lm"], - capture_output=True, timeout=30 - ) - if result.returncode != 0: - return False - lib = ctypes.CDLL(lib_path) - - lib.sn_set_perm.argtypes = [ctypes.POINTER(ctypes.c_int)] - lib.sn_set_perm.restype = None - - lib.sn_octave.argtypes = [ctypes.POINTER(ctypes.c_float), - ctypes.c_int, ctypes.c_int, - ctypes.c_float, ctypes.c_float] - lib.sn_octave.restype = None - - lib.sn_pack_bgra.argtypes = [ctypes.POINTER(ctypes.c_float), - ctypes.POINTER(ctypes.c_ubyte), - ctypes.c_int, ctypes.c_float, ctypes.c_int] - lib.sn_pack_bgra.restype = None - - _clib = lib - return True - except Exception: - return False - - -# --------------------------------------------------------------------------- -# Pure-Python fallback (used when gcc is unavailable) -# --------------------------------------------------------------------------- - -def _py_fade(t): +def _fade(t): return t * t * t * (t * (t * 6.0 - 15.0) + 10.0) -def _py_grad(h, x, y): + +def _grad(h, x, y): h &= 3 - if h == 0: return x + y - if h == 1: return -x + y - if h == 2: return x - y + if h == 0: + return x + y + if h == 1: + return -x + y + if h == 2: + return x - y return -x - y -def _py_generate(width, height, octaves, scale, seed, invert, progress_cb): + +def generate(width, height, octaves, scale, seed, invert, progress_cb): perms = [] for i in range(octaves): rng = random.Random(seed + i) - p = list(range(256)); rng.shuffle(p) + p = list(range(256)) + rng.shuffle(p) perms.append(p + p) - amps = [0.5 ** i for i in range(octaves)] - freqs = [2.0 ** i for i in range(octaves)] + amps = [0.5**i for i in range(octaves)] + freqs = [2.0**i for i in range(octaves)] max_a = sum(amps) - acc = [0.0] * (width * height) + acc = [0.0] * (width * height) for oi in range(octaves): - p = perms[oi]; amp = amps[oi]; fs = scale * freqs[oi] - inv_h = fs / height; inv_w = fs / width + p = perms[oi] + amp = amps[oi] + fs = scale * freqs[oi] + inv_h = fs / height + inv_w = fs / width for y in range(height): ny = y * inv_h nyi = int(math.floor(ny)) & 255 - yf = ny - math.floor(ny); yf1 = yf - 1.0; v = _py_fade(yf) + yf = ny - math.floor(ny) + yf1 = yf - 1.0 + v = _fade(yf) base = y * width for x in range(width): nx = x * inv_w xi = int(math.floor(nx)) & 255 - xf = nx - math.floor(nx); xf1 = xf - 1.0; u = _py_fade(xf) - aa=p[p[xi]+nyi]; ab=p[p[xi]+nyi+1] - ba=p[p[xi+1]+nyi]; bb=p[p[xi+1]+nyi+1] - x1 = _py_grad(aa,xf,yf) + u*(_py_grad(ba,xf1,yf) - _py_grad(aa,xf,yf)) - x2 = _py_grad(ab,xf,yf1) + u*(_py_grad(bb,xf1,yf1) - _py_grad(ab,xf,yf1)) - acc[base+x] += (x1 + v*(x2-x1)) * amp - progress_cb(int((oi+1)/octaves*90)) + xf = nx - math.floor(nx) + xf1 = xf - 1.0 + u = _fade(xf) + aa = p[p[xi] + nyi] + ab = p[p[xi] + nyi + 1] + ba = p[p[xi + 1] + nyi] + bb = p[p[xi + 1] + nyi + 1] + x1 = _grad(aa, xf, yf) + u * (_grad(ba, xf1, yf) - _grad(aa, xf, yf)) + x2 = _grad(ab, xf, yf1) + u * (_grad(bb, xf1, yf1) - _grad(ab, xf, yf1)) + acc[base + x] += (x1 + v * (x2 - x1)) * amp + progress_cb(int((oi + 1) / octaves * 90)) inv_norm = 1.0 / max_a # Normalise to [0,1] norm = [] for v in acc: v = (v * inv_norm + 1.0) * 0.5 - if v < 0.0: v = 0.0 - elif v > 1.0: v = 1.0 + if v < 0.0: + v = 0.0 + elif v > 1.0: + v = 1.0 norm.append(v) # Stretch so min→0 and max→1 - mn = min(norm); mx = max(norm) + mn = min(norm) + mx = max(norm) rng = mx - mn scale = (1.0 / rng) if rng > 1e-6 else 0.0 pixels = bytearray(width * height * 4) for i in range(width * height): v = (norm[i] - mn) * scale - if invert: v = 1.0 - v + if invert: + v = 1.0 - v bv = int(v * 255.0 + 0.5) b = i * 4 - pixels[b] = pixels[b+1] = pixels[b+2] = bv; pixels[b+3] = 255 + pixels[b] = pixels[b + 1] = pixels[b + 2] = bv + pixels[b + 3] = 255 progress_cb(100) return bytes(pixels) -# --------------------------------------------------------------------------- -# C-accelerated path -# --------------------------------------------------------------------------- - -def _c_generate(width, height, octaves, scale, seed, invert, progress_cb): - lib = _clib - npix = width * height - - acc_arr = (ctypes.c_float * npix)(*([0.0] * npix)) - bgra_arr = (ctypes.c_ubyte * (npix * 4))() - - amps = [0.5 ** i for i in range(octaves)] - freqs = [2.0 ** i for i in range(octaves)] - max_a = sum(amps) - - for oi in range(octaves): - rng = random.Random(seed + oi) - p = list(range(256)); rng.shuffle(p); p = p * 2 - perm_arr = (ctypes.c_int * 512)(*p) - lib.sn_set_perm(perm_arr) - lib.sn_octave(acc_arr, width, height, - ctypes.c_float(scale * freqs[oi]), - ctypes.c_float(amps[oi])) - progress_cb(int((oi + 1) / octaves * 90)) - - lib.sn_pack_bgra(acc_arr, bgra_arr, npix, - ctypes.c_float(max_a), ctypes.c_int(1 if invert else 0)) - progress_cb(100) - return bytes(bgra_arr) - - -def generate(width, height, octaves, scale, seed, invert, progress_cb): - if _try_build_clib(): - return _c_generate(width, height, octaves, scale, seed, invert, progress_cb) - return _py_generate(width, height, octaves, scale, seed, invert, progress_cb) - - # --------------------------------------------------------------------------- # Worker # --------------------------------------------------------------------------- + class NoiseWorker(QObject): progress = pyqtSignal(int) finished = pyqtSignal(bytes, int, int) - error = pyqtSignal(str) + error = pyqtSignal(str) def __init__(self, width, height, octaves, scale, seed, invert): super().__init__() - self.width = width - self.height = height + self.width = width + self.height = height self.octaves = octaves - self.scale = scale - self.seed = seed - self.invert = invert + self.scale = scale + self.seed = seed + self.invert = invert def run(self): try: - data = generate(self.width, self.height, self.octaves, - self.scale, self.seed, self.invert, - self.progress.emit) + data = generate( + self.width, + self.height, + self.octaves, + self.scale, + self.seed, + self.invert, + self.progress.emit, + ) self.finished.emit(data, self.width, self.height) except Exception as e: self.error.emit(str(e)) @@ -422,11 +263,14 @@ class SolidNoiseDock(DockWidget): self._set_busy(True) self._thread = QThread() - self._worker = NoiseWorker(w, h, - self._oct_slider.value(), - self._scale_spin.value(), - self._seed_spin.value(), - self._invert_chk.isChecked()) + self._worker = NoiseWorker( + w, + h, + self._oct_slider.value(), + self._scale_spin.value(), + self._seed_spin.value(), + self._invert_chk.isChecked(), + ) self._worker.moveToThread(self._thread) self._thread.started.connect(self._worker.run) self._worker.progress.connect(self._bar.setValue) @@ -451,10 +295,7 @@ class SolidNoiseDock(DockWidget): layer = doc.activeNode() layer.setPixelData(data, 0, 0, w, h) doc.refreshProjection() - accel = "C" if _clib else "Python" - self._status.setText( - f"Done {w}x{h}, {self._oct_slider.value()} oct [{accel}]" - ) + self._status.setText(f"Done {w}x{h}, {self._oct_slider.value()} oct") except Exception as e: self._on_error(str(e)) finally: @@ -472,6 +313,7 @@ class SolidNoiseDock(DockWidget): # Extension + docker registration # --------------------------------------------------------------------------- + class SolidNoisePlugin(Extension): def __init__(self, parent=None): super().__init__(parent)