TerrainArrays bleibt in den Apps: die gespeicherte .res verweist auf den Script-Pfad

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
slyphax 2026-10-31 15:30:46 +01:00
parent 17a8927fec
commit 92ee3fbdd1
2 changed files with 0 additions and 222 deletions

View file

@ -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)

View file

@ -1 +0,0 @@
uid://c5v7hht4apubd