Malen: Regler „Kante hart ↔ weich“ + Pinsel ab 1 m

Sly 25.10.: harte Kanten an Strichrändern, Pinsel auf 1 m, Regler für die
Kantenschärfe.
- apply_material_brush(…, weichheit): im äußeren Anteil des Radius wird
  gestreut ausgedünnt (fester Hash je Punkt → deterministisch, Nachmalen
  füllt nicht auf); der Shader macht daraus einen ausfransenden Übergang.
  Kein Formatwechsel (ein Material je Punkt bleibt).
- Panel: Regler 0–100 % (Default 40 %), nur im Malen-Modus; Pinselgröße ab 1 m.
- test_mal_fein 26/26 (Dichte fällt nach außen, deterministisch, 1-m-Pinsel),
  test_terrain_mirror an die Tool-Weichheit angepasst.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
slyphax 2026-10-25 00:30:16 +02:00
parent 40ca5ab6ba
commit 4a7b99ee3e
5 changed files with 116 additions and 6 deletions

View file

@ -30,6 +30,8 @@ var interaction # Objekt-Interaktion (für Modus-Wechsel)
var active := false
var brush_mode: int = TerrainField.Mode.RAISE
var brush_radius := 12.0 # Meter
## Kante beim Malen: 0 = hart, 1 = ganz weich (Karte 25.10.).
var brush_weichheit := 0.4
var brush_strength := 8.0 # Meter pro Sekunde (Heben/Senken)
var paint_material: int = 0 # Material-ID im Malen-Modus (0=Gras, 1=Sand, 2=Fels)
@ -137,10 +139,15 @@ func set_brush_mode(mode: int) -> void:
func set_radius(m: float) -> void:
brush_radius = clampf(m, 2.0, 64.0)
brush_radius = clampf(m, 1.0, 64.0) # ab 1 m (Karte 25.10., feines Raster)
_refresh_ring_mesh()
## Regler „Kante: hart ↔ weich“ (nur Malen).
func set_weichheit(w: float) -> void:
brush_weichheit = clampf(w, 0.0, 1.0)
func set_strength(mps: float) -> void:
brush_strength = clampf(mps, 1.0, 20.0)
@ -449,7 +456,7 @@ func _paint_step(x: float, z: float) -> bool:
if _sym_active() and _mats.size() >= _mres * _mres:
snap = _snap_mats(_brush_region_r(g, _mres))
var changed := TerrainField.apply_material_brush(_mats, _mres, _sx, _sz,
x, z, brush_radius, _stroke_material)
x, z, brush_radius, _stroke_material, brush_weichheit)
if changed and not snap.is_empty():
_mirror_mats(snap, g)
return changed

View file

