Malen: echter weicher Übergang am Pinselrand (Misch-Schicht statt Streuung)

Sly 25.10.: „weich“ soll ein Textur-Verlauf am Pinselrand sein, nicht fleckig;
gerade Strichkanten wirkten weiter hart (1-m-Übergang).
- Format (optional, alte Karten gültig): materials_b_r8_b64 (zweites Material
  je Punkt) + materials_w_r8_b64 (Anteil 0–255); ohne Mischung fallen die
  Felder weg. V15 prüft Länge/IDs/Paarigkeit.
- TerrainField.apply_material_brush_mix: innen voll, außen Smoothstep-Anteil;
  nur verstärken, nie abschwächen; Gras drübermalen mischt zurück.
- Shader: Top-2 aus 4 Nachbarpunkten × (a, b) mit Anteilen → nur noch 2
  Albedo-Fetches; Warp bleibt.
- Pinsel/Live-Vorschau, Spiegeln, Symmetrie-Analyse/-Anwenden, Resize,
  Anheben, Undo (terrain-Snapshot) tragen die Misch-Schicht mit.
- Streu-Rand (vorheriger Commit) entfernt.
- Tests: test_mal_fein 30/30 (Verlauf fällt weich, nie abschwächen,
  zurückmischen, Kodierung), Editor-Suite unverändert grün bis auf die
  bekannten Umgebungs-Tests; Kette Editor → Server → Client per
  run_map_download_e2e 15/15 mit gemischter Karte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
slyphax 2026-10-25 00:43:26 +02:00
parent 4a7b99ee3e
commit 4e973795f6
12 changed files with 349 additions and 73 deletions

View file

