scopedvr-map-editor/library/terrain_arrays.gd

156 lines
5.7 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 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,
]