scopedvr-map-editor/tests/test_terrain_splat.gd
Hermes (scopedvr-dev) 1d8763b881 Karte t_5c94d098 Terrain-Blending v2: Splat-Texel-Dichte folgt der KARTENGROESSE
Gleiche Ursache wie im Client (BEWEISE.md in work/terrain_blending_v2_t_5c94d098_bilder/):
splat_side_for(129) = 512 px = 2 m/Texel auf der 1024-m-Standardmap (8 m/Zelle)
-> Gewichtsrampe in 2-m-Stufen, sichtbar als Baender/Stufen an der Materialgrenze.

Aenderung Editor:
- library/terrain_arrays.gd (Spiegel-Datei, byte-identisch zum Client):
  splat_side_for(res, map_size_m = 0.0) = MAX(4 Texel/Zelle, 1 m/Texel aus der
  Karten-Kantenlaenge), Zweierpotenz, geklemmt [256, 1024]; build_splat_bytes
  ohne Allokation je Texel + Schnellpfad (gleiche ID in allen vier Nachbarzellen)
  -> unveraenderte Bytes, deutlich schneller.
- src/viewport/terrain_mesh.gd: _build_splat bekommt size_x/size_z und gibt sie an
  splat_side_for; der Live-Sub-Rect-Pfad greift nur noch, wenn auch die
  Kantengroesse stimmt (Size-Aenderung ohne res-Aenderung baut die Splat neu).
- tests/test_terrain_splat.gd (Spiegel, identisch im Client): +6 Kartenmass-Checks
  (31/31).

Suiten (frischer Forgejo-Klon): test_terrain_splat 31/31, test_terrain 155/155,
test_terrain_mirror 106/106.
2026-10-22 19:37:30 +00:00

179 lines
8.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends SceneTree
## Splat-Gewichts-Ableitung (Karte t_7a0a6b79 — weiche Übergänge).
## Diese Datei ist SPIEGEL-IDENTISCH in Client UND Editor
## (client/tests/test_terrain_splat.gd ↔ editor/tests/test_terrain_splat.gd):
## beide Suiten rechnen dieselben Referenzwerte gegen dieselbe
## TerrainArrays.build_splat_bytes-Formel — grün in beiden Repos beweist die
## identische Formel beider Seiten (Spiegel-Datei-Vertrag, wie terrain_arrays.gd).
## Ausführen: godot --headless --path . -s res://tests/test_terrain_splat.gd
## Exit 0 = alles grün.
##
## Referenz-Fall (exakt rechenbar, Zweierpotenz-Koordinaten):
## 4×4 Zellen, alles ID 2 außer Zelle (2,2) = 7; side = 8.
## Texel (6,6): t = 6.5/8·3 = 2.4375 (exakt) → fx = fz = 0.4375 (exakt)
## → w(id2) = 0.80859375, w(id7) = 0.19140625 → Bytes [2, 206, 7, 49].
var failures := 0
var checks := 0
func _init() -> void:
_run()
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)
func _check(cond: bool, label: String) -> void:
checks += 1
if cond:
print(" ok: %s" % label)
else:
failures += 1
print(" FAIL: %s" % label)
func _run() -> void:
_test_side_for()
_test_uniform_region()
_test_reference_texel()
_test_boundary_ramp()
_test_sub_rect_equality()
_test_three_meeting_cells()
func _test_side_for() -> void:
print("Testgruppe: splat_side_for (Texel-Dichte, Zweierpotenz, geklemmt)")
_check(TerrainArrays.splat_side_for(2) == 256, "res 2 → 256 (Minimum)")
_check(TerrainArrays.splat_side_for(33) == 256, "res 33 → 256 (Ziel 128 < 256)")
_check(TerrainArrays.splat_side_for(65) == 256, "res 65 → 256 (Ziel 256)")
_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:
print("Testgruppe: Einmaterial-Fläche → eine ID, w0 = 1")
var mats := PackedByteArray()
mats.resize(4 * 4)
mats.fill(2)
var bytes := TerrainArrays.build_splat_bytes(mats, 4, 8)
_check(bytes.size() == 8 * 8 * 4, "Bytes: 8² Texel × 4 Kanäle")
var t := TerrainArrays.decode_splat_texel(bytes, 0)
_check(t[0] == 2 and t[1] == 1.0 and t[2] == 2 and t[3] == 0.0,
"Eck-Texel: id0=2, w0=1, id1=2 (Platzhalter), w1=0")
func _test_reference_texel() -> void:
print("Testgruppe: Referenz-Texel (exakt rechenbar, Spiegel-Vertrag)")
# VERTEX-Konvention: die Material-Schicht indiziert MESH-VERTEXE (res²,
# row-major wie die Heightmap). Texel (6,6), res 4, side 8:
# t = 6.5/8·3 = 2.4375 (exakt) → zwischen Vertex 2 und 3, fx = fz = 0.4375.
# Vertex (2,2) trägt ID 7 → w(7) = 0.5625² = 0.31640625 (exakt),
# w(2) = 0.68359375 → 8-bit: 174 + 81 = 255.
var mats := PackedByteArray()
mats.resize(4 * 4)
mats.fill(2)
mats[2 * 4 + 2] = 7
var bytes := TerrainArrays.build_splat_bytes(mats, 4, 8)
var t := TerrainArrays.decode_splat_texel(bytes, (6 * 8 + 6) * 4)
_check(t[0] == 2, "Referenz-Texel (6,6): id0 = 2 (got %d)" % t[0])
_check(t[1] > 0.67 and t[1] < 0.70, "Referenz-Texel (6,6): w0 ≈ 0.6836 (got %.4f)" % t[1])
_check(t[2] == 7, "Referenz-Texel (6,6): id1 = 7 (got %d)" % t[2])
_check(t[3] > 0.30 and t[3] < 0.33, "Referenz-Texel (6,6): w1 ≈ 0.3164 (got %.4f)" % t[3])
# 8-bit-Vertrag: w0 + w1 ≈ 1 (Bytes 174 + 81 = 255).
var raw := (bytes.decode_u8((6 * 8 + 6) * 4 + 1)
+ bytes.decode_u8((6 * 8 + 6) * 4 + 3))
_check(raw == 255, "Referenz-Texel (6,6): Gewichts-Bytes summieren auf 255 (got %d)" % raw)
func _test_boundary_ramp() -> void:
print("Testgruppe: Zellgrenze → weiche Rampe (keine Snap-Kante)")
# 5×5 VERTEXE: x < 3 → id 1, sonst id 3. Grenze zwischen Vertex 2 und 3 —
# die bilineare Rampe kreuzt 0.5 genau bei t = 2.5 (Mitte zwischen den
# Vertex-Positionen); Rampenbreite = ein Zell-Abstand (wie in der Spec).
var res := 5
var mats := PackedByteArray()
mats.resize(res * res)
for z in res:
for x in res:
mats[z * res + x] = 1 if x < 3 else 3
var side := 64
var bytes := TerrainArrays.build_splat_bytes(mats, res, side)
# Texel 39: t = 2.46875 → fx = 0.469 → w(id1) = 0.531; Texel 40: t = 2.53125
# → w(id1) = 0.469 — die dominante ID wechselt EXAKT an der Mitte.
var t39 := TerrainArrays.decode_splat_texel(bytes, (16 * 64 + 39) * 4)
var t40 := TerrainArrays.decode_splat_texel(bytes, (16 * 64 + 40) * 4)
_check(t39[0] == 1 and t39[2] == 3, "Grenz-Texel 39: IDs {1, 3} (got %d/%d)" % [t39[0], t39[2]])
_check(t40[0] == 3 and t40[2] == 1, "Grenz-Texel 40: IDs {3, 1} (gewechselt, got %d/%d)" % [t40[0], t40[2]])
_check(absf(t39[1] - 0.53125) < 0.004, "Grenz-Texel 39: w0 ≈ 0.531 (got %.4f)" % t39[1])
_check(absf(t40[1] - 0.53125) < 0.004, "Grenz-Texel 40: w0 ≈ 0.531 (got %.4f)" % t40[1])
_check(absf((t39[1] + t40[3]) - 1.0) < 0.01, "Grenz-Texel 39/40 spiegeln sich an t = 2.5")
# Weit weg von der Grenze: reine Seite (w0 = 1):
var t8 := TerrainArrays.decode_splat_texel(bytes, (16 * 64 + 8) * 4)
_check(t8[0] == 1 and t8[1] > 0.999, "Flachbereich links: w0 = 1 (got %.4f)" % t8[1])
# Kein Snap: das gewichtete ID-Feld (w0·id0 + w1·id1) steigt über die
# Grenze monoton und ohne Ein-Texel-Sprung (> 0.3 wäre ein Snap-Muster).
var prev := -1.0
var ok_ramp := true
for tx in range(34, 46):
var t := TerrainArrays.decode_splat_texel(bytes, (16 * 64 + tx) * 4)
var blended: float = float(t[0]) * t[1] + float(t[2]) * t[3]
if prev >= 0.0 and (blended < prev - 0.0001 or blended - prev > 0.3):
ok_ramp = false
prev = blended
_check(ok_ramp, "Rampe über die Grenze monoton, ohne Ein-Texel-Sprung")
func _test_sub_rect_equality() -> void:
print("Testgruppe: Sub-Rect-Ableitung == Voll-Ableitung (Live-Pfad-Vertrag)")
var res := 8
var mats := PackedByteArray()
mats.resize(res * res)
for i in mats.size():
mats[i] = (i * 7 + 3) % 5 # deterministisches Muster mit 5 IDs
var side := 32
var full := TerrainArrays.build_splat_bytes(mats, res, side)
var same := true
for rect: Rect2i in [Rect2i(0, 0, 16, 8), Rect2i(8, 8, 12, 20), Rect2i(24, 0, 8, 32), Rect2i(5, 3, 7, 9)]:
var part := TerrainArrays.build_splat_bytes(mats, res, side, rect)
if part.size() != rect.size.x * rect.size.y * 4:
same = false
break
for j in rect.size.y:
for i in rect.size.x:
var fofs := ((rect.position.y + j) * side + rect.position.x + i) * 4
for c in 4:
if part[j * rect.size.x * 4 + i * 4 + c] != full[fofs + c]:
same = false
_check(same, "Sub-Rect-Bytes byte-identisch zum Voll-Bild (4 Rechtecke)")
func _test_three_meeting_cells() -> void:
print("Testgruppe: >2 IDs am Treffpunkt → Top-2-Gewichte (Spec-Vertrag)")
# 2×2-Zellen-Beispiel: Zelle (0,0)=1, (1,0)=2, (0,1)=3, (1,1)=4; res 2, side 8.
# Texel (3,3): t = 3.5/8·1 = 0.4375 → fx = fz = 0.4375.
# w: id1 0.3164, id2 0.2461, id3 0.2461, id4 0.1914 → Top-2 = {1, 2}
# (Gleichstand 2/3: kleinerer Index, deterministisch); id4 (0.1914) wird
# bewusst verworfen (Zweikant-Fälle). Normalisiert: 0.3164/0.5625 = 0.5625.
var mats := PackedByteArray([1, 2, 3, 4])
var bytes := TerrainArrays.build_splat_bytes(mats, 2, 8)
var t := TerrainArrays.decode_splat_texel(bytes, (3 * 8 + 3) * 4)
_check(t[0] == 1 and t[1] > 0.55 and t[1] < 0.58,
"Treffpunkt: id0=1, w0 ≈ 0.5625 (normalisiert, got %.4f)" % t[1])
_check(t[2] == 2, "Treffpunkt: id1 = 2 (zweitgrößter, Gleichstand → kleinerer Index, got %d)" % t[2])
_check(t[3] > 0.42 and t[3] < 0.45, "Treffpunkt: w1 ≈ 0.4375 (got %.4f)" % t[3])
_check(t[1] + t[3] > 0.99, "Treffpunkt: Top-2-Gewichte normalisiert (Summe %.4f)" % (t[1] + t[3]))