diff --git a/src/editor/terrain_tool.gd b/src/editor/terrain_tool.gd index 40012e6..1b942a3 100644 --- a/src/editor/terrain_tool.gd +++ b/src/editor/terrain_tool.gd @@ -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 diff --git a/src/svmap/terrain_field.gd b/src/svmap/terrain_field.gd index f1ae209..b9e2cc6 100644 --- a/src/svmap/terrain_field.gd +++ b/src/svmap/terrain_field.gd @@ -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, diff --git a/src/ui/panels/terrain_panel.gd b/src/ui/panels/terrain_panel.gd index 437f944..0124a89 100644 --- a/src/ui/panels/terrain_panel.gd +++ b/src/ui/panels/terrain_panel.gd @@ -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: diff --git a/tests/test_mal_fein.gd b/tests/test_mal_fein.gd index 55cd84b..9d00133 100644 --- a/tests/test_mal_fein.gd +++ b/tests/test_mal_fein.gd @@ -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)) diff --git a/tests/test_terrain_mirror.gd b/tests/test_terrain_mirror.gd index f21b9d1..52f4f3f 100644 --- a/tests/test_terrain_mirror.gd +++ b/tests/test_terrain_mirror.gd @@ -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")