@ -480,13 +480,18 @@ static func _analyze_terrain(doc: Dictionary, mode: int, an: Dictionary) -> void
var mats := SvmapIO.decode_materials(terrain) var mats := SvmapIO.decode_materials(terrain)
if mres < 2 or mats.size() < mres * mres: if mres < 2 or mats.size() < mres * mres:
return return
var mix := SvmapIO.decode_mix(terrain) # weiche Ränder: b + Anteil mitvergleichen
var mb: PackedByteArray = mix[0]
var mw: PackedByteArray = mix[1]
var mcells: Array = an["material_cells"] var mcells: Array = an["material_cells"]
for j in mres: for j in mres:
for i in mres: for i in mres:
var mc := SymmetryCore.mirror_cell(mode, i, j, mres) var mc := SymmetryCore.mirror_cell(mode, i, j, mres)
if not (i < mc.x or (i == mc.x and j < mc.y)): if not (i < mc.x or (i == mc.x and j < mc.y)):
continue continue
if mats[j * mres + i] != mats[mc.y * mres + mc.x]: var ia := j * mres + i
var ib := mc.y * mres + mc.x
if mats[ia] != mats[ib] or mb[ia] != mb[ib] or mw[ia] != mw[ib]:
mcells.append(Vector2i(i, j)) mcells.append(Vector2i(i, j))
if mcells.size() <= MAX_EXAMPLES: if mcells.size() <= MAX_EXAMPLES:
_push_dev(an, "Boden-Material (%d,%d)" % [i, j]) _push_dev(an, "Boden-Material (%d,%d)" % [i, j])
@ -685,14 +690,22 @@ static func _apply_materials(doc: Dictionary, mode: int, an: Dictionary,
var mats := SvmapIO.decode_materials(terrain) var mats := SvmapIO.decode_materials(terrain)
if mres < 2 or mats.size() < mres * mres: if mres < 2 or mats.size() < mres * mres:
return return
var mix := SvmapIO.decode_mix(terrain)
var mb: PackedByteArray = mix[0]
var mw: PackedByteArray = mix[1]
for cell in cells: for cell in cells:
var cc: Vector2i = cell var cc: Vector2i = cell
var mc := SymmetryCore.mirror_cell(mode, cc.x, cc.y, mres) var mc := SymmetryCore.mirror_cell(mode, cc.x, cc.y, mres)
var src := cc if not quelle_hoch else mc var src := cc if not quelle_hoch else mc
var dst := mc if not quelle_hoch else cc var dst := mc if not quelle_hoch else cc
mats[dst.y * mres + dst.x] = mats[src.y * mres + src.x] var di := dst.y * mres + dst.x
var si := src.y * mres + src.x
mats[di] = mats[si]
mb[di] = mb[si]
mw[di] = mw[si]
c["terrain_cells"] = int(c["terrain_cells"]) + 1 c["terrain_cells"] = int(c["terrain_cells"]) + 1
terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats)
SvmapIO.encode_mix(terrain, mb, mw)
static func _apply_spawns(doc: Dictionary, mode: int, an: Dictionary, static func _apply_spawns(doc: Dictionary, mode: int, an: Dictionary,

View file

@ -45,6 +45,8 @@ var _mats := PackedByteArray() # Material-Schicht (res²) — seit _refresh_
# Mal-Strich schreibt live hinein. # Mal-Strich schreibt live hinein.
var _res := 0 var _res := 0
var _mres := 0 # Raster der Material-Schicht (feines Raster, Karte 24.10.; alt = _res) var _mres := 0 # Raster der Material-Schicht (feines Raster, Karte 24.10.; alt = _res)
var _mix_b := PackedByteArray() # zweites Material je Punkt (weiche Ränder, Karte 25.10.)
var _mix_w := PackedByteArray() # Anteil des zweiten Materials 0–255
var _sx := 0.0 var _sx := 0.0
var _sz := 0.0 var _sz := 0.0
var _hmin := 0.0 var _hmin := 0.0
@ -126,6 +128,7 @@ func _refresh_from_doc() -> bool:
# des Strichs auf alles-Gras zurück (Sand/Fels „verschwinden" live). # des Strichs auf alles-Gras zurück (Sand/Fels „verschwinden" live).
_mats = SvmapIO.decode_materials(terrain) _mats = SvmapIO.decode_materials(terrain)
_mres = SvmapIO.material_resolution(terrain) _mres = SvmapIO.material_resolution(terrain)
_load_mix(terrain)
return _heights.size() >= _res * _res return _heights.size() >= _res * _res
@ -143,6 +146,24 @@ func set_radius(m: float) -> void:
_refresh_ring_mesh() _refresh_ring_mesh()
## Misch-Schicht (Karte 25.10.) aus dem terrain-Block — fehlt sie, Nullen.
func _load_mix(terrain: Dictionary) -> void:
var mix := SvmapIO.decode_mix(terrain)
_mix_b = mix[0]
_mix_w = mix[1]
## Materialpunkt als ein int (a | b<<8 | w<<16) — Spiegeln kopiert alle drei.
func _punkt(idx: int) -> int:
return int(_mats[idx]) | (int(_mix_b[idx]) << 8) | (int(_mix_w[idx]) << 16)
func _setze_punkt(idx: int, v: int) -> void:
_mats[idx] = v & 0xFF
_mix_b[idx] = (v >> 8) & 0xFF
_mix_w[idx] = (v >> 16) & 0xFF
## Regler „Kante: hart ↔ weich“ (nur Malen). ## Regler „Kante: hart ↔ weich“ (nur Malen).
func set_weichheit(w: float) -> void: func set_weichheit(w: float) -> void:
brush_weichheit = clampf(w, 0.0, 1.0) brush_weichheit = clampf(w, 0.0, 1.0)
@ -232,6 +253,7 @@ func _begin_stroke(mode: int, material_id: int = -1) -> void:
if mode == TerrainField.Mode.PAINT: if mode == TerrainField.Mode.PAINT:
_mats = SvmapIO.decode_materials(main.doc.get("terrain", {})) _mats = SvmapIO.decode_materials(main.doc.get("terrain", {}))
_mres = SvmapIO.material_resolution(main.doc.get("terrain", {})) _mres = SvmapIO.material_resolution(main.doc.get("terrain", {}))
_load_mix(main.doc.get("terrain", {}))
_refresh_ring_mesh() _refresh_ring_mesh()
main.call("_set_status", "%s läuft — Maustaste gedrückt halten. Beim Loslassen wird der ganze Strich EIN Rückgängig-Schritt (Strg+Z)." % _stroke_label()) main.call("_set_status", "%s läuft — Maustaste gedrückt halten. Beim Loslassen wird der ganze Strich EIN Rückgängig-Schritt (Strg+Z)." % _stroke_label())
@ -253,6 +275,8 @@ func _process(delta: float) -> void:
# das betroffene Zellrechteck (inkl. Spiegelzellen) und leitet nur # das betroffene Zellrechteck (inkl. Spiegelzellen) und leitet nur
# die Splat-Region neu ab; das Mesh selbst bleibt unangetastet # die Splat-Region neu ab; das Mesh selbst bleibt unangetastet
# (Vertex-Farben hängen NICHT vom Material ab). # (Vertex-Farben hängen NICHT vom Material ab).
if view_builder.has_method("set_terrain_mix"):
view_builder.call("set_terrain_mix", _mix_b, _mix_w)
view_builder.call("update_terrain", _heights, _mats, _splat_rect) view_builder.call("update_terrain", _heights, _mats, _splat_rect)
_ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3 _ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3
return return
@ -302,6 +326,7 @@ func _end_stroke() -> void:
var terrain: Dictionary = main.doc.get("terrain", {}) var terrain: Dictionary = main.doc.get("terrain", {})
if _stroke_mode == TerrainField.Mode.PAINT: if _stroke_mode == TerrainField.Mode.PAINT:
terrain["materials_r8_b64"] = SvmapIO.encode_materials(_mats) terrain["materials_r8_b64"] = SvmapIO.encode_materials(_mats)
SvmapIO.encode_mix(terrain, _mix_b, _mix_w)
main.history.commit("Boden gemalt (%s)" % SvmapIO.MATERIAL_IDS[clampi(_stroke_material, 0, SvmapIO.MATERIAL_IDS.size() - 1)], main.history.commit("Boden gemalt (%s)" % SvmapIO.MATERIAL_IDS[clampi(_stroke_material, 0, SvmapIO.MATERIAL_IDS.size() - 1)],
before, main.history.snapshot(["terrain"])) before, main.history.snapshot(["terrain"]))
else: else:
@ -455,8 +480,8 @@ func _paint_step(x: float, z: float) -> bool:
var snap := {} var snap := {}
if _sym_active() and _mats.size() >= _mres * _mres: if _sym_active() and _mats.size() >= _mres * _mres:
snap = _snap_mats(_brush_region_r(g, _mres)) snap = _snap_mats(_brush_region_r(g, _mres))
var changed := TerrainField.apply_material_brush(_mats, _mres, _sx, _sz, var changed := TerrainField.apply_material_brush_mix(_mats, _mix_b, _mix_w, _mres,
x, z, brush_radius, _stroke_material, brush_weichheit) _sx, _sz, x, z, brush_radius, _stroke_material, brush_weichheit)
if changed and not snap.is_empty(): if changed and not snap.is_empty():
_mirror_mats(snap, g) _mirror_mats(snap, g)
return changed return changed
@ -511,7 +536,7 @@ func _snap_mats(rect: Rect2i) -> Dictionary:
var snap := {} var snap := {}
for z: int in range(rect.position.y, rect.end.y): for z: int in range(rect.position.y, rect.end.y):
for x: int in range(rect.position.x, rect.end.x): for x: int in range(rect.position.x, rect.end.x):
snap[Vector2i(x, z)] = int(_mats[z * _mres + x]) snap[Vector2i(x, z)] = _punkt(z * _mres + x)
return snap return snap
@ -533,12 +558,12 @@ func _mirror_mats(snap: Dictionary, g: Vector2) -> void:
if mirror == pos: if mirror == pos:
continue continue
var midx: int = mirror.y * _mres + mirror.x var midx: int = mirror.y * _mres + mirror.x
var value: int = int(_mats[idx]) var value: int = _punkt(idx)
if snap.has(mirror): if snap.has(mirror):
if _cell_dist(mirror, g, cell_x, cell_z) < _cell_dist(pos, g, cell_x, cell_z): if _cell_dist(mirror, g, cell_x, cell_z) < _cell_dist(pos, g, cell_x, cell_z):
value = int(_mats[midx]) value = _punkt(midx)
_mats[idx] = value _setze_punkt(idx, value)
_mats[midx] = value _setze_punkt(midx, value)
done[mirror] = true done[mirror] = true

View file

@ -203,12 +203,19 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float,
var heights_old := SvmapIO.decode_heights(old_terrain) var heights_old := SvmapIO.decode_heights(old_terrain)
var mats_old := SvmapIO.decode_materials(old_terrain) var mats_old := SvmapIO.decode_materials(old_terrain)
var old_mres := SvmapIO.material_resolution(old_terrain) var old_mres := SvmapIO.material_resolution(old_terrain)
var mix_old := SvmapIO.decode_mix(old_terrain) # weiche Ränder (Karte 25.10.)
var mb_old: PackedByteArray = mix_old[0]
var mw_old: PackedByteArray = mix_old[1]
var new_mres := SvmapIO.material_resolution_for(new_res, new_sx, new_sz) var new_mres := SvmapIO.material_resolution_for(new_res, new_sx, new_sz)
var heights_new := PackedFloat32Array() var heights_new := PackedFloat32Array()
heights_new.resize(new_res * new_res) heights_new.resize(new_res * new_res)
var mats_new := PackedByteArray() var mats_new := PackedByteArray()
mats_new.resize(new_mres * new_mres) mats_new.resize(new_mres * new_mres)
var mb_new := PackedByteArray()
mb_new.resize(new_mres * new_mres)
var mw_new := PackedByteArray()
mw_new.resize(new_mres * new_mres)
var last_new := float(new_res - 1) var last_new := float(new_res - 1)
var half_x_old := old_sx * 0.5 var half_x_old := old_sx * 0.5
@ -239,7 +246,10 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float,
continue continue
var fx := clampf(roundf((x / old_sx + 0.5) * last_mold), 0.0, last_mold) var fx := clampf(roundf((x / old_sx + 0.5) * last_mold), 0.0, last_mold)
var fz := clampf(roundf((z / old_sz + 0.5) * last_mold), 0.0, last_mold) var fz := clampf(roundf((z / old_sz + 0.5) * last_mold), 0.0, last_mold)
mats_new[j * new_mres + i] = mats_old[int(fz) * old_mres + int(fx)] var src := int(fz) * old_mres + int(fx)
mats_new[j * new_mres + i] = mats_old[src]
mb_new[j * new_mres + i] = mb_old[src]
mw_new[j * new_mres + i] = mw_old[src]
var terrain := { var terrain := {
"type": "heightmap", "type": "heightmap",
@ -252,6 +262,7 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float,
"material_resolution": new_mres, "material_resolution": new_mres,
"materials_r8_b64": SvmapIO.encode_materials(mats_new), "materials_r8_b64": SvmapIO.encode_materials(mats_new),
} }
SvmapIO.encode_mix(terrain, mb_new, mw_new)
# bounds wie create_new auf die neue Größe — Y-Grenzen vom alten doc # bounds wie create_new auf die neue Größe — Y-Grenzen vom alten doc
# behalten (Resize ändert nur die Grundfläche, nicht die Luft). # behalten (Resize ändert nur die Grundfläche, nicht die Luft).

View file

@ -291,6 +291,7 @@ static func _validate_terrain(terrain: Dictionary) -> PackedStringArray:
break break
elif not (mats is String): elif not (mats is String):
errors.append("V15: materials_r8_b64 muss ein Base64-String sein (ist %s)." % str(mats)) errors.append("V15: materials_r8_b64 muss ein Base64-String sein (ist %s)." % str(mats))
errors.append_array(_validate_mix(terrain, mres_i))
# V20 (Karte t_28fdef03): Wasser-Level (optional). # V20 (Karte t_28fdef03): Wasser-Level (optional).
errors.append_array(_validate_wasser(terrain)) errors.append_array(_validate_wasser(terrain))
return errors return errors
@ -608,6 +609,72 @@ static func decode_materials(terrain: Dictionary) -> PackedByteArray:
return out return out
## V15b (Karte 25.10., weiche Übergänge): optionale Misch-Schicht —
## materials_b_r8_b64 (zweites Material je Punkt) + materials_w_r8_b64 (Anteil
## des zweiten Materials, 0–255). Nur BEIDE oder KEINE; Länge material_resolution².
static func _validate_mix(terrain: Dictionary, mres: int) -> PackedStringArray:
var errors := PackedStringArray()
var hb := terrain.has("materials_b_r8_b64")
var hw := terrain.has("materials_w_r8_b64")
if not hb and not hw:
return errors
if hb != hw:
errors.append("V15: materials_b_r8_b64 und materials_w_r8_b64 gibt es nur zusammen.")
return errors
var b = terrain["materials_b_r8_b64"]
var w = terrain["materials_w_r8_b64"]
if not (b is String) or not (w is String):
errors.append("V15: Misch-Schicht muss aus Base64-Strings bestehen.")
return errors
var braw := Marshalls.base64_to_raw(b as String)
var wraw := Marshalls.base64_to_raw(w as String)
if braw.size() != mres * mres or wraw.size() != mres * mres:
errors.append("V15: Misch-Schicht hat %d/%d Bytes, erwartet je %d (= material_resolution²)." % [
braw.size(), wraw.size(), mres * mres])
return errors
for v in braw:
if int(v) >= MATERIAL_IDS.size():
errors.append("V15: materials_b_r8_b64 enthält unbekannte Material-ID %d." % int(v))
break
return errors
## Misch-Schicht dekodieren: [b, w] (je material_resolution², fehlt sie → Nullen).
static func decode_mix(terrain: Dictionary) -> Array:
var n := material_resolution(terrain)
var b := PackedByteArray()
var w := PackedByteArray()
b.resize(maxi(n * n, 0))
w.resize(maxi(n * n, 0))
var bs = terrain.get("materials_b_r8_b64", "")
var ws = terrain.get("materials_w_r8_b64", "")
if bs is String and ws is String and (bs as String) != "" and (ws as String) != "":
var braw := Marshalls.base64_to_raw(bs as String)
var wraw := Marshalls.base64_to_raw(ws as String)
if braw.size() == b.size() and wraw.size() == w.size():
for i in braw.size():
if int(braw[i]) < MATERIAL_IDS.size():
b[i] = braw[i]
w = wraw
return [b, w]
## Misch-Schicht schreiben — ganz ohne Mischung (alle w = 0) fallen die Felder
## weg (Datei bleibt so klein wie vorher, alte Leser unberührt).
static func encode_mix(terrain: Dictionary, b: PackedByteArray, w: PackedByteArray) -> void:
var any := false
for v in w:
if v != 0:
any = true
break
if not any:
terrain.erase("materials_b_r8_b64")
terrain.erase("materials_w_r8_b64")
return
terrain["materials_b_r8_b64"] = encode_materials(b)
terrain["materials_w_r8_b64"] = Marshalls.raw_to_base64(w)
## Raster der Material-Schicht (feines Raster, Karte 24.10.) — ohne Feld ## Raster der Material-Schicht (feines Raster, Karte 24.10.) — ohne Feld
## gilt das Höhenraster (alte Maps). ## gilt das Höhenraster (alte Maps).
static func material_resolution(terrain: Dictionary) -> int: static func material_resolution(terrain: Dictionary) -> int:
@ -658,8 +725,12 @@ static func upgrade_material_layer(terrain: Dictionary) -> bool:
if cur >= target: if cur >= target:
return false return false
var mats := resample_materials(decode_materials(terrain), cur, target) var mats := resample_materials(decode_materials(terrain), cur, target)
var mix := decode_mix(terrain)
var mb := resample_materials(mix[0], cur, target)
var mw := resample_materials(mix[1], cur, target)
terrain["material_resolution"] = target terrain["material_resolution"] = target
terrain["materials_r8_b64"] = encode_materials(mats) terrain["materials_r8_b64"] = encode_materials(mats)
encode_mix(terrain, mb, mw)
return true return true

View file

@ -97,17 +97,9 @@ static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, si
## die Vertex-Interpolation des Meshes glättet die Grenze optisch. ## die Vertex-Interpolation des Meshes glättet die Grenze optisch.
## mats = dekodierte Material-Schicht (PackedByteArray, res² Einträge). ## mats = dekodierte Material-Schicht (PackedByteArray, res² Einträge).
static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float, size_z: float, static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float, size_z: float,
cx: float, cz: float, radius: float, material_id: int, weichheit: float = 0.0) -> bool: cx: float, cz: float, radius: float, material_id: int) -> bool:
if res < 2 or radius <= 0.0 or mats.size() < res * res: if res < 2 or radius <= 0.0 or mats.size() < res * res:
return false 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 gx := (cx / size_x + 0.5) * float(res - 1)
var gz := (cz / size_z + 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)) var r_cells_x := radius / (size_x / float(res - 1))
@ -123,9 +115,7 @@ static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float,
while x <= x1: while x <= x1:
var dx := (float(x) - gx) * (size_x / float(res - 1)) var dx := (float(x) - gx) * (size_x / float(res - 1))
var dz := (float(z) - gz) * (size_z / float(res - 1)) var dz := (float(z) - gz) * (size_z / float(res - 1))
var d2 := dx * dx + dz * dz if dx * dx + dz * dz < radius * radius:
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 var idx := z * res + x
if mats[idx] != material_id: if mats[idx] != material_id:
mats[idx] = material_id mats[idx] = material_id
@ -135,18 +125,62 @@ static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float,
return changed return changed
## Deterministischer Zufall 0..1 je Rasterpunkt (Streu-Rand der weichen Kante). ## Malen MIT weichem Rand (Karte 25.10., Sly: „weicher Übergang der Texturen
static func punkt_hash(x: int, z: int) -> float: ## am Rand des Malwerkzeugs“). Jeder Punkt kann zwei Materialien mischen:
var h := (x * 73856093) ^ (z * 19349663) ## a = mats (Hauptschicht), b = mix_b, Anteil von b = mix_w/255. Innerhalb
h = (h ^ (h >> 13)) * 1274126177 ## radius·(1−weichheit) wird voll gemalt, außen fällt der Anteil des neuen
h = h ^ (h >> 16) ## Materials weich auf 0 (Smoothstep). Ein Punkt wird nur verstärkt, nie
return float(h & 0xFFFF) / 65535.0 ## abgeschwächt (Nachmalen am Rand macht ihn kräftiger, nicht schwächer).
## Mischung bleibt zweiteilig: das neue Material + das bisher stärkste andere.
static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArray,
## Malwahrscheinlichkeit am Rand: t = 0 (Innenkante) → 1, t = 1 (Außenkante) → 0. mix_w: PackedByteArray, res: int, size_x: float, size_z: float,
static func rand_schwelle(t: float) -> float: cx: float, cz: float, radius: float, material_id: int, weichheit: float) -> bool:
var u := clampf(t, 0.0, 1.0) if res < 2 or radius <= 0.0 or mats.size() < res * res \
return 1.0 - u * u * (3.0 - 2.0 * u) or mix_b.size() < res * res or mix_w.size() < res * res:
return false
var soft := clampf(weichheit, 0.0, 1.0)
var inner := radius * (1.0 - soft)
var cell_x := size_x / float(res - 1)
var cell_z := size_z / float(res - 1)
var gx := (cx / size_x + 0.5) * float(res - 1)
var gz := (cz / size_z + 0.5) * float(res - 1)
var x0 := maxi(int(floor(gx - radius / cell_x)) - 1, 0)
var x1 := mini(int(ceil(gx + radius / cell_x)) + 1, res - 1)
var z0 := maxi(int(floor(gz - radius / cell_z)) - 1, 0)
var z1 := mini(int(ceil(gz + radius / cell_z)) + 1, res - 1)
var changed := false
for z in range(z0, z1 + 1):
for x in range(x0, x1 + 1):
var dx := (float(x) - gx) * cell_x
var dz := (float(z) - gz) * cell_z
var d := sqrt(dx * dx + dz * dz)
if d >= radius:
continue
var f := 1.0
if d > inner:
var t := clampf((d - inner) / maxf(radius - inner, 0.0001), 0.0, 1.0)
f = 1.0 - t * t * (3.0 - 2.0 * t)
var fb := int(roundf(f * 255.0))
if fb <= 0:
continue
var idx := z * res + x
var a := int(mats[idx])
var b := int(mix_b[idx])
var w := int(mix_w[idx])
var anteil := (255 - w if a == material_id else 0) + (w if b == material_id else 0)
if fb <= anteil:
continue
if fb >= 255:
mats[idx] = material_id
mix_b[idx] = material_id
mix_w[idx] = 0
else:
var andere := a if a != material_id else b
mats[idx] = andere
mix_b[idx] = material_id
mix_w[idx] = fb
changed = true
return changed
## Glätten: Mischung Richtung 3×3-Umgebungsmittel (zweiphasig — das Mittel wird ## Glätten: Mischung Richtung 3×3-Umgebungsmittel (zweiphasig — das Mittel wird

View file

@ -201,6 +201,12 @@ func update_terrain(heights: PackedFloat32Array, mats: PackedByteArray = PackedB
terrain_view.call("update_heights", heights, 0, 0.0, 0.0, mats, splat_rect_cells) terrain_view.call("update_heights", heights, 0, 0.0, 0.0, mats, splat_rect_cells)
## Misch-Schicht für das nächste Live-Update (Malen mit weichem Rand, Karte 25.10.).
func set_terrain_mix(mix_b: PackedByteArray, mix_w: PackedByteArray) -> void:
if terrain_view != null and is_instance_valid(terrain_view):
terrain_view.call("set_mix", mix_b, mix_w)
## Nur das Terrain neu aufbauen (nach Strich-Commit/Undo) — Objekt-Marker und ## Nur das Terrain neu aufbauen (nach Strich-Commit/Undo) — Objekt-Marker und
## Auswahl bleiben bestehen, kein Flackern der ganzen Szene. ## Auswahl bleiben bestehen, kein Flackern der ganzen Szene.
func rebuild_terrain_from_doc(data: Dictionary) -> void: func rebuild_terrain_from_doc(data: Dictionary) -> void:

View file

@ -41,6 +41,10 @@ uniform float texturen_an = 0.0; // 1.0 = Texturen aktiv (vom Mesh-Code gesetz
// Punkten — weiche Übergänge über genau einen Rasterabstand, egal wie fein. // Punkten — weiche Übergänge über genau einen Rasterabstand, egal wie fein.
// 4 Albedo- + 4 Normal-Fetches + 4 texelFetch, keine Branches. // 4 Albedo- + 4 Normal-Fetches + 4 texelFetch, keine Branches.
uniform sampler2D material_map : filter_nearest, repeat_disable; uniform sampler2D material_map : filter_nearest, repeat_disable;
// Misch-Schicht (Karte 25.10., weiche Pinselränder): je Punkt ein zweites
// Material (b) und dessen Anteil (w, 0–255). Ohne Mischung: alles 0.
uniform sampler2D material_b_map : filter_nearest, repeat_disable;
uniform sampler2D material_w_map : filter_nearest, repeat_disable;
uniform float material_res = 2.0; // Kantenlänge der Material-Karte (Punkte) uniform float material_res = 2.0; // Kantenlänge der Material-Karte (Punkte)
uniform float map_size_x = 1.0; // Terrain-Kantenlänge in Weltmetern uniform float map_size_x = 1.0; // Terrain-Kantenlänge in Weltmetern
uniform float map_size_z = 1.0; uniform float map_size_z = 1.0;
@ -80,6 +84,59 @@ float mat_id(ivec2 p) {
return clamp(floor(texelFetch(material_map, p, 0).r * 255.0 + 0.5), 0.0, layer_count - 1.0); return clamp(floor(texelFetch(material_map, p, 0).r * 255.0 + 0.5), 0.0, layer_count - 1.0);
} }
float mat_b(ivec2 p) {
return clamp(floor(texelFetch(material_b_map, p, 0).r * 255.0 + 0.5), 0.0, layer_count - 1.0);
}
// Top-2-Mischung (Karte 25.10.): 4 Nachbarpunkte × (a, b) = 8 Kandidaten mit
// Gewicht (bilinear × Punkt-Anteil). Gleiche IDs werden aufsummiert, die
// zwei stärksten Materialien gewinnen. Rückgabe: (id1, id2, Anteil id1).
// Danach nur 2 Albedo- + 2 Normal-Fetches.
vec3 top2(vec2 g) {
vec2 gi = min(floor(g), vec2(max(material_res - 2.0, 0.0)));
vec2 f = clamp(g - gi, 0.0, 1.0);
ivec2 p = ivec2(gi);
ivec2 q[4] = ivec2[4](p, p + ivec2(1, 0), p + ivec2(0, 1), p + ivec2(1, 1));
float cw[4] = float[4]((1.0 - f.x) * (1.0 - f.y), f.x * (1.0 - f.y), (1.0 - f.x) * f.y, f.x * f.y);
float ids[8];
float ws[8];
for (int k = 0; k < 4; k++) {
float wb = texelFetch(material_w_map, q[k], 0).r;
ids[2 * k] = mat_id(q[k]);
ws[2 * k] = cw[k] * (1.0 - wb);
ids[2 * k + 1] = mat_b(q[k]);
ws[2 * k + 1] = cw[k] * wb;
}
float id1 = ids[0];
float s1 = -1.0;
for (int i = 0; i < 8; i++) {
float s = 0.0;
for (int j = 0; j < 8; j++) {
s += ids[j] == ids[i] ? ws[j] : 0.0;
}
if (s > s1) {
s1 = s;
id1 = ids[i];
}
}
float id2 = id1;
float s2 = 0.0;
for (int i = 0; i < 8; i++) {
if (ids[i] == id1) {
continue;
}
float s = 0.0;
for (int j = 0; j < 8; j++) {
s += ids[j] == ids[i] ? ws[j] : 0.0;
}
if (s > s2) {
s2 = s;
id2 = ids[i];
}
}
return vec3(id1, id2, s1 / max(s1 + s2, 0.00001));
}
void fragment() { void fragment() {
vec3 col = COLOR.rgb; vec3 col = COLOR.rgb;
// --- Textur (t_ab408127): Material-ID aus Vertex-Alpha, Worldspace-UV --- // --- Textur (t_ab408127): Material-ID aus Vertex-Alpha, Worldspace-UV ---
@ -90,18 +147,14 @@ void fragment() {
// Top-2-Splat-Blend (t_7a0a6b79): die Weichheit steckt in den Gewich- // Top-2-Splat-Blend (t_7a0a6b79): die Weichheit steckt in den Gewich-
// ten der Splat-Textur — an Zellgrenzen rampen die IDs weich (bilinear // ten der Splat-Textur — an Zellgrenzen rampen die IDs weich (bilinear
// über die 4 umliegenden Zellen), im Flachbereich w0=1 (reines id0). // über die 4 umliegenden Zellen), im Flachbereich w0=1 (reines id0).
// Karte 25.10.: bilinear pro Pixel über die 4 Materialpunkte (Spiegel // Karte 25.10.: Top-2 aus 4 Nachbarpunkten inkl. Misch-Schicht
// zum Client-Shader terrain_splat.gdshader). // (Spiegel zum Client-Shader terrain_splat.gdshader).
float m1 = max(material_res - 1.0, 1.0); float m1 = max(material_res - 1.0, 1.0);
vec2 g = clamp(warp(clamp(map_uv, 0.0, 1.0) * m1), vec2(0.0), vec2(m1)); vec2 g = clamp(warp(clamp(map_uv, 0.0, 1.0) * m1), vec2(0.0), vec2(m1));
vec2 gi = min(floor(g), vec2(max(material_res - 2.0, 0.0))); vec3 t2 = top2(g);
vec2 f = clamp(g - gi, 0.0, 1.0);
ivec2 p = ivec2(gi);
vec2 uv = world_pos.xz / max(tile_size, 0.05); vec2 uv = world_pos.xz / max(tile_size, 0.05);
vec3 alb = texture(albedo_array, vec3(uv, mat_id(p))).rgb * (1.0 - f.x) * (1.0 - f.y) vec3 alb = mix(texture(albedo_array, vec3(uv, t2.y)).rgb,
+ texture(albedo_array, vec3(uv, mat_id(p + ivec2(1, 0)))).rgb * f.x * (1.0 - f.y) texture(albedo_array, vec3(uv, t2.x)).rgb, t2.z);
+ texture(albedo_array, vec3(uv, mat_id(p + ivec2(0, 1)))).rgb * (1.0 - f.x) * f.y
+ texture(albedo_array, vec3(uv, mat_id(p + ivec2(1, 1)))).rgb * f.x * f.y;
col = alb * col; // Tönung/Shading multipliziert (wie Client) col = alb * col; // Tönung/Shading multipliziert (wie Client)
} }
// --- Konturlinien (unverändert, Karte t_3ebe4086) --- // --- Konturlinien (unverändert, Karte t_3ebe4086) ---

View file

@ -94,6 +94,14 @@ var _splat_image: Image
var _splat_tex: ImageTexture var _splat_tex: ImageTexture
var _splat_side := 0 # Texel-Kantenlänge (Zweierpotenz) var _splat_side := 0 # Texel-Kantenlänge (Zweierpotenz)
var _splat_res := 0 # Zellraster-Auflösung, für die die Splat gilt var _splat_res := 0 # Zellraster-Auflösung, für die die Splat gilt
# Misch-Schicht (Karte 25.10.): zweites Material + Anteil je Punkt — eigene
# R8-Texturen neben der Material-Karte (gleiche Größe, Shader liest alle drei).
var _mix_b_cache := PackedByteArray()
var _mix_w_cache := PackedByteArray()
var _mix_b_image: Image
var _mix_w_image: Image
var _mix_b_tex: ImageTexture
var _mix_w_tex: ImageTexture
func _ready() -> void: func _ready() -> void:
@ -178,6 +186,9 @@ func rebuild_from_doc(data: Dictionary) -> void:
# Splat-Textur verwerfen — sie wird im update_heights frisch aus der # Splat-Textur verwerfen — sie wird im update_heights frisch aus der
# (nach Undo/Load evtl. geänderten) Zell-Schicht abgeleitet. # (nach Undo/Load evtl. geänderten) Zell-Schicht abgeleitet.
_splat_tex = null _splat_tex = null
var mix := SvmapIO.decode_mix(data.get("terrain", {}))
_mix_b_cache = mix[0]
_mix_w_cache = mix[1]
# Wasser (Karte t_28fdef03): Höhe merken — der Matsch-Ring (Ufer-Automatik) # Wasser (Karte t_28fdef03): Höhe merken — der Matsch-Ring (Ufer-Automatik)
# mischt sich in relief_color ein. INF = kein Wasser (Legacy unverändert). # mischt sich in relief_color ein. INF = kein Wasser (Legacy unverändert).
_wasser_level = SvmapWasser.level(data) _wasser_level = SvmapWasser.level(data)
@ -321,13 +332,24 @@ func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0
# Material-ID-Karte (Karte 25.10.): Malen = gleiche Größe → nur Daten # Material-ID-Karte (Karte 25.10.): Malen = gleiche Größe → nur Daten
# tauschen (1 Upload); sonst neu anlegen. splat_rect_cells bleibt als # tauschen (1 Upload); sonst neu anlegen. splat_rect_cells bleibt als
# Parameter (Aufrufer-Vertrag), wird aber nicht mehr gebraucht. # Parameter (Aufrufer-Vertrag), wird aber nicht mehr gebraucht.
if _splat_tex != null and _splat_res == mres and mats.size() == mres * mres: if _splat_tex != null and _splat_res == mres and mats.size() == mres * mres \
and _mix_b_tex != null and _mix_b_cache.size() == mres * mres:
_splat_image.set_data(mres, mres, false, Image.FORMAT_R8, mats) _splat_image.set_data(mres, mres, false, Image.FORMAT_R8, mats)
_splat_tex.update(_splat_image) _splat_tex.update(_splat_image)
_mix_b_image.set_data(mres, mres, false, Image.FORMAT_R8, _mix_b_cache)
_mix_b_tex.update(_mix_b_image)
_mix_w_image.set_data(mres, mres, false, Image.FORMAT_R8, _mix_w_cache)
_mix_w_tex.update(_mix_w_image)
else: else:
_build_splat(shm, mats, mres, size_x, size_z) _build_splat(shm, mats, mres, size_x, size_z)
## Misch-Schicht fürs nächste update_heights (Live-Malen, Karte 25.10.).
func set_mix(mix_b: PackedByteArray, mix_w: PackedByteArray) -> void:
_mix_b_cache = mix_b
_mix_w_cache = mix_w
## Raster einer Material-Schicht aus ihrer Länge (quadratisch). Unpassend/leer ## Raster einer Material-Schicht aus ihrer Länge (quadratisch). Unpassend/leer
## → Höhenraster (alte Karten, Legacy ohne Schicht). ## → Höhenraster (alte Karten, Legacy ohne Schicht).
static func mat_res_of(mats: PackedByteArray, res: int) -> int: static func mat_res_of(mats: PackedByteArray, res: int) -> int:
@ -406,8 +428,20 @@ func _build_splat(mat: ShaderMaterial, mats: PackedByteArray, res: int,
_splat_side = _splat_image.get_width() _splat_side = _splat_image.get_width()
_splat_res = res _splat_res = res
_splat_tex = ImageTexture.create_from_image(_splat_image) _splat_tex = ImageTexture.create_from_image(_splat_image)
# Misch-Schicht: passt sie nicht zum Raster (Legacy/leer) → Nullen.
if _mix_b_cache.size() != res * res or _mix_w_cache.size() != res * res:
_mix_b_cache = PackedByteArray()
_mix_b_cache.resize(res * res)
_mix_w_cache = PackedByteArray()
_mix_w_cache.resize(res * res)
_mix_b_image = TerrainArrays.material_image(_mix_b_cache, res)
_mix_w_image = TerrainArrays.material_image(_mix_w_cache, res)
_mix_b_tex = ImageTexture.create_from_image(_mix_b_image)
_mix_w_tex = ImageTexture.create_from_image(_mix_w_image)
if mat != null: if mat != null:
mat.set_shader_parameter("material_map", _splat_tex) mat.set_shader_parameter("material_map", _splat_tex)
mat.set_shader_parameter("material_b_map", _mix_b_tex)
mat.set_shader_parameter("material_w_map", _mix_w_tex)
mat.set_shader_parameter("material_res", float(_splat_side)) mat.set_shader_parameter("material_res", float(_splat_side))

View file

@ -13,10 +13,12 @@ func _initialize() -> void:
SvmapIO.upgrade_material_layer(t) SvmapIO.upgrade_material_layer(t)
var mres := SvmapIO.material_resolution(t) var mres := SvmapIO.material_resolution(t)
var mats := SvmapIO.decode_materials(t) var mats := SvmapIO.decode_materials(t)
for i in 40: var mix := SvmapIO.decode_mix(t)
TerrainField.apply_material_brush(mats, mres, float(t["size_x_m"]), float(t["size_z_m"]), for i in 40: # weicher Fels-Pfad (Misch-Schicht, Karte 25.10.)
-20.0 + float(i), 5.0, 0.6, 2) TerrainField.apply_material_brush_mix(mats, mix[0], mix[1], mres, float(t["size_x_m"]),
float(t["size_z_m"]), -20.0 + float(i), 5.0, 3.0, 2, 0.8)
t["materials_r8_b64"] = SvmapIO.encode_materials(mats) t["materials_r8_b64"] = SvmapIO.encode_materials(mats)
SvmapIO.encode_mix(t, mix[0], mix[1])
var sv := SvmapIO.save_to_file(out, doc) var sv := SvmapIO.save_to_file(out, doc)
print("PROBE ", "OK" if bool(sv["ok"]) else "FAIL %s" % str(sv["errors"]), " mres=", mres) print("PROBE ", "OK" if bool(sv["ok"]) else "FAIL %s" % str(sv["errors"]), " mres=", mres)
quit(0 if bool(sv["ok"]) else 1) quit(0 if bool(sv["ok"]) else 1)

View file

@ -20,13 +20,20 @@ func _run() -> void:
heights.resize(res * res) heights.resize(res * res)
var mats := PackedByteArray() var mats := PackedByteArray()
mats.resize(mres * mres) mats.resize(mres * mres)
for i in 60: # schräger 2-m-Pfad + Kurve var mb := PackedByteArray()
mb.resize(mres * mres)
var mw := PackedByteArray()
mw.resize(mres * mres)
var weich := float(OS.get_environment("PROBE_WEICH")) if OS.get_environment("PROBE_WEICH") != "" else 0.0
var radius := float(OS.get_environment("PROBE_R")) if OS.get_environment("PROBE_R") != "" else 2.0
for i in 60: # schräger Pfad + Kurve
var t := float(i) / 59.0 var t := float(i) / 59.0
TerrainField.apply_material_brush(mats, mres, size, size, TerrainField.apply_material_brush_mix(mats, mb, mw, mres, size, size,
-15.0 + 30.0 * t, -10.0 + 8.0 * sin(t * 3.0), 2.0, 1) -15.0 + 30.0 * t, -10.0 + 8.0 * sin(t * 3.0), radius, 1, weich)
var view := Node3D.new() var view := Node3D.new()
view.set_script(load("res://src/viewport/terrain_mesh.gd")) view.set_script(load("res://src/viewport/terrain_mesh.gd"))
root.add_child(view) root.add_child(view)
view.call("set_mix", mb, mw)
view.call("update_heights", heights, res, size, size, mats) view.call("update_heights", heights, res, size, size, mats)
view.call("set_contours", false) view.call("set_contours", false)
var cam := Camera3D.new() var cam := Camera3D.new()

View file

@ -129,32 +129,50 @@ func _only_checksum(errs: PackedStringArray) -> bool:
return true return true
## F8 (25.10.): Kante hart ↔ weich. 0 = voller Kreis, weich = Innenkern voll, ## F8 (25.10.): Kante hart ↔ weich = ECHTER Verlauf (Misch-Schicht b + Anteil w).
## Rand gestreut, Ausdünnung nimmt nach außen zu, deterministisch.
func _f8_weich() -> void: func _f8_weich() -> void:
var r := 1025 var r := 1025
var hart := PackedByteArray() var a := PackedByteArray()
hart.resize(r * r) a.resize(r * r)
TerrainField.apply_material_brush(hart, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.0) var b := PackedByteArray()
var weich := PackedByteArray() b.resize(r * r)
weich.resize(r * r) var w := PackedByteArray()
TerrainField.apply_material_brush(weich, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8) w.resize(r * r)
var weich2 := PackedByteArray() TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8)
weich2.resize(r * r) var c := (r - 1) / 2
TerrainField.apply_material_brush(weich2, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8) var mitte := c * r + c
var n_h := _count(hart) _check(a[mitte] == 1 and w[mitte] == 0, "F8 Pinselmitte voll Sand")
var n_w := _count(weich) var anteil := func(i: int) -> float:
_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]) return (float(255 - w[i]) / 255.0 if a[i] == 1 else 0.0) + (float(w[i]) / 255.0 if b[i] == 1 else 0.0)
_check(weich == weich2, "F8 deterministisch (gleicher Strich = gleiches Ergebnis)") var s4: float = anteil.call(c * r + c + 4)
# Ring-Dichten: innen (d<2) voll, Mitte (4..6) teils, außen (8..10) dünn var s7: float = anteil.call(c * r + c + 7)
var d_in := _ring(weich, r, 0.0, 2.0) var s9: float = anteil.call(c * r + c + 9)
var d_mid := _ring(weich, r, 4.0, 6.0) _check(s4 > s7 and s7 > s9 and s9 > 0.0 and s4 < 1.0,
var d_out := _ring(weich, r, 8.0, 10.0) "F8 Sand-Anteil fällt weich nach außen (4 m %.2f, 7 m %.2f, 9 m %.2f)" % [s4, s7, s9])
_check(d_in > 0.99 and d_mid > d_out and d_mid < 1.0 and d_out < 0.5, _check(anteil.call(c * r + c + 11) == 0.0, "F8 außerhalb des Radius unberührt")
"F8 Dichte fällt nach außen (innen %.2f, Mitte %.2f, Rand %.2f)" % [d_in, d_mid, d_out]) # Nachmalen schwächer am Rand darf nichts abschwächen
var vorher := w.duplicate()
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 3.0, 0.0, 10.0, 1, 0.8)
_check(anteil.call(c * r + c + 7) >= s7, "F8 Nachmalen verstärkt, schwächt nie ab")
# Rückmalen mit Gras (Strg+Klick) am Rand mischt zurück
var vor4: float = anteil.call(c * r + c + 4)
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 10.0, 0.0, 10.0, 0, 0.8)
var nach4: float = anteil.call(c * r + c + 4)
_check(nach4 < vor4 and nach4 > 0.0, "F8 Gras drübermalen mischt Sand zurück (%.2f → %.2f)" % [vor4, nach4])
# Kodierung: ohne Mischung keine Felder, mit Mischung gültig
var doc := SvmapDocument.create_new("Mix", 1024.0, "mittag")
var t: Dictionary = doc["terrain"]
t["materials_r8_b64"] = SvmapIO.encode_materials(a)
SvmapIO.encode_mix(t, b, w)
_check(t.has("materials_w_r8_b64") and SvmapIO._validate_core(doc).is_empty(),
"F8 Misch-Schicht gespeichert und gültig")
var leer := PackedByteArray()
leer.resize(r * r)
SvmapIO.encode_mix(t, leer, leer)
_check(not t.has("materials_w_r8_b64"), "F8 ohne Mischung keine Zusatzfelder")
var eins := PackedByteArray() var eins := PackedByteArray()
eins.resize(r * r) eins.resize(r * r)
TerrainField.apply_material_brush(eins, r, 1024.0, 1024.0, 0.3, 0.2, 1.0, 1, 0.0) TerrainField.apply_material_brush(eins, r, 1024.0, 1024.0, 0.3, 0.2, 1.0, 1)
_check(_count(eins) >= 1 and _count(eins) <= 5, "F8 1-m-Pinsel trifft %d Punkte" % _count(eins)) _check(_count(eins) >= 1 and _count(eins) <= 5, "F8 1-m-Pinsel trifft %d Punkte" % _count(eins))

