From 92ee3fbdd1102d24bd34ad9302a494973a525fc3 Mon Sep 17 00:00:00 2001 From: slyphax Date: Sat, 31 Oct 2026 15:30:46 +0100 Subject: [PATCH] TerrainArrays bleibt in den Apps: die gespeicherte .res verweist auf den Script-Pfad Co-Authored-By: Claude Opus 5.5 --- library/terrain_arrays.gd | 221 ---------------------------------- library/terrain_arrays.gd.uid | 1 - 2 files changed, 222 deletions(-) delete mode 100644 library/terrain_arrays.gd delete mode 100644 library/terrain_arrays.gd.uid diff --git a/library/terrain_arrays.gd b/library/terrain_arrays.gd deleted file mode 100644 index 331134a..0000000 --- a/library/terrain_arrays.gd +++ /dev/null @@ -1,221 +0,0 @@ -extends Resource -class_name TerrainArrays -## Träger-Resource für die Terrain-Texture2DArrays (Karte t_ab408127). -## GLEICHER Pfad in Client UND Editor (res://library/terrain_arrays.gd) — -## die gespeicherte .res verweist auf den Script-Pfad, der muss in beiden -## Repos auflösen (Repos sind entkoppelt, therefore identischer Ort). -## -## Warum diese Klasse: ResourceSaver speichert die internen Images eines -## Texture2DArray NICHT (gespeicherte .res/.tres enthält 20× null — 20.10. -## bewiesen). Images in einer eigenen exportierten Property dagegen landen -## zuverlässig in der .res-Datei. Der Verbraucher baut beim LOAD aus diesen -## Images sofort die Texture2DArrays (create_from_images) — die Images sind -## bereits ETC2-komprimiert (Quest-3-nativ, ~1 MB/Layer statt 4 MB RGBA8), -## der Bau ist also nur ein GPU-Upload, kein Rekomprimieren. - -@export var albedo_images: Array[Image] = [] -@export var normal_images: Array[Image] = [] -@export var tile_size := 1.0 # Meter pro Kachel (Worldspace-Tiling) - - -# ============================ SPLAT-GEWICHTS-ABLEITUNG ====================== -# Karte t_7a0a6b79 (weiche Übergänge): der alte Shader pickte pro Pixel GENAU -# EINE Textur-Schicht über die Vertex-Alpha (floor+0.5-Snap) — an Dreieck- -# kanten sprang die GPU-interpolierte Alpha hart zwischen Material-IDs -# (blockartige, dreieckstreppige Grenzen). NEU: aus der Zell-Schicht -# (materials_r8, res²) wird eine SPLAT-GEWICHTS-TEXTUR abgeleitet — je Texel -# die Top-2-Zell-IDs mit bilinearen Kernel-Gewichten: -# R = id0 (0..N-1), G = w0, B = id1, A = w1 (w0 + w1 = 1 normalisiert) -# Der Shader sampelt die Splat-Textur (FILTER_NEAREST — IDs dürfen NICHT -# bilinear vermischt werden; die Weichheit steckt IN den Gewichten) und mischt -# 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) 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). -## 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 - return mini(side, SPLAT_SIDE_MAX) - - -## Splat-Gewichts-Bytes (RGBA8, row-major) für die Zell-Schicht ableiten. -## rect = Texel-Rechteck (position/size im Bild) — leer = ganzes Bild. -## Bilinearer Kernel über die 4 umliegenden Zellen: an einer Zellgrenze rampen -## die Gewichte weich 0→1 (Weichheit = eine Zellbreite, wie in der Spec). -## Test-Vertrag: identische Formel in Client UND Editor (Spiegel-Datei). -static func build_splat_bytes(mats: PackedByteArray, res: int, side: int, - rect: Rect2i = Rect2i()) -> PackedByteArray: - var out := PackedByteArray() - if res < 2 or side < 1 or mats.size() < res * res: - return out - var rx := 0 - var ry := 0 - var rw := side - var rh := side - if rect.size.x > 0 and rect.size.y > 0: - rx = clampi(rect.position.x, 0, side - 1) - ry = clampi(rect.position.y, 0, side - 1) - rw = clampi(rect.size.x, 1, side - rx) - rh = clampi(rect.size.y, 1, side - ry) - out.resize(rw * rh * 4) - var last := float(res - 1) - # Achsen-Gewichte einmal vorrechnen (hängen nur von der Texel-Spalte/Zeile ab): - var ix0 := PackedInt32Array() - var ix1 := PackedInt32Array() - var fx := PackedFloat32Array() - ix0.resize(rw) - ix1.resize(rw) - fx.resize(rw) - for i in rw: - var t := (float(rx + i) + 0.5) / float(side) * last - var i0 := clampi(int(floorf(t)), 0, res - 1) - ix0[i] = i0 - ix1[i] = mini(i0 + 1, res - 1) - fx[i] = t - float(i0) - var iz0 := PackedInt32Array() - var iz1 := PackedInt32Array() - var fz := PackedFloat32Array() - iz0.resize(rh) - iz1.resize(rh) - fz.resize(rh) - for j in rh: - var t := (float(ry + j) + 0.5) / float(side) * last - var j0 := clampi(int(floorf(t)), 0, res - 1) - iz0[j] = j0 - 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] - var row0 := iz0[j] * res - var row1 := iz1[j] * res - for i in rw: - var wx0 := 1.0 - fx[i] - var wx1 := fx[i] - # 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 w := cws[c] - if w <= 0.0: - continue - var cid := cids[c] - var found := false - for k in n: - if mids[k] == cid: - mws[k] += w - found = true - break - 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 := mids[0] - var w0 := mws[0] - var id1 := id0 - var w1 := 0.0 - for k in range(1, n): - if mws[k] > w0: - id1 = id0 - w1 = w0 - 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) - out.encode_u8(ofs, clampi(id0, 0, 255)) - out.encode_u8(ofs + 1, b0) - out.encode_u8(ofs + 2, clampi(id1, 0, 255)) - out.encode_u8(ofs + 3, 255 - b0) - ofs += 4 - return out - - -## Ein Splat-Texel dekodieren (für Tests): → [id0, w0, id1, w1] (w0 ∈ 0..1). -static func decode_splat_texel(bytes: PackedByteArray, texel_ofs: int) -> Array: - return [ - int(bytes.decode_u8(texel_ofs)), - float(bytes.decode_u8(texel_ofs + 1)) / 255.0, - int(bytes.decode_u8(texel_ofs + 2)), - float(bytes.decode_u8(texel_ofs + 3)) / 255.0, - ] - - -## Material-ID-Karte für den Shader (Karte 25.10., weiche Übergänge pro Pixel): -## R8, ein Texel je Materialpunkt (mres²) — der Shader liest per texelFetch die -## 4 umliegenden IDs und mischt bilinear. Ersetzt im Rendering die Splat- -## Ableitung (build_splat_bytes bleibt für Probes/Tests). Unpassende Schicht → -## 2×2 alles Gras (Shader bleibt nie ohne Textur). -static func material_image(mats: PackedByteArray, mres: int) -> Image: - if mres < 2 or mats.size() != mres * mres: - var leer := PackedByteArray() - leer.resize(4) - return Image.create_from_data(2, 2, false, Image.FORMAT_R8, leer) - return Image.create_from_data(mres, mres, false, Image.FORMAT_R8, mats) diff --git a/library/terrain_arrays.gd.uid b/library/terrain_arrays.gd.uid deleted file mode 100644 index 2c9b50f..0000000 --- a/library/terrain_arrays.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c5v7hht4apubd