prepairing for pivot to standalone rust program
This commit is contained in:
@@ -16,6 +16,7 @@ The `tests/` directory remains prototype-only reference code and is not imported
|
|||||||
## Installing in Krita
|
## Installing in Krita
|
||||||
|
|
||||||
Copy `pykrita/advanced_erosion_generator.desktop` and the `pykrita/advanced_erosion_generator/` folder into Krita's `pykrita` directory.
|
Copy `pykrita/advanced_erosion_generator.desktop` and the `pykrita/advanced_erosion_generator/` folder into Krita's `pykrita` directory.
|
||||||
|
The current plugin implementation is pure Python and does not depend on `numpy` or the test virtual environment.
|
||||||
|
|
||||||
## Current UI
|
## Current UI
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Service
|
|
||||||
ServiceTypes=Krita/PythonPlugin
|
|
||||||
X-KDE-Library=advanced_erosion_generator
|
|
||||||
X-Python-2-Compatible=false
|
|
||||||
Name=Advanced Erosion Generator
|
|
||||||
Comment=Generate erosion-based terrain heightmaps at the active canvas resolution.
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from krita import Krita
|
|
||||||
|
|
||||||
from .extension import AdvancedErosionGeneratorExtension
|
|
||||||
|
|
||||||
|
|
||||||
app = Krita.instance()
|
|
||||||
extension = AdvancedErosionGeneratorExtension(parent=app)
|
|
||||||
app.addExtension(extension)
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
import random
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from krita import Extension, Krita
|
|
||||||
from PyQt5.QtCore import QByteArray, Qt
|
|
||||||
from PyQt5.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QDialog,
|
|
||||||
QDialogButtonBox,
|
|
||||||
QDoubleSpinBox,
|
|
||||||
QFormLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QMessageBox,
|
|
||||||
QPushButton,
|
|
||||||
QSlider,
|
|
||||||
QSpinBox,
|
|
||||||
QToolButton,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .generator import GenerationSettings, generate_heightmap, heightmap_to_pixel_data
|
|
||||||
|
|
||||||
|
|
||||||
class FloatSliderInput(QWidget):
|
|
||||||
def __init__(self, minimum, maximum, step, value, decimals=2, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._factor = 10 ** decimals
|
|
||||||
|
|
||||||
self.slider = QSlider(Qt.Horizontal, self)
|
|
||||||
self.slider.setRange(int(round(minimum * self._factor)), int(round(maximum * self._factor)))
|
|
||||||
self.slider.setSingleStep(max(1, int(round(step * self._factor))))
|
|
||||||
self.slider.setPageStep(max(1, int(round(step * self._factor * 5))))
|
|
||||||
|
|
||||||
self.spin = QDoubleSpinBox(self)
|
|
||||||
self.spin.setRange(minimum, maximum)
|
|
||||||
self.spin.setDecimals(decimals)
|
|
||||||
self.spin.setSingleStep(step)
|
|
||||||
|
|
||||||
layout = QHBoxLayout()
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
layout.addWidget(self.slider, 1)
|
|
||||||
layout.addWidget(self.spin)
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.slider.valueChanged.connect(self._sync_from_slider)
|
|
||||||
self.spin.valueChanged.connect(self._sync_from_spin)
|
|
||||||
self.set_value(value)
|
|
||||||
|
|
||||||
def _sync_from_slider(self, slider_value):
|
|
||||||
spin_value = slider_value / self._factor
|
|
||||||
if abs(self.spin.value() - spin_value) > (0.5 / self._factor):
|
|
||||||
self.spin.blockSignals(True)
|
|
||||||
self.spin.setValue(spin_value)
|
|
||||||
self.spin.blockSignals(False)
|
|
||||||
|
|
||||||
def _sync_from_spin(self, spin_value):
|
|
||||||
slider_value = int(round(spin_value * self._factor))
|
|
||||||
if self.slider.value() != slider_value:
|
|
||||||
self.slider.blockSignals(True)
|
|
||||||
self.slider.setValue(slider_value)
|
|
||||||
self.slider.blockSignals(False)
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.spin.setValue(value)
|
|
||||||
self.slider.setValue(int(round(value * self._factor)))
|
|
||||||
|
|
||||||
def value(self):
|
|
||||||
return self.spin.value()
|
|
||||||
|
|
||||||
|
|
||||||
class IntSliderInput(QWidget):
|
|
||||||
def __init__(self, minimum, maximum, step, value, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.slider = QSlider(Qt.Horizontal, self)
|
|
||||||
self.slider.setRange(minimum, maximum)
|
|
||||||
self.slider.setSingleStep(step)
|
|
||||||
self.slider.setPageStep(max(step, step * 2))
|
|
||||||
|
|
||||||
self.spin = QSpinBox(self)
|
|
||||||
self.spin.setRange(minimum, maximum)
|
|
||||||
self.spin.setSingleStep(step)
|
|
||||||
|
|
||||||
layout = QHBoxLayout()
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
layout.addWidget(self.slider, 1)
|
|
||||||
layout.addWidget(self.spin)
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.slider.valueChanged.connect(self._sync_from_slider)
|
|
||||||
self.spin.valueChanged.connect(self._sync_from_spin)
|
|
||||||
self.set_value(value)
|
|
||||||
|
|
||||||
def _sync_from_slider(self, slider_value):
|
|
||||||
if self.spin.value() != slider_value:
|
|
||||||
self.spin.blockSignals(True)
|
|
||||||
self.spin.setValue(slider_value)
|
|
||||||
self.spin.blockSignals(False)
|
|
||||||
|
|
||||||
def _sync_from_spin(self, spin_value):
|
|
||||||
if self.slider.value() != spin_value:
|
|
||||||
self.slider.blockSignals(True)
|
|
||||||
self.slider.setValue(spin_value)
|
|
||||||
self.slider.blockSignals(False)
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.spin.setValue(value)
|
|
||||||
self.slider.setValue(value)
|
|
||||||
|
|
||||||
def value(self):
|
|
||||||
return self.spin.value()
|
|
||||||
|
|
||||||
|
|
||||||
class CollapsibleSection(QWidget):
|
|
||||||
def __init__(self, title, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.button = QToolButton(self)
|
|
||||||
self.button.setText(title)
|
|
||||||
self.button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
|
||||||
self.button.setArrowType(Qt.RightArrow)
|
|
||||||
self.button.setCheckable(True)
|
|
||||||
self.button.setChecked(False)
|
|
||||||
|
|
||||||
self.content = QWidget(self)
|
|
||||||
self.content.setVisible(False)
|
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
layout.addWidget(self.button)
|
|
||||||
layout.addWidget(self.content)
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.button.toggled.connect(self._toggle)
|
|
||||||
|
|
||||||
def _toggle(self, expanded):
|
|
||||||
self.button.setArrowType(Qt.DownArrow if expanded else Qt.RightArrow)
|
|
||||||
self.content.setVisible(expanded)
|
|
||||||
|
|
||||||
def set_content_layout(self, layout):
|
|
||||||
self.content.setLayout(layout)
|
|
||||||
|
|
||||||
|
|
||||||
class TerrainDialog(QDialog):
|
|
||||||
def __init__(self, document, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.document = document
|
|
||||||
|
|
||||||
self.setWindowTitle("Advanced Erosion Generator")
|
|
||||||
self.setModal(True)
|
|
||||||
self.resize(560, 320)
|
|
||||||
|
|
||||||
main_layout = QVBoxLayout()
|
|
||||||
|
|
||||||
doc_label = QLabel(
|
|
||||||
f"Output size: {self.document.width()} x {self.document.height()} px",
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
main_layout.addWidget(doc_label)
|
|
||||||
|
|
||||||
form_layout = QFormLayout()
|
|
||||||
|
|
||||||
self.generation_scale = FloatSliderInput(0.1, 10.0, 0.1, 1.0, decimals=2, parent=self)
|
|
||||||
self.erosion_scale = FloatSliderInput(0.08, 0.25, 0.01, 0.15, decimals=2, parent=self)
|
|
||||||
|
|
||||||
self.seed_input = QSpinBox(self)
|
|
||||||
self.seed_input.setRange(0, 2147483647)
|
|
||||||
self.seed_input.setValue(0)
|
|
||||||
|
|
||||||
seed_row = QWidget(self)
|
|
||||||
seed_layout = QHBoxLayout()
|
|
||||||
seed_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
seed_layout.addWidget(self.seed_input, 1)
|
|
||||||
|
|
||||||
self.randomize_seed_button = QPushButton("Randomize", self)
|
|
||||||
self.randomize_seed_button.clicked.connect(self._randomize_seed)
|
|
||||||
seed_layout.addWidget(self.randomize_seed_button)
|
|
||||||
seed_row.setLayout(seed_layout)
|
|
||||||
|
|
||||||
form_layout.addRow("Generation Scale", self.generation_scale)
|
|
||||||
form_layout.addRow("Erosion Scale", self.erosion_scale)
|
|
||||||
form_layout.addRow("Random Seed", seed_row)
|
|
||||||
|
|
||||||
main_layout.addLayout(form_layout)
|
|
||||||
|
|
||||||
advanced_section = CollapsibleSection("Advanced", self)
|
|
||||||
advanced_layout = QFormLayout()
|
|
||||||
advanced_layout.setContentsMargins(12, 0, 0, 0)
|
|
||||||
|
|
||||||
self.erosion_strength = FloatSliderInput(0.01, 0.10, 0.01, 0.05, decimals=2, parent=self)
|
|
||||||
self.erosion_gully_weight = FloatSliderInput(0.0, 1.0, 0.01, 0.5, decimals=2, parent=self)
|
|
||||||
self.erosion_detail = FloatSliderInput(0.7, 3.0, 0.1, 1.5, decimals=2, parent=self)
|
|
||||||
self.erosion_octaves = IntSliderInput(1, 8, 1, 5, parent=self)
|
|
||||||
|
|
||||||
advanced_layout.addRow("Strength", self.erosion_strength)
|
|
||||||
advanced_layout.addRow("Gully Weight", self.erosion_gully_weight)
|
|
||||||
advanced_layout.addRow("Detail", self.erosion_detail)
|
|
||||||
advanced_layout.addRow("Octaves", self.erosion_octaves)
|
|
||||||
advanced_section.set_content_layout(advanced_layout)
|
|
||||||
main_layout.addWidget(advanced_section)
|
|
||||||
|
|
||||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
|
|
||||||
button_box.accepted.connect(self.accept)
|
|
||||||
button_box.rejected.connect(self.reject)
|
|
||||||
main_layout.addWidget(button_box)
|
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
|
||||||
|
|
||||||
def _randomize_seed(self):
|
|
||||||
self.seed_input.setValue(random.randint(0, 2147483647))
|
|
||||||
|
|
||||||
def settings(self):
|
|
||||||
return GenerationSettings(
|
|
||||||
generation_scale=self.generation_scale.value(),
|
|
||||||
erosion_scale=self.erosion_scale.value(),
|
|
||||||
random_seed=self.seed_input.value(),
|
|
||||||
erosion_strength=self.erosion_strength.value(),
|
|
||||||
erosion_gully_weight=self.erosion_gully_weight.value(),
|
|
||||||
erosion_detail=self.erosion_detail.value(),
|
|
||||||
erosion_octaves=self.erosion_octaves.value(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AdvancedErosionGeneratorExtension(Extension):
|
|
||||||
def __init__(self, parent):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.action = None
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def createActions(self, window):
|
|
||||||
self.action = window.createAction(
|
|
||||||
"advanced_erosion_generator",
|
|
||||||
"Generate Advanced Erosion Terrain",
|
|
||||||
"tools/scripts",
|
|
||||||
)
|
|
||||||
self.action.triggered.connect(self.open_dialog)
|
|
||||||
|
|
||||||
def open_dialog(self):
|
|
||||||
document = Krita.instance().activeDocument()
|
|
||||||
if document is None:
|
|
||||||
QMessageBox.warning(
|
|
||||||
None,
|
|
||||||
"Advanced Erosion Generator",
|
|
||||||
"Open or create a document before generating terrain.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
parent_window = None
|
|
||||||
active_window = Krita.instance().activeWindow()
|
|
||||||
if active_window is not None:
|
|
||||||
parent_window = active_window.qwindow()
|
|
||||||
|
|
||||||
dialog = TerrainDialog(document, parent=parent_window)
|
|
||||||
if dialog.exec_() != QDialog.Accepted:
|
|
||||||
return
|
|
||||||
|
|
||||||
settings = dialog.settings()
|
|
||||||
self._generate_into_document(document, settings)
|
|
||||||
|
|
||||||
def _generate_into_document(self, document, settings):
|
|
||||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
||||||
try:
|
|
||||||
width = int(document.width())
|
|
||||||
height = int(document.height())
|
|
||||||
heightmap = generate_heightmap(width, height, settings)
|
|
||||||
pixel_data = heightmap_to_pixel_data(
|
|
||||||
heightmap, document.colorModel(), document.colorDepth()
|
|
||||||
)
|
|
||||||
|
|
||||||
layer_name = f"Eroded Terrain {settings.random_seed}"
|
|
||||||
layer = document.createNode(layer_name, "paintlayer")
|
|
||||||
document.rootNode().addChildNode(layer, None)
|
|
||||||
layer.setPixelData(QByteArray(pixel_data), 0, 0, width, height)
|
|
||||||
document.refreshProjection()
|
|
||||||
except Exception as exc:
|
|
||||||
QMessageBox.critical(
|
|
||||||
None,
|
|
||||||
"Advanced Erosion Generator",
|
|
||||||
f"Terrain generation failed:\n{exc}\n\n{traceback.format_exc()}",
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
QApplication.restoreOverrideCursor()
|
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
try:
|
|
||||||
from numba import njit, prange
|
|
||||||
except ImportError:
|
|
||||||
def njit(*args, **kwargs):
|
|
||||||
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
|
||||||
return args[0]
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
prange = range
|
|
||||||
|
|
||||||
|
|
||||||
TAU = np.float32(6.28318530717959)
|
|
||||||
|
|
||||||
EROSION_LACUNARITY = np.float32(2.0)
|
|
||||||
EROSION_GAIN = np.float32(0.5)
|
|
||||||
EROSION_CELL_SCALE = np.float32(0.7)
|
|
||||||
EROSION_NORMALIZATION = np.float32(0.5)
|
|
||||||
EROSION_ROUNDING = np.array([0.1, 0.0, 0.1, 2.0], dtype=np.float32)
|
|
||||||
EROSION_ONSET = np.array([1.25, 1.25, 2.8, 1.5], dtype=np.float32)
|
|
||||||
EROSION_ASSUMED_SLOPE = np.array([0.7, 1.0], dtype=np.float32)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class GenerationSettings:
|
|
||||||
generation_scale: float = 1.0
|
|
||||||
erosion_scale: float = 0.15
|
|
||||||
random_seed: int = 0
|
|
||||||
erosion_strength: float = 0.05
|
|
||||||
erosion_gully_weight: float = 0.5
|
|
||||||
erosion_detail: float = 1.5
|
|
||||||
erosion_octaves: int = 5
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def fract(x):
|
|
||||||
return x - np.floor(x)
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def mix(a, b, t):
|
|
||||||
return a + (b - a) * np.float32(t)
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def clamp(x, lo, hi):
|
|
||||||
return min(max(x, np.float32(lo)), np.float32(hi))
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def clamp01(x):
|
|
||||||
return clamp(x, 0.0, 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def hash22(p, random_seed):
|
|
||||||
seed_x = float(random_seed) * 0.7548776662466927
|
|
||||||
seed_y = float(random_seed) * 0.5698402909980532
|
|
||||||
kx, ky = 0.3183099, 0.3678794
|
|
||||||
px = (float(p[0]) + seed_x) * kx + ky
|
|
||||||
py = (float(p[1]) + seed_y) * ky + kx
|
|
||||||
|
|
||||||
val = px * py * (px + py)
|
|
||||||
f_val = val - np.floor(val)
|
|
||||||
|
|
||||||
rx = -1.0 + 2.0 * fract(16.0 * kx * f_val)
|
|
||||||
ry = -1.0 + 2.0 * fract(16.0 * ky * f_val)
|
|
||||||
return np.array([rx, ry], dtype=np.float32)
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def noised(p, random_seed):
|
|
||||||
i = np.floor(p)
|
|
||||||
f = (p - i).astype(np.float32)
|
|
||||||
|
|
||||||
u = f * f * f * (f * (f * np.float32(6.0) - np.float32(15.0)) + np.float32(10.0))
|
|
||||||
du = np.float32(30.0) * f * f * (f * (f - np.float32(2.0)) + np.float32(1.0))
|
|
||||||
|
|
||||||
ga = hash22(i + np.array([0.0, 0.0], dtype=np.float32), random_seed)
|
|
||||||
gb = hash22(i + np.array([1.0, 0.0], dtype=np.float32), random_seed)
|
|
||||||
gc = hash22(i + np.array([0.0, 1.0], dtype=np.float32), random_seed)
|
|
||||||
gd = hash22(i + np.array([1.0, 1.0], dtype=np.float32), random_seed)
|
|
||||||
|
|
||||||
va = np.dot(ga, f - np.array([0.0, 0.0], dtype=np.float32))
|
|
||||||
vb = np.dot(gb, f - np.array([1.0, 0.0], dtype=np.float32))
|
|
||||||
vc = np.dot(gc, f - np.array([0.0, 1.0], dtype=np.float32))
|
|
||||||
vd = np.dot(gd, f - np.array([1.0, 1.0], dtype=np.float32))
|
|
||||||
|
|
||||||
val = va + u[0] * (vb - va) + u[1] * (vc - va) + u[0] * u[1] * (va - vb - vc + vd)
|
|
||||||
|
|
||||||
deriv_x = (
|
|
||||||
ga[0]
|
|
||||||
+ u[0] * (gb[0] - ga[0])
|
|
||||||
+ u[1] * (gc[0] - ga[0])
|
|
||||||
+ u[0] * u[1] * (ga[0] - gb[0] - gc[0] + gd[0])
|
|
||||||
+ du[0] * (u[1] * (va - vb - vc + vd) + (vb - va))
|
|
||||||
)
|
|
||||||
deriv_y = (
|
|
||||||
ga[1]
|
|
||||||
+ u[0] * (gb[1] - ga[1])
|
|
||||||
+ u[1] * (gc[1] - ga[1])
|
|
||||||
+ u[0] * u[1] * (ga[1] - gb[1] - gc[1] + gd[1])
|
|
||||||
+ du[1] * (u[0] * (va - vb - vc + vd) + (vc - va))
|
|
||||||
)
|
|
||||||
|
|
||||||
return val, np.array([deriv_x, deriv_y], dtype=np.float32)
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def fractal_noise(p, freq, octaves, lacunarity, gain, random_seed):
|
|
||||||
val = np.float32(0.0)
|
|
||||||
deriv = np.zeros(2, dtype=np.float32)
|
|
||||||
nf = np.float32(freq)
|
|
||||||
na = np.float32(1.0)
|
|
||||||
for _ in range(octaves):
|
|
||||||
v, d = noised(p * nf, random_seed)
|
|
||||||
val += np.float32(v) * na
|
|
||||||
deriv += d * na * nf
|
|
||||||
na *= np.float32(gain)
|
|
||||||
nf *= np.float32(lacunarity)
|
|
||||||
return val, deriv
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def phacelle_noise(p, norm_dir, freq, offset, normalization, random_seed):
|
|
||||||
side_dir = np.array([-norm_dir[1], norm_dir[0]], dtype=np.float32) * freq * TAU
|
|
||||||
offset_tau = np.float32(offset) * TAU
|
|
||||||
|
|
||||||
p_int = np.floor(p)
|
|
||||||
p_frac = (p - p_int).astype(np.float32)
|
|
||||||
|
|
||||||
phase_dir = np.zeros(2, dtype=np.float32)
|
|
||||||
weight_sum = np.float32(0.0)
|
|
||||||
|
|
||||||
for i in range(-1, 3):
|
|
||||||
for j in range(-1, 3):
|
|
||||||
grid_offset = np.array([float(i), float(j)], dtype=np.float32)
|
|
||||||
grid_point = p_int + grid_offset
|
|
||||||
random_offset = hash22(grid_point, random_seed) * np.float32(0.5)
|
|
||||||
|
|
||||||
vector_from_cell_point = p_frac - grid_offset - random_offset
|
|
||||||
sqr_dist = np.dot(vector_from_cell_point, vector_from_cell_point)
|
|
||||||
|
|
||||||
weight = np.exp(-sqr_dist * np.float32(2.0))
|
|
||||||
weight = max(np.float32(0.0), weight - np.float32(0.01111))
|
|
||||||
weight_sum += weight
|
|
||||||
|
|
||||||
wave_input = np.dot(vector_from_cell_point, side_dir) + offset_tau
|
|
||||||
phase_dir[0] += np.cos(wave_input) * weight
|
|
||||||
phase_dir[1] += np.sin(wave_input) * weight
|
|
||||||
|
|
||||||
if weight_sum < 1e-6:
|
|
||||||
return np.array([0.0, 0.0], dtype=np.float32), side_dir
|
|
||||||
|
|
||||||
interpolated = phase_dir / weight_sum
|
|
||||||
magnitude = np.sqrt(np.dot(interpolated, interpolated))
|
|
||||||
magnitude = max(np.float32(1.0) - np.float32(normalization), magnitude)
|
|
||||||
|
|
||||||
return interpolated / magnitude, side_dir
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def pow_inv(t, power):
|
|
||||||
return np.float32(1.0) - np.power(np.float32(1.0) - clamp01(t), np.float32(power))
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def ease_out(t):
|
|
||||||
v = np.float32(1.0) - clamp01(t)
|
|
||||||
return np.float32(1.0) - v * v
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def smooth_start(t, smoothing):
|
|
||||||
if t >= smoothing:
|
|
||||||
return t - np.float32(0.5) * np.float32(smoothing)
|
|
||||||
return np.float32(0.5) * t * t / np.float32(smoothing)
|
|
||||||
|
|
||||||
|
|
||||||
@njit(inline="always")
|
|
||||||
def safe_normalize(n):
|
|
||||||
length = np.sqrt(np.dot(n, n))
|
|
||||||
if length > 1e-10:
|
|
||||||
return n / length
|
|
||||||
return n
|
|
||||||
|
|
||||||
|
|
||||||
@njit(parallel=True)
|
|
||||||
def _run_erosion(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
generation_scale,
|
|
||||||
erosion_scale,
|
|
||||||
random_seed,
|
|
||||||
erosion_strength,
|
|
||||||
erosion_gully_weight,
|
|
||||||
erosion_detail,
|
|
||||||
erosion_octaves,
|
|
||||||
):
|
|
||||||
output = np.zeros((height, width), dtype=np.float32)
|
|
||||||
|
|
||||||
scale = np.float32(erosion_scale)
|
|
||||||
strength_base = np.float32(erosion_strength) * scale
|
|
||||||
|
|
||||||
for y in prange(height):
|
|
||||||
for x in range(width):
|
|
||||||
p = np.array([x / width, y / height], dtype=np.float32) * np.float32(
|
|
||||||
generation_scale
|
|
||||||
)
|
|
||||||
|
|
||||||
height_freq = np.float32(3.0)
|
|
||||||
height_amp = np.float32(0.125)
|
|
||||||
val, deriv = fractal_noise(
|
|
||||||
p, height_freq, 3, 2.0, 0.1, np.int64(random_seed)
|
|
||||||
)
|
|
||||||
|
|
||||||
n_height = val * height_amp + np.float32(0.5)
|
|
||||||
n_slope = deriv * height_amp
|
|
||||||
|
|
||||||
curr_height = n_height
|
|
||||||
curr_slope = n_slope
|
|
||||||
|
|
||||||
fade_target = clamp(
|
|
||||||
(curr_height - np.float32(0.5)) / (height_amp * np.float32(0.6)),
|
|
||||||
-1.0,
|
|
||||||
1.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
freq = np.float32(1.0) / (scale * EROSION_CELL_SCALE)
|
|
||||||
slope_length = max(
|
|
||||||
np.sqrt(np.dot(curr_slope, curr_slope)), np.float32(1e-10)
|
|
||||||
)
|
|
||||||
|
|
||||||
rounding_for_input = (
|
|
||||||
mix(
|
|
||||||
EROSION_ROUNDING[1],
|
|
||||||
EROSION_ROUNDING[0],
|
|
||||||
clamp01(fade_target + np.float32(0.5)),
|
|
||||||
)
|
|
||||||
* EROSION_ROUNDING[2]
|
|
||||||
)
|
|
||||||
combi_mask = ease_out(
|
|
||||||
smooth_start(
|
|
||||||
slope_length * EROSION_ONSET[0],
|
|
||||||
rounding_for_input * EROSION_ONSET[0],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
gully_slope = mix(
|
|
||||||
curr_slope,
|
|
||||||
curr_slope / slope_length * EROSION_ASSUMED_SLOPE[0],
|
|
||||||
EROSION_ASSUMED_SLOPE[1],
|
|
||||||
)
|
|
||||||
|
|
||||||
strength = strength_base
|
|
||||||
rounding_mult = np.float32(1.0)
|
|
||||||
total_h_delta = np.float32(0.0)
|
|
||||||
total_strength = np.float32(0.0)
|
|
||||||
|
|
||||||
for _ in range(erosion_octaves):
|
|
||||||
phacelle_vec, side_dir = phacelle_noise(
|
|
||||||
p * freq,
|
|
||||||
safe_normalize(gully_slope),
|
|
||||||
EROSION_CELL_SCALE,
|
|
||||||
0.25,
|
|
||||||
EROSION_NORMALIZATION,
|
|
||||||
np.int64(random_seed),
|
|
||||||
)
|
|
||||||
p_deriv_dir = side_dir * -freq
|
|
||||||
sloping = abs(phacelle_vec[1])
|
|
||||||
|
|
||||||
gully_slope += (
|
|
||||||
np.sign(phacelle_vec[1])
|
|
||||||
* p_deriv_dir
|
|
||||||
* strength
|
|
||||||
* np.float32(erosion_gully_weight)
|
|
||||||
)
|
|
||||||
gullies_h = phacelle_vec[0]
|
|
||||||
faded_gullies_h = mix(
|
|
||||||
fade_target,
|
|
||||||
gullies_h * np.float32(erosion_gully_weight),
|
|
||||||
combi_mask,
|
|
||||||
)
|
|
||||||
total_h_delta += faded_gullies_h * strength
|
|
||||||
total_strength += strength
|
|
||||||
fade_target = faded_gullies_h
|
|
||||||
|
|
||||||
rounding_for_octave = (
|
|
||||||
mix(
|
|
||||||
EROSION_ROUNDING[1],
|
|
||||||
EROSION_ROUNDING[0],
|
|
||||||
clamp01(phacelle_vec[0] + np.float32(0.5)),
|
|
||||||
)
|
|
||||||
* rounding_mult
|
|
||||||
)
|
|
||||||
new_mask = ease_out(
|
|
||||||
smooth_start(
|
|
||||||
sloping * EROSION_ONSET[1],
|
|
||||||
rounding_for_octave * EROSION_ONSET[1],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
combi_mask = pow_inv(combi_mask, erosion_detail) * new_mask
|
|
||||||
strength *= EROSION_GAIN
|
|
||||||
freq *= EROSION_LACUNARITY
|
|
||||||
rounding_mult *= EROSION_ROUNDING[3]
|
|
||||||
|
|
||||||
terrain_height_offset = np.array([-0.65, 0.0], dtype=np.float32)
|
|
||||||
final_fade_target = clamp(
|
|
||||||
(n_height - np.float32(0.5)) / (height_amp * np.float32(0.6)),
|
|
||||||
-1.0,
|
|
||||||
1.0,
|
|
||||||
)
|
|
||||||
offset = (
|
|
||||||
mix(
|
|
||||||
terrain_height_offset[0],
|
|
||||||
-final_fade_target,
|
|
||||||
terrain_height_offset[1],
|
|
||||||
)
|
|
||||||
* total_strength
|
|
||||||
)
|
|
||||||
|
|
||||||
output[y, x] = n_height + total_h_delta + offset
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def generate_heightmap(width, height, settings):
|
|
||||||
return _run_erosion(
|
|
||||||
width=int(width),
|
|
||||||
height=int(height),
|
|
||||||
generation_scale=np.float32(settings.generation_scale),
|
|
||||||
erosion_scale=np.float32(settings.erosion_scale),
|
|
||||||
random_seed=np.int64(settings.random_seed),
|
|
||||||
erosion_strength=np.float32(settings.erosion_strength),
|
|
||||||
erosion_gully_weight=np.float32(settings.erosion_gully_weight),
|
|
||||||
erosion_detail=np.float32(settings.erosion_detail),
|
|
||||||
erosion_octaves=int(settings.erosion_octaves),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_heightmap(heightmap):
|
|
||||||
h_min = float(np.min(heightmap))
|
|
||||||
h_max = float(np.max(heightmap))
|
|
||||||
if abs(h_max - h_min) < 1e-10:
|
|
||||||
return np.zeros_like(heightmap, dtype=np.float32)
|
|
||||||
return ((heightmap - h_min) / (h_max - h_min)).astype(np.float32)
|
|
||||||
|
|
||||||
|
|
||||||
def heightmap_to_pixel_data(heightmap, color_model, color_depth):
|
|
||||||
normalized = normalize_heightmap(heightmap)
|
|
||||||
|
|
||||||
if color_depth == "U8":
|
|
||||||
max_value = 255
|
|
||||||
gray = (normalized * max_value).astype(np.uint8)
|
|
||||||
alpha_value = np.uint8(max_value)
|
|
||||||
alpha_dtype = np.uint8
|
|
||||||
elif color_depth == "U16":
|
|
||||||
max_value = 65535
|
|
||||||
gray = (normalized * max_value).astype(np.uint16)
|
|
||||||
alpha_value = np.uint16(max_value)
|
|
||||||
alpha_dtype = np.uint16
|
|
||||||
elif color_depth == "F16":
|
|
||||||
gray = normalized.astype(np.float16)
|
|
||||||
alpha_value = np.float16(1.0)
|
|
||||||
alpha_dtype = np.float16
|
|
||||||
elif color_depth == "F32":
|
|
||||||
gray = normalized.astype(np.float32)
|
|
||||||
alpha_value = np.float32(1.0)
|
|
||||||
alpha_dtype = np.float32
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported document color depth: {color_depth}")
|
|
||||||
|
|
||||||
alpha = np.full(gray.shape, alpha_value, dtype=alpha_dtype)
|
|
||||||
|
|
||||||
if color_model == "RGBA":
|
|
||||||
channels = np.stack((gray, gray, gray, alpha), axis=-1)
|
|
||||||
elif color_model == "GRAYA":
|
|
||||||
channels = np.stack((gray, alpha), axis=-1)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unsupported document color model: {color_model}. Use an RGBA or grayscale document."
|
|
||||||
)
|
|
||||||
|
|
||||||
return channels.tobytes()
|
|
||||||
Reference in New Issue
Block a user