@ -97,9 +97,17 @@ static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, si
## die Vertex-Interpolation des Meshes glättet die Grenze optisch.
## mats = dekodierte Material-Schicht (PackedByteArray, res² Einträge).
static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float, size_z: float,
cx: float, cz: float, radius: float, material_id: int) -> bool:
cx: float, cz: float, radius: float, material_id: int, weichheit: float = 0.0) -> bool:
if res < 2 or radius <= 0.0 or mats.size() < res * res:
return false
# Weiche Kante (Karte 25.10., Sly: Regler „hart ↔ weich“): Die Schicht kennt
# pro Punkt nur EIN Material — deshalb wird der Rand gestreut ausgedünnt:
# im äußeren Anteil `weichheit` des Radius bekommt ein Punkt das Material
# nur mit fallender Wahrscheinlichkeit. Der Zufall ist ein fester Hash je
# Punkt (deterministisch: gleicher Strich = gleiches Ergebnis, Nachmalen
# füllt nicht auf). Der Shader macht aus der Streuung einen weichen Verlauf.
var w := clampf(weichheit, 0.0, 1.0)
var inner := radius * (1.0 - w)
var gx := (cx / size_x + 0.5) * float(res - 1)
var gz := (cz / size_z + 0.5) * float(res - 1)
var r_cells_x := radius / (size_x / float(res - 1))
@ -115,7 +123,9 @@ static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float,
while x <= x1:
var dx := (float(x) - gx) * (size_x / float(res - 1))
var dz := (float(z) - gz) * (size_z / float(res - 1))
if dx * dx + dz * dz < radius * radius:
var d2 := dx * dx + dz * dz
if d2 < radius * radius and (w <= 0.0 or d2 <= inner * inner \
or punkt_hash(x, z) < rand_schwelle((sqrt(d2) - inner) / maxf(radius - inner, 0.0001))):
var idx := z * res + x
if mats[idx] != material_id:
mats[idx] = material_id
@ -125,6 +135,20 @@ static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float,
return changed
## Deterministischer Zufall 0..1 je Rasterpunkt (Streu-Rand der weichen Kante).
static func punkt_hash(x: int, z: int) -> float:
var h := (x * 73856093) ^ (z * 19349663)
h = (h ^ (h >> 13)) * 1274126177
h = h ^ (h >> 16)
return float(h & 0xFFFF) / 65535.0
## Malwahrscheinlichkeit am Rand: t = 0 (Innenkante) → 1, t = 1 (Außenkante) → 0.
static func rand_schwelle(t: float) -> float:
var u := clampf(t, 0.0, 1.0)
return 1.0 - u * u * (3.0 - 2.0 * u)
## Glätten: Mischung Richtung 3×3-Umgebungsmittel (zweiphasig — das Mittel wird
## aus dem STAND der Feldes berechnet, sonst wandert die Welle in eine Richtung).
static func _apply_smooth(heights: PackedFloat32Array, res: int, hmin: float, hmax: float,

View file

@ -21,6 +21,8 @@ var terrain_radius_label: Label
var terrain_strength_slider: HSlider
var terrain_strength_label: Label
var terrain_paint_hint: Label # Malen: Hinweis auf die Raster-Feinheit (24.10.)
var terrain_weich_label: Label # Malen: Kante hart ↔ weich (25.10.)
var terrain_weich_slider: HSlider
# --- Bauen (Karte t_97ea59c5) — im selben Werkzeug-Tab (aus main.gd ausgezogen)
var build_edit_btn: Button
@ -132,7 +134,7 @@ func build() -> void:
terrain_radius_label = _field_label("Pinsel-Größe: 12 m")
terrain_panel.add_child(terrain_radius_label)
terrain_radius_slider = HSlider.new()
terrain_radius_slider.min_value = 2.0
terrain_radius_slider.min_value = 1.0
terrain_radius_slider.max_value = 64.0
terrain_radius_slider.step = 1.0
terrain_radius_slider.value = 12.0
@ -159,6 +161,22 @@ func build() -> void:
terrain_paint_hint.visible = false
terrain_panel.add_child(terrain_paint_hint)
# Kante beim Malen (Karte 25.10., Sly): 0 % = harter Rand, 100 % = der ganze
# Pinsel läuft weich aus (gestreut, der Shader glättet die Streuung).
terrain_weich_label = _field_label("Kante: 40 % weich")
terrain_weich_label.visible = false
terrain_panel.add_child(terrain_weich_label)
terrain_weich_slider = HSlider.new()
terrain_weich_slider.min_value = 0.0
terrain_weich_slider.max_value = 100.0
terrain_weich_slider.step = 5.0
terrain_weich_slider.value = 40.0
terrain_weich_slider.custom_minimum_size = UIScale.v2(0, 32)
terrain_weich_slider.tooltip_text = "Links = harte Kante, rechts = weicher Übergang"
terrain_weich_slider.visible = false
terrain_weich_slider.value_changed.connect(_on_terrain_weich_changed)
terrain_panel.add_child(terrain_weich_slider)
terrain_panel.add_child(_muted_label(
"Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Strg+Linksklick macht das Gegenteil — im Malen-Modus malt Strg+Klick Gras zurück. "
+ "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). Rechte Maustaste = Flug, ESC beendet den Terrain-Modus."))
@ -250,6 +268,9 @@ func _on_terrain_mode_changed(_pressed: bool) -> void:
if terrain_strength_slider != null:
terrain_strength_slider.visible = not paint
terrain_strength_label.visible = not paint
if terrain_weich_slider != null:
terrain_weich_slider.visible = paint
terrain_weich_label.visible = paint
if terrain_paint_hint != null:
terrain_paint_hint.visible = paint
terrain_paint_hint.text = paint_hint_text(main.doc.get("terrain", {}) if main.doc is Dictionary else {})
@ -302,6 +323,12 @@ func _on_terrain_radius_changed(value: float) -> void:
main.terrain_tool.call("set_radius", value)
func _on_terrain_weich_changed(value: float) -> void:
terrain_weich_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
if main.terrain_tool != null:
main.terrain_tool.call("set_weichheit", value / 100.0)
func _on_terrain_strength_changed(value: float) -> void:
terrain_strength_label.text = "Stärke: %.1f m pro Sekunde" % value
if main.terrain_tool != null:

View file

@ -20,6 +20,7 @@ func _initialize() -> void:
_f2_f4_f6_f7()
_f3()
_f5()
_f8_weich()
print("test_mal_fein: %d/%d ok" % [checks - failures, checks])
quit(1 if failures > 0 else 0)
@ -126,3 +127,54 @@ func _only_checksum(errs: PackedStringArray) -> bool:
if not str(e).begins_with("V13"):
return false
return true
## F8 (25.10.): Kante hart ↔ weich. 0 = voller Kreis, weich = Innenkern voll,
## Rand gestreut, Ausdünnung nimmt nach außen zu, deterministisch.
func _f8_weich() -> void:
var r := 1025
var hart := PackedByteArray()
hart.resize(r * r)
TerrainField.apply_material_brush(hart, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.0)
var weich := PackedByteArray()
weich.resize(r * r)
TerrainField.apply_material_brush(weich, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8)
var weich2 := PackedByteArray()
weich2.resize(r * r)
TerrainField.apply_material_brush(weich2, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8)
var n_h := _count(hart)
var n_w := _count(weich)
_check(n_w < n_h and n_w > n_h / 4, "F8 weich malt weniger Punkte als hart (%d vs %d)" % [n_w, n_h])
_check(weich == weich2, "F8 deterministisch (gleicher Strich = gleiches Ergebnis)")
# Ring-Dichten: innen (d<2) voll, Mitte (4..6) teils, außen (8..10) dünn
var d_in := _ring(weich, r, 0.0, 2.0)
var d_mid := _ring(weich, r, 4.0, 6.0)
var d_out := _ring(weich, r, 8.0, 10.0)
_check(d_in > 0.99 and d_mid > d_out and d_mid < 1.0 and d_out < 0.5,
"F8 Dichte fällt nach außen (innen %.2f, Mitte %.2f, Rand %.2f)" % [d_in, d_mid, d_out])
var eins := PackedByteArray()
eins.resize(r * r)
TerrainField.apply_material_brush(eins, r, 1024.0, 1024.0, 0.3, 0.2, 1.0, 1, 0.0)
_check(_count(eins) >= 1 and _count(eins) <= 5, "F8 1-m-Pinsel trifft %d Punkte" % _count(eins))
func _count(m: PackedByteArray) -> int:
var n := 0
for v in m:
if v != 0:
n += 1
return n
func _ring(m: PackedByteArray, r: int, a: float, b: float) -> float:
var c := (r - 1) / 2
var tot := 0
var hit := 0
for z in range(c - 12, c + 13):
for x in range(c - 12, c + 13):
var d := Vector2(x - c, z - c).length()
if d >= a and d < b:
tot += 1
if m[z * r + x] != 0:
hit += 1
return float(hit) / float(maxi(tot, 1))

View file

@ -418,7 +418,7 @@ func _test_off_mode_unchanged() -> void:
ref_mats.resize(RES * RES)
for i in int(ceil(0.3 / 0.05)):
TerrainField.apply_material_brush(ref_mats, RES, SIZE, SIZE,
-100.0, -60.0, RADIUS, 1)
-100.0, -60.0, RADIUS, 1, float(ttool_p.get("brush_weichheit")))
var ref_mats_b64 := SvmapIO.encode_materials(ref_mats)
_check(str(doc_p["terrain"]["materials_r8_b64"]) == ref_mats_b64,
"AUS/Malen: Material-Schicht byte-identisch zur direkten Anwendung")