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:
parent
4a7b99ee3e
commit
4e973795f6
12 changed files with 349 additions and 73 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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) ---
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue