467 lines
19 KiB
GDScript
467 lines
19 KiB
GDScript
extends RefCounted
|
||
class_name GlbMeshStore
|
||
## Echte GLB-Renderings für Bibliotheks-Objekte im Editor-Viewport
|
||
## (Karte t_9b50b231). Der Viewport-Bauer zeigt bisher nur den
|
||
## Kollisions-Platzhalter (MeshShapes.make_placeholder) — diese statische
|
||
## Loader-Schicht holt stattdessen das ECHTE Modell aus dem Mesh-Lager
|
||
## (Sibling-Repos, gleiche Quellen wie tools/gen_thumbs.gd) und fällt ohne
|
||
## GLB auf den Platzhalter zurück.
|
||
##
|
||
## Messkonvention wie addons/modular_builder/scopedvr_export.gd
|
||
## `normalisiere`: das geladene Mesh wird in Fußpunkt-Koordinaten gebacken —
|
||
## Unterkante auf y = 0, XZ um den Mittelpunkt zentriert. Pack-GLBs haben
|
||
## ihren Mesh-Ursprung oft in der Mitte, deshalb wird die AABB-Unterkante
|
||
## GERECHNET und nie blind platziert. Damit gilt wie beim Compound-Platzhalter:
|
||
## MeshShapes.half_height(mesh) == 0 und marker.position == Fußpunkt.
|
||
##
|
||
## Plattform-Regel (PLAN.md): nur relative Kandidaten-Pfade, konfigurierbar
|
||
## über ProjectSettings — läuft identisch auf Linux und Windows.
|
||
##
|
||
## Performance: EIN Parse pro OID, Ergebnis gecacht — der Rebuild bei jedem
|
||
## Platzieren/Undo liest nur noch aus dem Cache.
|
||
|
||
## ProjectSettings-Schlüssel für die GLB-Quellen (relativ zum Editor-Projekt).
|
||
const QUELLEN_SETTING := "scopedvr/editor/glb_quellen"
|
||
## Voreinstellung = dieselben Quellen wie tools/gen_thumbs.gd (Sibling-Repos).
|
||
const DEFAULT_QUELLEN := ["../scopedvr-match-server/assets/objekte", "../scopedvr-client/assets/objekte"]
|
||
## Mitgelieferter Modell-Ordner (Karte t_12cf94fe): direkt neben der App
|
||
## (im Editor = Projektordner, in der exportierten App = neben der EXE).
|
||
## Der sync über tools/sync_models.sh legt hier die GLBs + Pack-Texturen
|
||
## ab — dann läuft der Map Builder auf einem fremden PC OHNE Sibling-Repos.
|
||
## Der Ordner trägt eine .gdignore (kein Import/PCK-Aufnahme; FileAccess
|
||
## mit absolutem Pfad funktioniert trotzdem).
|
||
const MODELS_DIR := "models"
|
||
## Klarer Hinweistext (Karte t_12cf94fe, Auftrag 3), wenn Modelle fehlen:
|
||
## wird als Klartext-Meldung gezeigt — kein stiller Platzhalter.
|
||
## Notfall-Modus: Terrain/Bauteile brauchen keine GLBs und funktionieren immer.
|
||
const FEHLT_TEXT := "Für die Anzeige fehlen die Modelle. So bekommst du sie:\n" \
|
||
+ " 1. tools/sync_models.sh aus dem Map-Editor-Ordner laufen lassen\n" \
|
||
+ " (kopiert die Modelle in den Ordner %s),\n" \
|
||
+ " 2. oder in den Einstellungen einen Ordner mit den Objekt-GLBs setzen.\n" \
|
||
+ "Terrain und Bauteile funktionieren trotzdem vollständig (Notfall-Modus)."
|
||
## Registry (A6): liefert das Feld "glb" für die Kategorie-verschachtelte Ablage.
|
||
const MANIFEST_PATH := "res://library/objects.json"
|
||
static var _manifest: Dictionary = {}
|
||
static var _manifest_geladen := false
|
||
|
||
# WYSIWYG-Fix (t_c8748efc, korrigiert in t_279df760): GLTFDocument löst externe
|
||
# Textur-URIs (Sibling-Repos, außerhalb res://) NICHT auf — im Editor-Viewport
|
||
# blieben die GLB-Objekte untexturiert/Editor-Farbe, während sie im Spiel
|
||
# getextured sind. Flächen OHNE albedo_texture bekommen hier die Textur, die das
|
||
# ITHappy-Pack für ihren MATERIALNAMEN vorsieht — Zuordnung identisch zu
|
||
# tools/PACK_TEXTURE_MAPPING.md (Client-Repo, verbindlich):
|
||
# military_base (+_00x) = Palette (Demonstration_Textures1.png, 1024²)
|
||
# picture_1/2/3 = echtes Multicam (Demonstration_picture_N.png, 2048²)
|
||
# Wichtig: Nichts wird überschrieben — GLTF-aufgelöste Texturen bleiben unangetastet.
|
||
# (Der frühere Inhalt-Vergleich + Kategorie-Camo stammte aus aa94bde und hat auch
|
||
# korrekte Pack-Texturen wieder durch Camo ersetzt — das war der Fehler.)
|
||
const PACK_TEX_PALETTE := "Demonstration_Textures1.png"
|
||
const PACK_TEX_PICTURE_1 := "Demonstration_picture_1.png"
|
||
const PACK_TEX_PICTURE_2 := "Demonstration_picture_2.png"
|
||
const PACK_TEX_PICTURE_3 := "Demonstration_picture_3.png"
|
||
|
||
# ImageTexture pro Textur-Pfad gecacht — sonst erzeugt jede der 291 GLBs
|
||
# ihre eigene 2048²-Textur (~16 MB VRAM je Stück statt 2 × 16 MB gesamt).
|
||
# false = Ladeversuch fehlgeschlagen (kein endloser Retry + Warn-Flut).
|
||
static var _camo_tex_cache: Dictionary = {}
|
||
## Dekodierte Multicam-Bilder (RAM, 2 × 2048² ≈ 32 MB) für den Inhalts-
|
||
## Vergleich gegen GLTF-eigene Textur-Instanzen.
|
||
static var _camo_bild_cache: Dictionary = {}
|
||
## Urteil pro GLTF-Textur-Instanz (instance_id → geteilte Textur | false),
|
||
## damit der 16-MB-Datenvergleich nur einmal je Instanz läuft.
|
||
static var _camo_ersetzung_cache: Dictionary = {}
|
||
|
||
# Cache pro OID: Mesh (normalisiert) oder `false` = GLB gefunden, aber nicht
|
||
# ladbar (Parse-Fehler — kein endloser Retry pro Rebuild). Fehlende Datei
|
||
# bleibt UNGECACHED (nur ein billiger Datei-Check pro Rebuild; legt man die
|
||
# GLB später ab, erscheint sie ohne Editor-Neustart).
|
||
static var _cache: Dictionary = {}
|
||
|
||
|
||
## Kandidaten-Pfade der Quellen in Ladereihenfolge (Karte t_12cf94fe,
|
||
## Auftrag 1 — EINE Stelle, erster Treffer gewinnt):
|
||
## 1. in den Einstellungen gesetzter Pfad (QUELLEN_SETTING),
|
||
## 2. bisheriges Verhalten = Sibling-Repos (DEFAULT_QUELLEN),
|
||
## 3. mitgelieferter Ordner models/ direkt neben der App (MODELS_DIR).
|
||
## Doppelnennungen werden gefiltert (Reihenfolge bleibt erhalten); ein
|
||
## ungültiger Settings-Wert (kein Array) fällt komplett auf die Vorgabe.
|
||
static func quellen() -> Array:
|
||
var out: Array = []
|
||
var setting: Variant = ProjectSettings.get_setting(QUELLEN_SETTING, DEFAULT_QUELLEN)
|
||
if setting is Array or setting is PackedStringArray:
|
||
for q in setting:
|
||
_quelle_add(out, q)
|
||
for q in DEFAULT_QUELLEN:
|
||
_quelle_add(out, q)
|
||
_quelle_add(out, MODELS_DIR)
|
||
return out
|
||
|
||
|
||
## Gültigen Quellpfad dedupliziert anhängen (typisiert, keine impliziten casts).
|
||
static func _quelle_add(out: Array, q: Variant) -> void:
|
||
if not (q is String):
|
||
return
|
||
var s := (q as String).strip_edges()
|
||
if s.is_empty() or out.has(s):
|
||
return
|
||
out.append(s)
|
||
|
||
|
||
## Absoluter Pfad des mitgelieferten Modell-Ordners (für die Klartext-
|
||
## Meldung und tools/sync_models.sh-Verweis). Im Editor = Projektordner,
|
||
## in der exportierten App = Ordner der EXE.
|
||
static func models_pfad() -> String:
|
||
return ProjectSettings.globalize_path("res://").path_join(MODELS_DIR)
|
||
|
||
|
||
## Erster EXISTIERENDER Dateipfad für die OID (ohne Laden/Parse — billiger
|
||
## Datei-Check über alle Kandidaten in Quellen-Reihenfolge). Leerer String =
|
||
## kein Quellpfad hat die Datei. Für Klartext-Meldungen/Probe-Logs: zeigt,
|
||
## AUS WELCHEM Ordner die Modelle kommen.
|
||
static func probe_source_fuer(oid: String) -> String:
|
||
if oid.is_empty():
|
||
return ""
|
||
for pfad in _kandidaten(oid):
|
||
if FileAccess.file_exists(pfad):
|
||
return pfad
|
||
return ""
|
||
|
||
|
||
## Ausgefüllter Klartext-Hinweis (FEHLT_TEXT + echter models/-Pfad) —
|
||
## diese EINE Funktion liefert die Meldung, die die App zeigt, wenn
|
||
## Modelle fehlen (Karte t_12cf94fe, Auftrag 3).
|
||
static func fehl_text() -> String:
|
||
return FEHLT_TEXT % models_pfad()
|
||
|
||
|
||
## Kandidaten-Pfade (absolut) in Ladereihenfolge: Registry-`glb` (A6, Kategorie-
|
||
## Ablage assets/objekte/military_desert/<kategorie>/<oid>.glb) VOR der flachen
|
||
## Alt-Ablage <quelle>/<oid>.glb. Die Quelle ist ein Mesh-Lager (<repo>/assets/
|
||
## objekte) — für den verschachtelten Pfad wird sie auf die Repo-Wurzel gekürzt.
|
||
static func _kandidaten(oid: String) -> Array:
|
||
var basis := ProjectSettings.globalize_path("res://")
|
||
var out: Array = []
|
||
var rel := str(_eintrag(oid).get("glb", ""))
|
||
if rel.begins_with("res://"):
|
||
var sub := rel.substr(6)
|
||
# Operator-Korrektur 20.10. (Eigenständigkeits-Beweis): der mitgelieferte
|
||
# models/-Ordner NEBEN der App enthält die Pack-Struktur DIREKT
|
||
# (models/military_desert/props/box_010.glb) — nicht unter
|
||
# models/assets/objekte/. Ohne den zweiten Kandidaten unten werden ALLE
|
||
# verschachtelten Pack-Objekte in der eigenständigen App nicht gefunden
|
||
# (belegt: barrel_001/box_010 -> leer, fehlende_glbs=2 trotz 334 GLBs).
|
||
var sub_kurz := sub.trim_prefix("assets/objekte/")
|
||
for quelle in quellen():
|
||
var q := str(quelle)
|
||
var marke := "/assets/objekte"
|
||
var wurzel := q.substr(0, q.length() - marke.length()) if q.ends_with(marke) else q
|
||
out.append(basis.path_join(wurzel).path_join(sub))
|
||
if sub_kurz != sub:
|
||
out.append(basis.path_join(wurzel).path_join(sub_kurz))
|
||
for quelle in quellen():
|
||
out.append(basis.path_join(str(quelle)).path_join(oid + ".glb"))
|
||
return out
|
||
|
||
|
||
## Registry-Eintrag der OID (einmal gelesen, dann gecacht).
|
||
static func _eintrag(oid: String) -> Dictionary:
|
||
if not _manifest_geladen:
|
||
_manifest_geladen = true
|
||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(MANIFEST_PATH))
|
||
if parsed is Dictionary:
|
||
_manifest = (parsed as Dictionary).get("objects", {})
|
||
return _manifest.get(oid, {})
|
||
|
||
|
||
## Echtes Mesh für eine Objekt-ID (normalisiert, Fußpunkt-Koordinaten) oder
|
||
## null = keine brauchbare GLB → Aufrufer zeigt den Platzhalter.
|
||
static func mesh_fuer(oid: String) -> Mesh:
|
||
if oid.is_empty():
|
||
return null
|
||
# Bauteile (bau_*) entstehen aus dem Katalog — identisch zu den GLBs, die
|
||
# der Client lädt (tools/gen_bauteile.gd), aber ohne Modell-Ordner nötig.
|
||
if BauKatalog.ist_bau(oid):
|
||
return BauKatalog.mesh(oid)
|
||
if _cache.has(oid):
|
||
return _cache[oid] if _cache[oid] is Mesh else null
|
||
# Im Spiel sind die Modelle importierter Teil des Projekts.
|
||
var im_projekt := _projekt_pfad(oid)
|
||
if not im_projekt.is_empty():
|
||
var szene: Node = (load(im_projekt) as PackedScene).instantiate()
|
||
var aus_projekt := _zusammenbacken(szene)
|
||
szene.free()
|
||
_cache[oid] = aus_projekt if aus_projekt != null else false
|
||
return aus_projekt
|
||
for pfad in _kandidaten(oid):
|
||
if FileAccess.file_exists(pfad):
|
||
var m := _glb_mesh(pfad)
|
||
if m != null:
|
||
camo_tex_setzen(m, pfad)
|
||
_cache[oid] = m
|
||
return m
|
||
# Datei da, aber kaputt → NEGATIV cachen (Parse-Fehler wiederholt
|
||
# sich sonst bei jedem Rebuild und kostet Zeit).
|
||
_cache[oid] = false
|
||
return null
|
||
return null
|
||
|
||
|
||
static func _projekt_pfad(oid: String) -> String:
|
||
var rel := str(_eintrag(oid).get("glb", ""))
|
||
if rel.begins_with("res://") and ResourceLoader.exists(rel, "PackedScene"):
|
||
return rel
|
||
return ""
|
||
|
||
|
||
## Liegt für die OID überhaupt eine GLB-Datei im Lager? (Billiger Check ohne
|
||
## Parse — für Statuszeilen-Hinweise beim Platzieren.)
|
||
static func fehlt(oid: String) -> bool:
|
||
if oid.is_empty():
|
||
return true
|
||
if BauKatalog.ist_bau(oid):
|
||
return false
|
||
if _cache.has(oid):
|
||
return not (_cache[oid] is Mesh)
|
||
if not _projekt_pfad(oid).is_empty():
|
||
return false
|
||
for pfad in _kandidaten(oid):
|
||
if FileAccess.file_exists(pfad):
|
||
return false
|
||
return true
|
||
|
||
|
||
## true = KEINE Quelle (Einstellungen/Siblings/models/) enthält irgend eine
|
||
## GLB-Datei → die App hat nirgends echte Modelle. Für das Klartext-Banner
|
||
## beim Start (Karte t_12cf94fe, Auftrag 3). Nicht-cachend, einmal pro Start.
|
||
static func lager_ist_leer() -> bool:
|
||
for quelle in quellen():
|
||
if _quelle_hat_glb(ProjectSettings.globalize_path("res://").path_join(str(quelle))):
|
||
return false
|
||
return true
|
||
|
||
|
||
## Enthält der Ordner (rekursiv) mindestens eine .glb-Datei?
|
||
static func _quelle_hat_glb(basis: String) -> bool:
|
||
var d := DirAccess.open(basis)
|
||
if d == null:
|
||
return false
|
||
d.list_dir_begin()
|
||
var name := d.get_next()
|
||
while name != "":
|
||
var pfad := basis.path_join(name)
|
||
if d.current_is_dir():
|
||
if _quelle_hat_glb(pfad):
|
||
return true
|
||
elif name.ends_with(".glb"):
|
||
return true
|
||
name = d.get_next()
|
||
return false
|
||
|
||
|
||
## Cache leeren (Tests / nach manuellem Eingriff ins Mesh-Lager).
|
||
static func cache_leeren() -> void:
|
||
_cache.clear()
|
||
_camo_tex_cache.clear()
|
||
_camo_bild_cache.clear()
|
||
_camo_ersetzung_cache.clear()
|
||
_manifest.clear()
|
||
_manifest_geladen = false
|
||
|
||
|
||
# ------------------------------------------------------------ Laden --------
|
||
|
||
## GLB OHNE Import-System laden (GLTFDocument, wie gen_thumbs) und ALLE
|
||
## MeshInstance3D der Szene in EIN ArrayMesh zusammenbacken (Jedes Teil-Node
|
||
## mit seiner Node-Transform; Materialien je Oberfläche bleiben erhalten —
|
||
## echte Texturen statt Editor-Farbe). null = Load-Fehlschlag.
|
||
static func _glb_mesh(pfad: String) -> Mesh:
|
||
var gltf := GLTFDocument.new()
|
||
var state := GLTFState.new()
|
||
if gltf.append_from_file(pfad, state) != OK:
|
||
push_warning("GlbMeshStore: GLB nicht ladbar: %s" % pfad)
|
||
return null
|
||
var szene: Node = gltf.generate_scene(state)
|
||
if szene == null:
|
||
return null
|
||
var m := _zusammenbacken(szene)
|
||
# Das Mesh ist eine Resource (RefCounted) — das Freigeben der Szene
|
||
# berührt die Referenz im Rückgabewert nicht.
|
||
szene.free()
|
||
return m
|
||
|
||
|
||
## Alle Mesh-Instanzen der Szene → ein ArrayMesh (Transforms angewandt,
|
||
## Materialien übernommen), dann normalisiert (Fußpunkt-Konvention).
|
||
static func _zusammenbacken(szene: Node) -> Mesh:
|
||
var roh := ArrayMesh.new()
|
||
_sammeln(szene, Transform3D.IDENTITY, roh)
|
||
if roh.get_surface_count() == 0:
|
||
return null
|
||
return _normalisiert(roh)
|
||
|
||
|
||
static func _sammeln(n: Node, xf: Transform3D, out: ArrayMesh) -> void:
|
||
if n is Node3D:
|
||
xf = xf * (n as Node3D).transform
|
||
if n is MeshInstance3D and (n as MeshInstance3D).mesh != null:
|
||
var m: Mesh = (n as MeshInstance3D).mesh
|
||
for s in m.get_surface_count():
|
||
var arrays: Array = m.surface_get_arrays(s)
|
||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||
continue
|
||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||
for i in verts.size():
|
||
verts[i] = xf * verts[i]
|
||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||
if arrays[Mesh.ARRAY_NORMAL] is PackedVector3Array:
|
||
var norms: PackedVector3Array = arrays[Mesh.ARRAY_NORMAL]
|
||
for i in norms.size():
|
||
norms[i] = (xf.basis * norms[i]).normalized()
|
||
arrays[Mesh.ARRAY_NORMAL] = norms
|
||
# Statisch fürs Editor-Viewport: Skin-Daten fliegen raus (sonst
|
||
# verlangt add_surface_from_arrays ein Skeleton).
|
||
arrays[Mesh.ARRAY_BONES] = null
|
||
arrays[Mesh.ARRAY_WEIGHTS] = null
|
||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||
var mat: Material = m.surface_get_material(s)
|
||
if mat != null:
|
||
out.surface_set_material(out.get_surface_count() - 1, mat)
|
||
for c in n.get_children():
|
||
_sammeln(c, xf, out)
|
||
|
||
|
||
## Fußpunkt-Konvention (wie scopedvr_export.normalisiere): Unterkante y = 0,
|
||
## XZ um den AABB-Mittelpunkt zentriert. Materialien bleiben an den Flächen.
|
||
static func _normalisiert(mesh: ArrayMesh) -> ArrayMesh:
|
||
var aabb := mesh.get_aabb()
|
||
var offset := Vector3(-aabb.get_center().x, -aabb.position.y, -aabb.get_center().z)
|
||
var out := ArrayMesh.new()
|
||
out.resource_name = mesh.resource_name
|
||
for s in mesh.get_surface_count():
|
||
var arrays: Array = mesh.surface_get_arrays(s)
|
||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||
continue
|
||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||
for i in verts.size():
|
||
verts[i] = verts[i] + offset
|
||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||
var mat: Material = mesh.surface_get_material(s)
|
||
if mat != null:
|
||
out.surface_set_material(out.get_surface_count() - 1, mat)
|
||
return out
|
||
|
||
|
||
# ------------------------------------------------------- Camo-Textur --------
|
||
|
||
## Camo-Fix (t_50a07d4b): Multicam-Textur von der Platte laden und auf alle
|
||
## Flächen ohne albedo_texture setzen (GLTFDocument löst externe URIs der
|
||
## Sibling-Repos nicht auf). Zuordnung exakt wie tools/CAMO_TEXTURE_MAPPING.md
|
||
## bzw. gen_thumbs._camo_tex_setzen — diese Funktion ist ab jetzt DIE EINE
|
||
## Quelle der Zuordnung (gen_thumbs delegiert hierher). Objekte außerhalb
|
||
## military_desert/<kategorie>/<oid>.glb bleiben unangetastet. Fehlt die
|
||
## Textur-Datei, wird klar gewarnt (push_warning) statt still weiß zu rendern.
|
||
##
|
||
## EINE-Quelle-Dedup (t_50a07d4b): GLTFDocument lädt externe Texturen
|
||
## inzwischen selbst — aber als EIGENE ImageTexture-Instanz JEDES Parses
|
||
## (291 GLBs = 291 × 16 MB VRAM statt 2 × 16 MB, bewiesen per Probe:
|
||
## iid pro Parse unterschiedlich, Inhalt identisch zur Datei). Deshalb wird
|
||
## hier ZUSÄTZLICH geprüft: stimmt der Inhalt einer GLTF-Textur mit einer
|
||
## Multicam-Datei überein, ersetzen wir sie durch DIE eine geteilte Instanz
|
||
## aus dem Cache — gleiche Optik (WYSIWYG bleibt), eine Quelle statt Kopien.
|
||
## Eingebettete/Fremd-Texturen (Alt-Bestand, Waffen-GLBs) bleiben unangetastet.
|
||
static func camo_tex_setzen(mesh: Mesh, glb_pfad: String) -> void:
|
||
# Siehe Kopf-Kommentar: nur Flächen OHNE aufgelöste Textur werden versorgt.
|
||
var tex_dir := glb_pfad.get_base_dir().path_join("../../../textures/military_desert")
|
||
for s in mesh.get_surface_count():
|
||
var mat := mesh.surface_get_material(s)
|
||
if not (mat is BaseMaterial3D):
|
||
continue
|
||
var bm := mat as BaseMaterial3D
|
||
if bm.albedo_texture != null:
|
||
continue
|
||
var datei := pack_datei_fuer(bm.resource_name)
|
||
var tex := _camo_tex(tex_dir.path_join(datei))
|
||
if tex != null:
|
||
bm.albedo_texture = tex
|
||
else:
|
||
push_warning("GlbMeshStore: Pack-Textur fehlt fuer Material '%s': %s" % [bm.resource_name, datei])
|
||
|
||
|
||
## Materialname (GLTF) -> Pack-Texturdatei. Quelle: tools/PACK_TEXTURE_MAPPING.md (Client).
|
||
static func pack_datei_fuer(material_name: String) -> String:
|
||
match material_name:
|
||
"picture_1":
|
||
return PACK_TEX_PICTURE_1
|
||
"picture_2":
|
||
return PACK_TEX_PICTURE_2
|
||
"picture_3":
|
||
return PACK_TEX_PICTURE_3
|
||
"military_base":
|
||
return PACK_TEX_PALETTE
|
||
if material_name.begins_with("military_base"):
|
||
return PACK_TEX_PALETTE
|
||
return PACK_TEX_PALETTE
|
||
|
||
static func _camo_image(pfad: String, warnen: bool) -> Image:
|
||
if _camo_bild_cache.has(pfad):
|
||
var v: Variant = _camo_bild_cache[pfad]
|
||
return v if v is Image else null
|
||
if not FileAccess.file_exists(pfad):
|
||
if warnen:
|
||
push_warning("GlbMeshStore: Camo-Textur fehlt: %s" % pfad)
|
||
_camo_bild_cache[pfad] = false
|
||
return null
|
||
var img := Image.load_from_file(pfad)
|
||
if img == null:
|
||
if warnen:
|
||
push_warning("GlbMeshStore: Camo-Textur nicht ladbar: %s" % pfad)
|
||
_camo_bild_cache[pfad] = false
|
||
return null
|
||
_camo_bild_cache[pfad] = img
|
||
return img
|
||
|
||
|
||
## DIE EINE geteilte ImageTexture für eine Multicam-Datei (VRAM-Cache).
|
||
## Alle getextureden GLB-Flächen teilen sich genau diese Instanz.
|
||
static func _camo_tex(tex_pfad: String) -> ImageTexture:
|
||
if _camo_tex_cache.has(tex_pfad):
|
||
var v: Variant = _camo_tex_cache[tex_pfad]
|
||
return v if v is ImageTexture else null
|
||
var img := _camo_image(tex_pfad, true)
|
||
if img == null:
|
||
_camo_tex_cache[tex_pfad] = false
|
||
return null
|
||
var tex := ImageTexture.create_from_image(img)
|
||
_camo_tex_cache[tex_pfad] = tex
|
||
return tex
|
||
|
||
|
||
## Ersetzt eine GLTF-eigene ImageTexture durch die geteilte Multicam-Instanz,
|
||
## WENN der Inhalt (Größe, Format, Pixeldaten) mit einer der Kandidaten-
|
||
## Dateien übereinstimmt. null = kein Match → Textur bleibt unangetastet
|
||
## (eingebettete/Fremd-Texturen). Urteil je Instanz gecacht.
|
||
static func _camo_ersetzung_fuer(tex: Texture2D, kandidaten: Array) -> ImageTexture:
|
||
if not (tex is ImageTexture):
|
||
return null
|
||
var iid := tex.get_instance_id()
|
||
if _camo_ersetzung_cache.has(iid):
|
||
var v: Variant = _camo_ersetzung_cache[iid]
|
||
return v if v is ImageTexture else null
|
||
var img: Image = (tex as ImageTexture).get_image()
|
||
if img == null:
|
||
_camo_ersetzung_cache[iid] = false
|
||
return null
|
||
var out: ImageTexture = null
|
||
for pfad in kandidaten:
|
||
var vergleich := _camo_image(str(pfad), false)
|
||
if vergleich == null:
|
||
continue
|
||
if img.get_width() == vergleich.get_width() \
|
||
and img.get_height() == vergleich.get_height() \
|
||
and img.get_format() == vergleich.get_format() \
|
||
and img.data == vergleich.data:
|
||
out = _camo_tex(str(pfad))
|
||
break
|
||
_camo_ersetzung_cache[iid] = out if out != null else false
|
||
return out
|