diff --git a/library/terrain_arrays.gd b/library/terrain_arrays.gd index 255eb6c..2f70264 100644 --- a/library/terrain_arrays.gd +++ b/library/terrain_arrays.gd @@ -31,17 +31,28 @@ class_name TerrainArrays # die zwei Layer: mix(tex(id1), tex(id0), w0). Datenformat UNVERÄNDERT — die # Zell-Schicht bleibt die Quelle der Wahrheit. # -# Texel-Dichte: SPLAT_TEXELS_PER_CELL Texel je Terrain-Zelle (4 × res−1), -# als Zweierpotenz geklemmt auf [SPLAT_SIDE_MIN, SPLAT_SIDE_MAX] — beim Malen -# kann dann ein kleiner Sub-Rect neu abgeleitet werden (Live-Pfad). +# Texel-Dichte: SPLAT_TEXELS_PER_CELL Texel je Terrain-Zelle (4 × res−1) UND +# mindestens SPLAT_TARGET_M_PER_TEXEL Meter je Texel (Karte t_5c94d098 — +# Messung: 8 m/Zelle × 4 = 2 m/Texel bei der 1024-m-Standardmap ergab eine +# in 2-m-Stufen gerasterte Gewichtsrampe, sichtbar als Bänder/Stufen quer zur +# Materialgrenze), als Zweierpotenz geklemmt auf [SPLAT_SIDE_MIN, SPLAT_SIDE_MAX] +# — beim Malen kann dann ein kleiner Sub-Rect neu abgeleitet werden (Live-Pfad). const SPLAT_TEXELS_PER_CELL := 4 const SPLAT_SIDE_MIN := 256 const SPLAT_SIDE_MAX := 1024 +const SPLAT_TARGET_M_PER_TEXEL := 1.0 ## Splat-Textur-Kantengröße für ein res×res-Zellraster (Zweierpotenz). -static func splat_side_for(res: int) -> int: +## map_size_m = Kantenlänge der Map in Weltmetern (optional, 0 = nicht gegeben): +## die Karten-GRÖSSE begrenzt die erreichbare Weichheit, nicht die Zellzahl — +## dieselbe Zellzahl liefert auf einer großen Map viel gröbere Texel. +## Ergebnis (Budget-geklemmt, 4 MB RGBA8 bei 1024²): 1024 m → 1024 (1 m/Texel), +## 256 m → 512 (0,5 m/Texel), 4096 m → 1024 (4 m/Texel, Klemme). +static func splat_side_for(res: int, map_size_m: float = 0.0) -> int: var target := maxi(1, res - 1) * SPLAT_TEXELS_PER_CELL + if map_size_m > 0.0: + target = maxi(target, int(ceilf(map_size_m / SPLAT_TARGET_M_PER_TEXEL))) var side := SPLAT_SIDE_MIN while side < target: side *= 2 @@ -95,6 +106,17 @@ static func build_splat_bytes(mats: PackedByteArray, res: int, side: int, iz1[j] = mini(j0 + 1, res - 1) fz[j] = t - float(j0) var ofs := 0 + # Wiederverwendete Kandidaten-Slots: die Ableitung läuft bei 1024² über + # 1 Mio. Texel — Allokationen je Texel (alte Fassung: 2 Packed-Arrays + + # Array-Literale) kosteten messbar Zeit (Karte t_5c94d098). + var cids := PackedInt32Array() + var cws := PackedFloat32Array() + var mids := PackedInt32Array() + var mws := PackedFloat32Array() + cids.resize(4) + cws.resize(4) + mids.resize(4) + mws.resize(4) for j in rh: var wz0 := 1.0 - fz[j] var wz1 := fz[j] @@ -103,38 +125,68 @@ static func build_splat_bytes(mats: PackedByteArray, res: int, side: int, for i in rw: var wx0 := 1.0 - fx[i] var wx1 := fx[i] - # ≤ 4 Zellen mit bilinearen Gewichten; Duplikat-IDs zusammenziehen: - var ids := PackedInt32Array() - var ws := PackedFloat32Array() - var cells := PackedInt32Array([ - row0 + ix0[i], row0 + ix1[i], row1 + ix0[i], row1 + ix1[i]]) - var weights := [wx0 * wz0, wx1 * wz0, wx0 * wz1, wx1 * wz1] + # Die vier Nachbarzellen (bilineare Gewichte) — Reihenfolge identisch + # zur Vorversion (Referenz-Checks in tests/test_terrain_splat.gd). + cids[0] = int(mats[row0 + ix0[i]]) + cids[1] = int(mats[row0 + ix1[i]]) + cids[2] = int(mats[row1 + ix0[i]]) + cids[3] = int(mats[row1 + ix1[i]]) + cws[0] = wx0 * wz0 + cws[1] = wx1 * wz0 + cws[2] = wx0 * wz1 + cws[3] = wx1 * wz1 + var a0 := cids[0] + if a0 == cids[1] and a0 == cids[2] and a0 == cids[3]: + # SCHNELLPFAD (Loch im Material, der Regelfall): alle vier Zellen + # dieselbe ID → w0 = 1, keine Mischung. Überspringt das + # Zusammenziehen der Gewichte (innerhalb einer Materialfläche + # praktisch jeder Texel, macht die 1024²-Ableitung bezahlbar). + out.encode_u8(ofs, a0) + out.encode_u8(ofs + 1, 255) + out.encode_u8(ofs + 2, a0) + out.encode_u8(ofs + 3, 0) + ofs += 4 + continue + # Duplikat-IDs zusammenziehen (Gewichte aufsummieren): + var n := 0 for c in 4: - var id := int(mats[cells[c]]) - var w: float = weights[c] + var w := cws[c] + if w <= 0.0: + continue + var cid := cids[c] var found := false - for k in ids.size(): - if ids[k] == id: - ws[k] += w + for k in n: + if mids[k] == cid: + mws[k] += w found = true break - if not found and w > 0.0: - ids.append(id) - ws.append(w) + if not found: + mids[n] = cid + mws[n] = w + n += 1 + if n == 0: + # Kann rechnerisch nicht auftreten (die 4 Gewichte summieren zu 1); + # defensiv: Texel = Layer 0 mit vollem Gewicht. + out.encode_u8(ofs, 0) + out.encode_u8(ofs + 1, 255) + out.encode_u8(ofs + 2, 0) + out.encode_u8(ofs + 3, 0) + ofs += 4 + continue # Top-2 IDs nach Gewicht (Fenster von max. 4 Kandidaten): - var id0 := ids[0] - var w0 := ws[0] + var id0 := mids[0] + var w0 := mws[0] var id1 := id0 var w1 := 0.0 - for k in range(1, ids.size()): - if ws[k] > w0: + for k in range(1, n): + if mws[k] > w0: id1 = id0 w1 = w0 - id0 = ids[k] - w0 = ws[k] - elif ws[k] > w1 or id1 == id0: - id1 = ids[k] - w1 = ws[k] + id0 = mids[k] + w0 = mws[k] + elif mws[k] > w1 or id1 == id0: + id1 = mids[k] + w1 = mws[k] # Normalisieren (w0 + w1 = 1) und 8-bit-packen: var wsum := maxf(w0 + w1, 0.0001) var b0 := clampi(roundi(w0 / wsum * 255.0), 0, 255) diff --git a/src/viewport/terrain_mesh.gd b/src/viewport/terrain_mesh.gd index e0b5929..45bd3c9 100644 --- a/src/viewport/terrain_mesh.gd +++ b/src/viewport/terrain_mesh.gd @@ -319,10 +319,11 @@ func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0 var shm := mat as ShaderMaterial shm.set_shader_parameter("map_size_x", size_x) shm.set_shader_parameter("map_size_z", size_z) - if splat_rect_cells.size.x > 0 and _splat_tex != null and _splat_res == res: + if splat_rect_cells.size.x > 0 and _splat_tex != null and _splat_res == res \ + and _splat_side == TerrainArrays.splat_side_for(res, maxf(size_x, size_z)): _refresh_splat_region(splat_rect_cells) - elif _splat_tex == null or _splat_res != res: - _build_splat(shm, mats, res) + else: + _build_splat(shm, mats, res, size_x, size_z) ## Höhenlinien-Uniform an die aktuelle Situation anpassen (Pref ∧ Relief ≥ 2 m). @@ -381,11 +382,14 @@ func resolution() -> int: ## (mats, res²) bleibt die Quelle der Wahrheit — die Textur ist reine ## Ableitung (Datenformat unverändert). Unbrauchbare Schicht → alte Textur ## bleibt (Robustheit, kein Crash). -func _build_splat(mat: ShaderMaterial, mats: PackedByteArray, res: int) -> void: +## size_x/size_z (t_5c94d098): Karten-Kantenlänge in Metern → Texel-Dichte der +## Splat (1 m/Texel bei 1024 m; vorher 2 m/Texel = sichtbare Stufen). +func _build_splat(mat: ShaderMaterial, mats: PackedByteArray, res: int, + size_x: float = 0.0, size_z: float = 0.0) -> void: if res < 2: _splat_tex = null return - var side := TerrainArrays.splat_side_for(res) + var side := TerrainArrays.splat_side_for(res, maxf(size_x, size_z)) var bytes := TerrainArrays.build_splat_bytes(mats, res, side) if bytes.size() != side * side * 4: return diff --git a/tests/test_terrain_splat.gd b/tests/test_terrain_splat.gd index 8987ea1..8be6b91 100644 --- a/tests/test_terrain_splat.gd +++ b/tests/test_terrain_splat.gd @@ -53,6 +53,16 @@ func _test_side_for() -> void: _check(TerrainArrays.splat_side_for(129) == 512, "res 129 → 512 (Ziel 512)") _check(TerrainArrays.splat_side_for(257) == 1024, "res 257 → 1024 (Ziel 1024)") _check(TerrainArrays.splat_side_for(513) == 1024, "res 513 → 1024 (Maximum-Klemme)") + # Karten-KANTENLÄNGE (Karte t_5c94d098): die Texel-Dichte hängt auch an der + # Map-Größe, nicht nur an der Zellzahl — 129 Zellen sind auf einer + # 1024-m-Map 8 m/Zelle (4 Texel/Zelle = 2 m/Texel, sichtbare Stufen), auf + # einer 256-m-Map 2 m/Zelle (4 Texel/Zelle = 0,5 m/Texel, fein genug). + _check(TerrainArrays.splat_side_for(129, 256.0) == 512, "res 129 + 256 m → 512 (0,5 m/Texel)") + _check(TerrainArrays.splat_side_for(129, 1024.0) == 1024, "res 129 + 1024 m → 1024 (1 m/Texel statt 2)") + _check(TerrainArrays.splat_side_for(129, 2048.0) == 1024, "res 129 + 2048 m → 1024 (Budget-Klemme)") + _check(TerrainArrays.splat_side_for(257, 1024.0) == 1024, "res 257 + 1024 m → 1024") + _check(TerrainArrays.splat_side_for(65, 512.0) == 512, "res 65 + 512 m → 512 (Meter-Ziel gewinnt)") + _check(TerrainArrays.splat_side_for(129, 0.0) == 512, "ohne Kartenmaß → alte res-Regel (512)") func _test_uniform_region() -> void: diff --git a/tests/test_terrain_splat.gd.uid b/tests/test_terrain_splat.gd.uid new file mode 100644 index 0000000..1913632 --- /dev/null +++ b/tests/test_terrain_splat.gd.uid @@ -0,0 +1 @@ +uid://yo5xqgub0r50