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), # 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 ## Splat-Textur-Kantengröße für ein res×res-Zellraster (Zweierpotenz). static func splat_side_for(res: int) -> int: var target := maxi(1, res - 1) * SPLAT_TEXELS_PER_CELL 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 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] # ≤ 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] for c in 4: var id := int(mats[cells[c]]) var w: float = weights[c] var found := false for k in ids.size(): if ids[k] == id: ws[k] += w found = true break if not found and w > 0.0: ids.append(id) ws.append(w) # Top-2 IDs nach Gewicht (Fenster von max. 4 Kandidaten): var id0 := ids[0] var w0 := ws[0] var id1 := id0 var w1 := 0.0 for k in range(1, ids.size()): if ws[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] # 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, ]