View file

@ -233,6 +233,7 @@ func _test_half_stroke_all_modes() -> void:
var ttool_p = parts_p[0] var ttool_p = parts_p[0]
var doc_p: Dictionary = parts_p[3] var doc_p: Dictionary = parts_p[3]
ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras
ttool_p.set("brush_weichheit", 0.0) # harte Kante: Vergleich mit apply_material_brush
_stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3) _stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3)
var mats_p := _decode_mats(doc_p) var mats_p := _decode_mats(doc_p)
var sand := 0 var sand := 0
@ -413,12 +414,13 @@ func _test_off_mode_unchanged() -> void:
var ttool_p = parts_p[0] var ttool_p = parts_p[0]
var doc_p: Dictionary = parts_p[3] var doc_p: Dictionary = parts_p[3]
ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras
ttool_p.set("brush_weichheit", 0.0) # harte Kante: Vergleich mit apply_material_brush
_stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3) _stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3)
var ref_mats := PackedByteArray() var ref_mats := PackedByteArray()
ref_mats.resize(RES * RES) ref_mats.resize(RES * RES)
for i in int(ceil(0.3 / 0.05)): for i in int(ceil(0.3 / 0.05)):
TerrainField.apply_material_brush(ref_mats, RES, SIZE, SIZE, TerrainField.apply_material_brush(ref_mats, RES, SIZE, SIZE,
-100.0, -60.0, RADIUS, 1, float(ttool_p.get("brush_weichheit"))) -100.0, -60.0, RADIUS, 1)
var ref_mats_b64 := SvmapIO.encode_materials(ref_mats) var ref_mats_b64 := SvmapIO.encode_materials(ref_mats)
_check(str(doc_p["terrain"]["materials_r8_b64"]) == ref_mats_b64, _check(str(doc_p["terrain"]["materials_r8_b64"]) == ref_mats_b64,
"AUS/Malen: Material-Schicht byte-identisch zur direkten Anwendung") "AUS/Malen: Material-Schicht byte-identisch zur direkten Anwendung")