initial commit from prototype
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
from krita import Krita
|
||||
|
||||
from .extension import AdvancedErosionGeneratorExtension
|
||||
|
||||
|
||||
app = Krita.instance()
|
||||
extension = AdvancedErosionGeneratorExtension(parent=app)
|
||||
app.addExtension(extension)
|
||||
@@ -0,0 +1,286 @@
|
||||
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()
|
||||
@@ -0,0 +1,391 @@
|
||||
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