185 lines
6.6 KiB
GDScript
185 lines
6.6 KiB
GDScript
extends SceneTree
|
|
## Thumbnail-Generator für die Objekt-Bibliothek (Karte t_3a998ae4).
|
|
## Rendert JEDESM Bibliotheks-Objekt einmalig als 128-px-PNG nach
|
|
## library/thumbs/<id>.png — die Bilder werden committed, der Editor lädt sie
|
|
## dann einfach aus der Datei (LibraryBrowser.load_thumb).
|
|
##
|
|
## Ausführen (braucht echtes Rendering, NICHT --headless):
|
|
## xvfb-run -a godot4 --rendering-driver opengl3 --path . --script res://tools/gen_thumbs.gd
|
|
## (auf Slys PC auch ohne xvfb: godot4 --rendering-driver opengl3 --path . --script ...)
|
|
##
|
|
## Natur-Update (t_7cf80244): wird das echte Mesh im Mesh-Lager gefunden
|
|
## (Sibling-Repo scopedvr-match-server oder scopedvr-client, assets/objekte),
|
|
## wird das GLB direkt gerendert (GLTFDocument, ohne Import-System) — die
|
|
## Bibliothek zeigt dann die echten Bäume/Felsen/Pilze, nicht nur den
|
|
## Kollisions-Platzhalter. Ohne GLB: Platzhalter wie bisher.
|
|
##
|
|
## Exit 0 = alle Thumbnails geschrieben.
|
|
|
|
const SIZE := 128
|
|
const BG_COLOR := Color(0.16, 0.18, 0.21)
|
|
## Relative Quellen für echte GLB-Meshes (vom Editor-Projektordner aus).
|
|
const GLB_QUELLEN := ["../scopedvr-match-server/assets/objekte", "../scopedvr-client/assets/objekte"]
|
|
|
|
var manifest: Dictionary = {}
|
|
var ids: Array = []
|
|
var idx := -1
|
|
var settle_frames := 0
|
|
var vp: SubViewport
|
|
var mi: MeshInstance3D
|
|
var cam: Camera3D
|
|
var out_dir := ""
|
|
|
|
|
|
func _initialize() -> void:
|
|
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(SvmapIO.MANIFEST_PATH))
|
|
if parsed is Dictionary:
|
|
manifest = parsed
|
|
ids = (manifest.get("objects", {}) as Dictionary).keys()
|
|
if ids.is_empty():
|
|
push_error("gen_thumbs: Bibliothek leer — nichts zu rendern.")
|
|
quit(1)
|
|
return
|
|
out_dir = ProjectSettings.globalize_path("res://library/thumbs")
|
|
DirAccess.make_dir_recursive_absolute(out_dir)
|
|
|
|
vp = SubViewport.new()
|
|
vp.size = Vector2i(SIZE, SIZE)
|
|
vp.own_world_3d = true
|
|
vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(vp)
|
|
|
|
var env := Environment.new()
|
|
env.background_mode = Environment.BG_COLOR
|
|
env.background_color = BG_COLOR
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
|
env.ambient_light_color = Color(1, 1, 1)
|
|
env.ambient_light_energy = 0.35
|
|
var we := WorldEnvironment.new()
|
|
we.environment = env
|
|
vp.add_child(we)
|
|
|
|
var sun := DirectionalLight3D.new()
|
|
sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0)
|
|
sun.light_energy = 0.85
|
|
vp.add_child(sun)
|
|
|
|
mi = MeshInstance3D.new()
|
|
vp.add_child(mi)
|
|
cam = Camera3D.new()
|
|
cam.current = true
|
|
vp.add_child(cam)
|
|
idx = -1 # erst im ersten _process starten — look_at braucht eingetretene Knoten
|
|
|
|
|
|
func _next() -> void:
|
|
idx += 1
|
|
if idx >= ids.size():
|
|
print("GEN_THUMBS_OK: %d Thumbnails in %s" % [ids.size(), out_dir])
|
|
quit(0)
|
|
return
|
|
var oid := str(ids[idx])
|
|
var entry: Dictionary = (manifest["objects"] as Dictionary)[oid]
|
|
var col: Dictionary = entry.get("collision", {})
|
|
# Trimesh-Objekte (Modular-Builder-Export, t_82d2a8dd): das Thumbnail wurde
|
|
# beim Export aus dem ECHTEN Mesh gerendert (der Platzhalter hier wäre nur
|
|
# eine Box) — existiert die Datei schon, wird sie nicht überschrieben.
|
|
if str(col.get("shape", "box")) == "trimesh" and FileAccess.file_exists(out_dir + "/" + oid + ".png"):
|
|
print(" thumb: %s übersprungen (echtes Thumbnail vom Export)" % oid)
|
|
_next()
|
|
return
|
|
var mesh := _mesh_fuer(oid, col)
|
|
mi.mesh = mesh
|
|
# Kamera diagonal aufs Objekt, passend zur Mesh-Größe (Kiste nah, Turm weit).
|
|
var aabb := mesh.get_aabb()
|
|
var center := aabb.get_center()
|
|
var radius := aabb.size.length() * 0.5
|
|
var dir := Vector3(1.0, 0.65, 1.0).normalized()
|
|
cam.position = center + dir * (radius * 2.3 + 0.3)
|
|
cam.look_at(center, Vector3.UP)
|
|
settle_frames = 4
|
|
|
|
|
|
func _process(_delta: float) -> bool:
|
|
if idx < 0:
|
|
_next()
|
|
return false
|
|
if settle_frames > 0:
|
|
settle_frames -= 1
|
|
return false
|
|
var img := vp.get_texture().get_image()
|
|
var oid := str(ids[idx])
|
|
img.save_png(out_dir + "/" + oid + ".png")
|
|
print(" thumb: %s (%dx%d)" % [oid, img.get_width(), img.get_height()])
|
|
_next()
|
|
return false
|
|
|
|
|
|
# ---------------------------------------------------- echtes Mesh laden ------
|
|
|
|
## Mesh für ein Thumbnail: echtes GLB aus dem Mesh-Lager (Sibling-Repos),
|
|
## sonst Kollisions-Platzhalter (alte Fallback-Regel).
|
|
##
|
|
## A6-Fundament (19.10.): Military-Objekte liegen KATEGORIE-verschachtelt
|
|
## (assets/objekte/military_desert/<kategorie>/<oid>.glb, Pfad im Registry-Feld
|
|
## "glb"). Deshalb stehen die Kandidaten jetzt in dieser Reihenfolge:
|
|
## 1. Registry-`glb` gegen jede QUELLEN-WURZEL (deren "/assets/objekte"-Suffix
|
|
## entfernt, damit derselbe relative Pfad aufgeht),
|
|
## 2. flache Alt-Ablage <quelle>/<oid>.glb.
|
|
func _mesh_fuer(oid: String, col: Dictionary) -> Mesh:
|
|
for pfad in _kandidaten(oid):
|
|
if FileAccess.file_exists(pfad):
|
|
var m := _glb_mesh(pfad)
|
|
if m != null:
|
|
# Camo-Textur-EINE-Quelle (t_50a07d4b): die Zuordnung/Logik
|
|
# lebt jetzt in GlbMeshStore.camo_tex_setzen (Live-Pfad
|
|
# Editor-Viewport nutzt denselben Code, WYSIWYG garantiert).
|
|
GlbMeshStore.camo_tex_setzen(m, pfad)
|
|
return m
|
|
return MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
|
|
|
|
|
|
## Kandidaten-Pfade (absolut) für eine OID — Registry-Pfad vor flacher Alt-Ablage.
|
|
func _kandidaten(oid: String) -> Array:
|
|
var basis := ProjectSettings.globalize_path("res://")
|
|
var out: Array = []
|
|
var eintrag: Dictionary = (manifest.get("objects", {}) as Dictionary).get(oid, {})
|
|
var rel := str(eintrag.get("glb", ""))
|
|
if rel.begins_with("res://"):
|
|
var sub := rel.substr(6)
|
|
for quelle in GLB_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))
|
|
for quelle in GLB_QUELLEN:
|
|
out.append(basis.path_join(str(quelle)).path_join(oid + ".glb"))
|
|
return out
|
|
|
|
|
|
## GLB OHNE Import-System laden (GLTFDocument) und das erste Mesh liefern.
|
|
## null = Load-Fehlschlag → Aufrufer fällt auf den Platzhalter zurück.
|
|
func _glb_mesh(pfad: String) -> Mesh:
|
|
var gltf := GLTFDocument.new()
|
|
var state := GLTFState.new()
|
|
if gltf.append_from_file(pfad, state) != OK:
|
|
push_warning("gen_thumbs: GLB nicht ladbar: %s" % pfad)
|
|
return null
|
|
var szene: Node = gltf.generate_scene(state)
|
|
if szene == null:
|
|
return null
|
|
var m := _erstes_mesh(szene)
|
|
# Das Mesh ist eine Resource (RefCounted) — das Freigeben der Szene
|
|
# berührt die Referenz im Rückgabewert nicht.
|
|
szene.free()
|
|
return m
|
|
|
|
|
|
func _erstes_mesh(n: Node) -> Mesh:
|
|
if n is MeshInstance3D and (n as MeshInstance3D).mesh != null:
|
|
return (n as MeshInstance3D).mesh
|
|
for c in n.get_children():
|
|
var m := _erstes_mesh(c)
|
|
if m != null:
|
|
return m
|
|
return null
|