extends SceneTree ## Thumbnail-Generator für die Objekt-Bibliothek (Karte t_3a998ae4). ## Rendert JEDESM Bibliotheks-Objekt einmalig als 128-px-PNG nach ## library/thumbs/.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.7 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 = 1.1 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). func _mesh_fuer(oid: String, col: Dictionary) -> Mesh: var basis := ProjectSettings.globalize_path("res://") for quelle in GLB_QUELLEN: var pfad := basis.path_join(str(quelle)).path_join(oid + ".glb") if FileAccess.file_exists(pfad): var m := _glb_mesh(pfad) if m != null: return m return MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0) ## 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