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:
parent
17a8927fec
commit
92ee3fbdd1
2 changed files with 0 additions and 222 deletions
|
|
@ -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)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c5v7hht4apubd
|
|
||||||
Loading…
Reference in a new issue