From 95ddcb40b36e163145ddce987d102e03db8f2d63 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 6 Aug 2026 07:40:57 +0200 Subject: [PATCH] Tree Gen 1.28.0: Faell-Teile mit exakter Ast-Zuordnung (Punkt 4) Knopf "Faell-Teile exportieren": Stamm in 2-3 Segmente, jeder Ast als eigenes Teil SAMT seinen Sub-Aesten und Blatt-Cards, dazu _Pieces_All.fbx mit gemeinsamem Ursprung. Je Teil zwei Material-Slots, Pivot am Massenmittelpunkt, eigene UCX-Huelle. Die Ast-Zugehoerigkeit ist ein ATTRIBUT, keine Schaetzung - wie von der UE-Seite vorgeschlagen. Der Generator legt "ast_id" an den Astpunkten ab (0 = Stamm, 1..N = Aeste). Sub-Aeste erben die ID ihres Astes automatisch, weil sie aus Punkten auf dessen Kurve entstehen; nachgemessen: bei Sub Count 0 hat jeder Ast 28 Verts, bei 3 dann 64, der Stamm bleibt konstant bei 156. Beim Blatt-Streuen wird ast_id am Punkt abgegriffen und auf die Card gespeichert. Damit ist die Zuordnung exakt nachzaehlbar, und genau das prueft der Test: Summe der Cards ueber alle Teile == Cards des Stand-Baums. 170 = 170. Die Falle, die diese Bilanz aufgedeckt hat: die Z-Schnitte am Stamm zerschnitten anfangs auch Blatt-Cards. Aus einer wurden zwei, die Bilanz stand auf 178 gegen 170 - und die Atlas-UVs der Haelften waeren kaputt gewesen. Eine Card ist ein unteilbares Billboard; sie wandert jetzt als GANZES in das Segment, in dem ihre Inselmitte liegt. Nur die Rinde wird geschnitten, ihre Schlagflaechen werden gedeckelt. Massenmittelpunkt flaechengewichtet statt Vertex-Mittelwert: wo das Mesh dicht tesselliert ist (Astansatz), wuerde der Mittelwert den Pivot dorthin ziehen und das Teil torkelte beim Fallen. Alles am REIMPORTIERTEN FBX gemessen: 12 Teile (3 Stamm, 9 Ast), UCX 18-32 Verts im Budget, Pivot maximal 16 mm vom Schwerpunkt, 0 non-manifold Kanten, Gesamtdatei 5.41 m hoch - also volle Baumhoehe und deckungsgleich rekonstruierbar. Nebenbei: _deselect fiel ueber None-Eintraege in der Objektliste, die beim Entfernen von Objekten zwischen den Teilen staendig auftreten. Neu: tests/test_faell_teile.py. 21 Tests bestehen. Co-Authored-By: Claude Opus 4.8 --- README.md | 36 ++ blender_manifest_tree.toml | 2 +- dist/index.html | 6 +- dist/index.json | 8 +- dist/stylized_tree_generator-1.27.0.zip | Bin 29267 -> 0 bytes dist/stylized_tree_generator-1.28.0.zip | Bin 0 -> 33772 bytes stylized_tree_generator.py | 446 +++++++++++++++++++++++- tests/test_faell_teile.py | 193 ++++++++++ 8 files changed, 679 insertions(+), 12 deletions(-) delete mode 100644 dist/stylized_tree_generator-1.27.0.zip create mode 100644 dist/stylized_tree_generator-1.28.0.zip create mode 100644 tests/test_faell_teile.py diff --git a/README.md b/README.md index b541d15..0a65a5b 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,41 @@ Zwei Fallen beim Bauen: - `join` loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach tot. Die Aufraeumliste fuehrt deshalb **Namen** statt Objekte. +### Faell-Teile (Punkt 4) + +Kein Laufzeit-Zerlegen: beim Faellen tauscht das Spiel die ISM-Instanz gegen +einen Actor aus vordefinierten Teilen. Knopf **Faell-Teile exportieren**: + +| Teil | Inhalt | +|---|---| +| `_Piece_Stamm01..03` | Stammsegmente, Schnittflaechen mit Deckel | +| `_Piece_Ast01..N` | je ein Ast **samt seinen Sub-Aesten und Blatt-Cards** | +| `_Pieces_All.fbx` | alle Teile an ihrer Originalposition, gemeinsamer Ursprung | + +Je Teil: zwei Material-Slots, Pivot = **Massenmittelpunkt**, eigene `UCX_`-Huelle. + +**Die Ast-Zugehoerigkeit ist ein Attribut, keine Schaetzung.** Der Generator legt +`ast_id` an den Astpunkten ab (0 = Stamm, 1..N = Aeste); Sub-Aeste erben die ID +ihres Astes automatisch, weil sie aus Punkten auf dessen Kurve entstehen — +gemessen: bei `Sub Count` 0 hat jeder Ast 28 Verts, bei 3 dann 64, der Stamm +bleibt bei 156. Beim Blatt-Streuen wird `ast_id` am Punkt abgegriffen und auf +die Card gespeichert. „Welche Card gehoert zu welchem Ast" ist damit eine +Nachschlage-Frage statt einer Nearest-Neighbor-Heuristik — und **exakt +nachzaehlbar**: + +> `tests/test_faell_teile.py` prueft die **Blatt-Bilanz**: Summe der Cards ueber +> alle Teile == Cards des Stand-Baums. Gemessen **170 = 170**. + +**Falle dabei:** Die Z-Schnitte am Stamm zerschnitten anfangs auch Blatt-Cards — +aus einer wurden zwei, die Bilanz stand auf 178 gegen 170, und die Atlas-UVs der +Haelften waeren kaputt gewesen. Eine Card ist ein **unteilbares** Billboard: +sie wandert jetzt als Ganzes in das Segment, in dem ihre Inselmitte liegt. +Nur die Rinde wird geschnitten. + +Alles am **reimportierten** FBX gemessen: 12 Teile, UCX 18–32 Verts im Budget, +Pivot maximal 16 mm vom Schwerpunkt, 0 non-manifold Kanten, Gesamtdatei 5.41 m +hoch (= volle Baumhoehe, die Teile sind also deckungsgleich rekonstruierbar). + ### Proportionen: gemessen, nicht geschaetzt Drei Presets sahen falsch aus. Die Ursachen wurden gemessen bzw. belegt, nicht @@ -606,6 +641,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`. | Datei | prueft | |---|---| | `tests/test_baum_export.py` | Stand-Baum: ein Mesh, zwei Slots, Ursprung, **Kugel-Normalen im exportierten FBX** | +| `tests/test_faell_teile.py` | Faell-Teile: **Blatt-Bilanz 170=170**, Pivot am Massenmittelpunkt, UCX je Teil, Gesamtdatei mit gemeinsamem Ursprung | | `tests/test_blatt_varianten.py` | Card-Sammlung: alle Varianten werden gezogen, seed-stabil, Einzelobjekt weiter moeglich | | `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 | | `tests/test_bruchstuecke.py` | Bruchstuecke: Volumen 100 % des Muttersteins, dichte Topologie, genau ein Erz-Stueck, Material-Slots, Ursprung, UCX je Teil | diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 1375eff..a214939 100644 --- a/blender_manifest_tree.toml +++ b/blender_manifest_tree.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_tree_generator" -version = "1.27.0" +version = "1.28.0" name = "Stylized Tree Generator" tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen" maintainer = "D4rkst3r" diff --git a/dist/index.html b/dist/index.html index 4e434ed..9fea14a 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.27.0 + stylized_tree_generator-1.28.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 28.6KB + 33.0KB -

Built 2026-08-06, 04:48

+

Built 2026-08-06, 05:40

diff --git a/dist/index.json b/dist/index.json index c42e433..b53dc80 100644 --- a/dist/index.json +++ b/dist/index.json @@ -82,7 +82,7 @@ "id": "stylized_tree_generator", "name": "Stylized Tree Generator", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", - "version": "1.27.0", + "version": "1.28.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.27.0.zip", - "archive_size": 29267, - "archive_hash": "sha256:daeb815cde034e527cdffb0e76a980e964f228b7a0f39070ba78477be9fc3e57" + "archive_url": "./stylized_tree_generator-1.28.0.zip", + "archive_size": 33772, + "archive_hash": "sha256:84ff026fecccddc944a1f847d832f3d8b45b37f142bb9e1cf342a103e2ca290f" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.27.0.zip b/dist/stylized_tree_generator-1.27.0.zip deleted file mode 100644 index 457c7287d5d6eaadfcd3d70b3171f1cdf2576982..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29267 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 ZfBr&cIY_AgJc9k(|0HK1{m%cn{TH$dvw;8r diff --git a/dist/stylized_tree_generator-1.28.0.zip b/dist/stylized_tree_generator-1.28.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..cd2af8a9840e62fea7edb1fbd083c7852e4ec2ed GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index a4e33ca..446d7a1 100644 --- a/stylized_tree_generator.py +++ b/stylized_tree_generator.py @@ -1,7 +1,7 @@ bl_info = { "name": "Stylized Tree Generator", "author": "D4rkst3r", - "version": (1, 27, 0), + "version": (1, 28, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -19,6 +19,7 @@ bl_info = { import bpy import bmesh +import math import os from mathutils import Matrix, Vector import sys @@ -589,8 +590,24 @@ def build_group(): _sock(anch, "Name").default_value = "anchor" L(apos.outputs[0], _sock(anch, "Value")) + # AST-INDEX: jeder Punkt auf dem Stamm ist der Ansatz genau EINES Astes. + # Sein Index wird als Attribut mitgeschleift, damit spaeter feststeht, welche + # Geometrie - und vor allem welche Blatt-Card - zu welchem Ast gehoert. + # Ohne das muesste man die Zuordnung raeumlich raten (Nearest Neighbor); + # so ist sie eine Nachschlage-Frage und exakt nachzaehlbar. + # 0 bleibt fuer den Stamm reserviert, Aeste zaehlen ab 1. + aidx = N("GeometryNodeInputIndex"); aidx.location = (900, 560) + aplus = N("ShaderNodeMath"); aplus.location = (1050, 560) + aplus.operation = 'ADD'; aplus.inputs[1].default_value = 1.0 + L(aidx.outputs[0], aplus.inputs[0]) + astid = N("GeometryNodeStoreNamedAttribute"); astid.location = (1200, 400) + astid.domain = 'POINT'; astid.data_type = 'INT' + L(_out(anch, "Geometry"), _sock(astid, "Geometry")) + _sock(astid, "Name").default_value = "ast_id" + L(aplus.outputs[0], _sock(astid, "Value")) + inst = N("GeometryNodeInstanceOnPoints"); inst.location = (1400, 320) - L(_out(anch, "Geometry"), _sock(inst, "Points")) + L(_out(astid, "Geometry"), _sock(inst, "Points")) L(_out(bsetr, "Curve"), _sock(inst, "Instance")) L(pfin.outputs[0], _sock(inst, "Scale")) if align is not None: @@ -1293,8 +1310,22 @@ def build_leaf_group(rebuild=False): L(slo.outputs[0], rsc.inputs[2]) L(V["Size"], rsc.inputs[3]) + # AST-INDEX auf die Blattpunkte uebernehmen. Der Baum traegt "ast_id" je + # Vertex; beim Streuen wird er am Punkt abgegriffen und mitgespeichert. + # Damit ist "welche Card gehoert zu welchem Ast" spaeter eine + # Nachschlage-Frage und keine Nearest-Neighbor-Schaetzung - und der Test + # kann die Zuordnung exakt nachzaehlen. + lna = N("GeometryNodeInputNamedAttribute"); lna.location = (-320, 60) + lna.data_type = 'INT' + _sock(lna, "Name").default_value = "ast_id" + lstore = N("GeometryNodeStoreNamedAttribute"); lstore.location = (-100, 200) + lstore.domain = 'POINT'; lstore.data_type = 'INT' + L(_out(dist, "Points"), _sock(lstore, "Geometry")) + _sock(lstore, "Name").default_value = "ast_id" + L(_out(lna, "Attribute"), _sock(lstore, "Value")) + inst = N("GeometryNodeInstanceOnPoints"); inst.location = (100, 200) - L(_out(dist, "Points"), _sock(inst, "Points")) + L(_out(lstore, "Geometry"), _sock(inst, "Points")) L(_out(quelle, "Output"), _sock(inst, "Instance")) try: L(V["Use Collection"], _sock(inst, "Pick Instance")) @@ -1493,6 +1524,214 @@ def _merge_for_export(context, tree_ob, leaf_ob, name): return stamm, made + +# --------------------------------------------------------------------------- +# Faell-Teile: Stammsegmente und Aeste als eigene Physik-Assets +# --------------------------------------------------------------------------- + +def _tree_ucx(quell_objs, name, max_verts=32): + """Konvexe Huelle als `UCX_` - gleiches Muster wie bei den Felsen. + + Nur die PUNKTE einlesen: mit `from_mesh` kaemen die Originalflaechen mit und + die Huelle laege obendrauf statt darum herum. + """ + bm = bmesh.new() + for o in quell_objs: + if o.type == 'MESH': + for v in o.data.vertices: + bm.verts.new(o.matrix_world @ v.co) + if len(bm.verts) < 4: + bm.free() + return None + bm.verts.ensure_lookup_table() + res = bmesh.ops.convex_hull(bm, input=bm.verts[:], use_existing_faces=False) + weg = list(set(res.get("geom_interior", []) + res.get("geom_unused", []))) + if weg: + bmesh.ops.delete(bm, geom=weg, context='VERTS') + bm.verts.ensure_lookup_table() + bm.normal_update() + if not bm.faces: + bm.free() + return None + + quelle = [v.co.copy() for v in bm.verts] + winkel = math.radians(1.0) + while len(bm.verts) > max_verts and winkel < math.radians(60.0): + bmesh.ops.dissolve_limit(bm, angle_limit=winkel, + verts=bm.verts[:], edges=bm.edges[:]) + bmesh.ops.delete(bm, geom=[v for v in bm.verts if not v.link_faces], + context='VERTS') + bm.verts.ensure_lookup_table() + if not bm.faces: + break + winkel += math.radians(3.0) + bm.normal_update() + + # Das Vereinfachen schneidet Ecken ab - die Huelle waere danach KLEINER als + # das Teil und der Spieler wuerde einsinken. Exakt so weit aufblasen, dass + # wieder alles hineinpasst. + if bm.faces and quelle: + mitte = Vector((0.0, 0.0, 0.0)) + for v in bm.verts: + mitte += v.co + mitte /= max(len(bm.verts), 1) + faktor = 1.0 + for f in bm.faces: + n = f.normal + if n.length_squared < 1e-12: + continue + d = n.dot(f.verts[0].co) + basis = d - n.dot(mitte) + if basis <= 1e-9: + continue + for co in quelle: + noetig = (n.dot(co) - n.dot(mitte)) / basis + if noetig > faktor: + faktor = noetig + if faktor > 1.0: + for v in bm.verts: + v.co = mitte + (v.co - mitte) * faktor + + bmesh.ops.triangulate(bm, faces=bm.faces[:]) + me = bpy.data.meshes.new("UCX_" + name) + bm.to_mesh(me) + bm.free() + ob = bpy.data.objects.new("UCX_" + name, me) + bpy.context.scene.collection.objects.link(ob) + return ob + + +def _mass_center(me): + """Massenmittelpunkt als flaechengewichteter Schwerpunkt der Faces. + + Fuer einen Physik-Actor ist der Pivot der Drehpunkt - liegt er in einer Ecke, + torkelt das Teil beim Fallen. Der reine Vertex-Mittelwert taugt nicht: dort, + wo das Mesh dicht tesselliert ist (Astansatz), zieht er den Punkt hin. + Flaechengewichtet trifft es deutlich besser und funktioniert auch bei + offener Geometrie - die Blatt-Cards sind offen. + """ + gesamt = 0.0 + acc = Vector((0.0, 0.0, 0.0)) + for p in me.polygons: + a = p.area + if a <= 0.0: + continue + acc += p.center * a + gesamt += a + if gesamt <= 0.0: + if not me.vertices: + return Vector((0.0, 0.0, 0.0)) + m = Vector((0.0, 0.0, 0.0)) + for v in me.vertices: + m += v.co + return m / len(me.vertices) + return acc / gesamt + + +def _teil_aus_ids(context, quellen, ids, name, z_von=None, z_bis=None): + """Baut ein Teil-Objekt aus allen Faces, deren ast_id in `ids` liegt. + + `quellen` ist eine Liste (Objekt, Material-Slot). Optional wird zusaetzlich + nach Hoehe geschnitten - so entstehen die Stammsegmente. + """ + rinde = _get_or_create_mat(TREE_MATERIALS[0], (0.29, 0.21, 0.14, 1.0)) + laub = _get_or_create_mat(TREE_MATERIALS[1], (0.18, 0.34, 0.14, 1.0)) + + bm = bmesh.new() + lay = None + for quelle, slot in quellen: + me = quelle.data + attr = me.attributes.get("ast_id") + if attr is None: + continue + werte = [d.value for d in attr.data] + tmp = bmesh.new() + tmp.from_mesh(me) + tmp.transform(quelle.matrix_world) + tmp.verts.ensure_lookup_table() + tmp.faces.ensure_lookup_table() + # Faces behalten, deren Vertices die passende ID tragen. + weg = [] + for f in tmp.faces: + fid = werte[f.verts[0].index] if f.verts[0].index < len(werte) else 0 + if fid not in ids: + weg.append(f) + if weg: + bmesh.ops.delete(tmp, geom=weg, context='FACES') + if z_von is not None or z_bis is not None: + if slot == 1: + # Blatt-Cards NICHT schneiden. Eine Card ist ein unteilbares + # Billboard - halbiert man sie, entstehen zwei Faces mit + # kaputten Atlas-UVs, und die Bilanz stimmt nicht mehr + # (gemessen 178 Cards in den Teilen gegen 170 im Stand-Baum). + # Stattdessen wandert jede Card als GANZES in das Segment, in + # dem ihre Inselmitte liegt. + inseln = [] + offen = set(tmp.faces) + while offen: + start = offen.pop() + insel = [start] + stapel = [start] + while stapel: + f = stapel.pop() + for e in f.edges: + for nf in e.link_faces: + if nf in offen: + offen.discard(nf) + insel.append(nf) + stapel.append(nf) + inseln.append(insel) + weg2 = [] + for insel in inseln: + mz = sum(f.calc_center_median().z for f in insel) / len(insel) + drin = ((z_von is None or mz >= z_von) + and (z_bis is None or mz < z_bis)) + if not drin: + weg2.extend(insel) + if weg2: + bmesh.ops.delete(tmp, geom=weg2, context='FACES') + else: + if z_von is not None: + bmesh.ops.bisect_plane( + tmp, geom=tmp.verts[:] + tmp.edges[:] + tmp.faces[:], + plane_co=Vector((0, 0, z_von)), plane_no=Vector((0, 0, -1)), + clear_outer=True) + if z_bis is not None and tmp.faces: + bmesh.ops.bisect_plane( + tmp, geom=tmp.verts[:] + tmp.edges[:] + tmp.faces[:], + plane_co=Vector((0, 0, z_bis)), plane_no=Vector((0, 0, 1)), + clear_outer=True) + # Schlagflaechen schliessen - offene Segmente taugen nicht als + # Physik-Actor und die UCX-Huelle waere sinnlos. + rand = [e for e in tmp.edges if len(e.link_faces) == 1] + if rand: + bmesh.ops.holes_fill(tmp, edges=rand, sides=0) + if not tmp.faces: + tmp.free() + continue + # Material-Index setzen, bevor es in den Sammel-bmesh geht. + for f in tmp.faces: + f.material_index = slot + hilf = bpy.data.meshes.new("__t") + tmp.to_mesh(hilf) + tmp.free() + bm.from_mesh(hilf) + bpy.data.meshes.remove(hilf) + + if not bm.faces: + bm.free() + return None + + me_neu = bpy.data.meshes.new(name) + bm.to_mesh(me_neu) + bm.free() + me_neu.materials.append(rinde) + me_neu.materials.append(laub) + ob = bpy.data.objects.new(name, me_neu) + context.scene.collection.objects.link(ob) + return ob + + # --------------------------------------------------------------------------- # Addon-UI # --------------------------------------------------------------------------- @@ -1571,6 +1810,29 @@ class TreeGenSettings(PropertyGroup): name="_Frucht-Ebene anlegen", default=False, description="Zusaetzlich _Frucht (gleicher Ursprung) fuer die Frucht-Ebene", ) + piece_trunk_segments: IntProperty( + name="Stamm-Segmente", default=3, min=2, max=3, + description="In wie viele Stuecke der Stamm beim Faellen zerfaellt", + ) + piece_ucx: BoolProperty( + name="UCX je Teil", default=True, + description="Legt je Teil eine konvexe Kollisionshuelle ins FBX", + ) + piece_ucx_verts: IntProperty( + name="UCX max. Verts", default=32, min=8, max=128, + description="Obergrenze fuer die Kollisionshuelle je Teil", + ) + piece_export_combined: BoolProperty( + name="Zusatzdatei mit gemeinsamem Ursprung", default=True, + description=("Schreibt zusaetzlich _Pieces_All.fbx, in der alle " + "Teile an ihrer Originalposition stehen - damit Unreal den " + "zusammengesetzten Baum deckungsgleich aufbauen kann"), + ) + piece_keep: BoolProperty( + name="Teile in der Szene behalten", default=False, + description="Laesst die erzeugten Teile zum Nachschauen stehen", + ) + export_dir: StringProperty( name="Export-Ordner", default="", subtype='DIR_PATH', description="Zielordner fuer die spielfertigen FBX", @@ -1654,10 +1916,15 @@ def _safe_name(name): def _deselect(context): + # Die Objektliste kann nach dem Entfernen von Objekten kurzzeitig None- + # Eintraege und tote Referenzen enthalten - beim Piece-Export passiert das + # zwischen den Teilen staendig. for o in list(context.view_layer.objects): + if o is None: + continue try: o.select_set(False) - except (ReferenceError, RuntimeError): + except (ReferenceError, RuntimeError, AttributeError): pass @@ -1788,6 +2055,165 @@ class TREEGEN_OT_export_game(Operator): return {'FINISHED'} + +class TREEGEN_OT_export_pieces(Operator): + """Exportiert den Baum als Faell-Teile fuer das Zerlegen im Spiel. + + Kein Laufzeit-Zerlegen: beim Faellen tauscht das Spiel die ISM-Instanz gegen + einen Actor aus diesen vordefinierten Teilen. Sie muessen deshalb + vollwertige Assets sein - eigener Massenmittelpunkt als Pivot, zwei + Material-Slots, konvexe UCX-Huelle. + """ + bl_idname = "treegen.export_pieces" + bl_label = "Faell-Teile exportieren" + bl_description = ("Schneidet den Stamm in Segmente, loest die Aeste samt " + "ihren Blatt-Cards heraus und schreibt je Teil eine FBX " + "mit UCX-Huelle") + bl_options = {'REGISTER'} + + def execute(self, context): + s = context.scene.tree_gen_settings + verzeichnis = bpy.path.abspath(s.export_dir) if s.export_dir else "" + if not verzeichnis or not os.path.isdir(verzeichnis): + self.report({'ERROR'}, "Bitte einen gueltigen Export-Ordner waehlen.") + return {'CANCELLED'} + + baeume = [o for o in context.selected_objects + if o.type == 'MESH' + and not o.name.endswith(("_Leaf", "_Frucht"))] + if not baeume: + self.report({'ERROR'}, "Keinen Baum ausgewaehlt.") + return {'CANCELLED'} + + gesamt = 0 + for baum in baeume: + gesamt += self._ein_baum(context, s, baum, verzeichnis) + + self.report({'INFO'}, "%d Faell-Teil(e) nach %s." % (gesamt, verzeichnis)) + return {'FINISHED'} + + def _ein_baum(self, context, s, baum, verzeichnis): + blatt = bpy.data.objects.get(baum.name + "_Leaf") + basis = _safe_name(baum.name) + + # Ausgewertete Kopien: die GN-Modifier muessen aufgeloest sein, sonst + # gibt es keine Faces zum Schneiden. + dg = context.evaluated_depsgraph_get() + temp = [] + + def _ausgewertet(src): + me = bpy.data.meshes.new_from_object( + src.evaluated_get(dg), preserve_all_data_layers=True, depsgraph=dg) + ob = bpy.data.objects.new(src.name + "__src", me) + ob.matrix_world = src.matrix_world.copy() + context.scene.collection.objects.link(ob) + temp.append(ob.name) + return ob + + stamm_src = _ausgewertet(baum) + blatt_src = _ausgewertet(blatt) if blatt is not None else None + + quellen = [(stamm_src, 0)] + if blatt_src is not None: + quellen.append((blatt_src, 1)) + + # Welche Ast-IDs gibt es? + attr = stamm_src.data.attributes.get("ast_id") + if attr is None: + self.report({'WARNING'}, "%s traegt kein ast_id - bitte den Baum neu " + "erzeugen." % baum.name) + for n in temp: + o = bpy.data.objects.get(n) + if o: + bpy.data.objects.remove(o, do_unlink=True) + return 0 + ast_ids = sorted({d.value for d in attr.data if d.value > 0}) + + zs = [(stamm_src.matrix_world @ v.co).z for v in stamm_src.data.vertices] + z_unten, z_oben = min(zs), max(zs) + + teile = [] + # --- Stamm in Segmente ------------------------------------------ + n_seg = s.piece_trunk_segments + # Nur bis zum untersten Astansatz schneiden waere schoener, ist aber + # unnoetig: die Aeste sind eigene Teile, der Stamm-Anteil bleibt uebrig. + grenzen = [z_unten + (z_oben - z_unten) * i / n_seg + for i in range(n_seg + 1)] + for i in range(n_seg): + name = "%s_Piece_Stamm%02d" % (basis, i + 1) + ob = _teil_aus_ids(context, quellen, {0}, name, + z_von=grenzen[i], z_bis=grenzen[i + 1]) + if ob is not None: + teile.append(ob) + + # --- Aeste, jeder mit SEINEN Blatt-Cards ------------------------ + for k, aid in enumerate(ast_ids): + name = "%s_Piece_Ast%02d" % (basis, k + 1) + ob = _teil_aus_ids(context, quellen, {aid}, name) + if ob is not None: + teile.append(ob) + + # --- Gesamtdatei mit GEMEINSAMEM Ursprung ----------------------- + # Damit Unreal den zusammengesetzten Baum deckungsgleich rekonstruieren + # kann: hier stehen alle Teile noch an ihrer Originalposition. + if s.piece_export_combined and teile: + _deselect(context) + for t in teile: + t.select_set(True) + context.view_layer.objects.active = teile[0] + bpy.ops.export_scene.fbx( + filepath=os.path.join(verzeichnis, basis + "_Pieces_All.fbx"), + use_selection=True, object_types={'MESH'}, + use_mesh_modifiers=False, mesh_smooth_type='OFF', + use_triangles=True, use_custom_props=True, colors_type='SRGB', + add_leaf_bones=False, bake_anim=False, path_mode='AUTO', + apply_scale_options='FBX_SCALE_ALL') + + # --- Je Teil eine Datei, Pivot = Massenmittelpunkt -------------- + geschrieben = 0 + for t in teile: + schwer = _mass_center(t.data) + t.data.transform(Matrix.Translation(-schwer)) + t.matrix_basis = Matrix.Identity(4) + + hull = _tree_ucx([t], t.name, max_verts=s.piece_ucx_verts) \ + if s.piece_ucx else None + + _deselect(context) + t.select_set(True) + if hull is not None: + hull.select_set(True) + context.view_layer.objects.active = t + bpy.ops.export_scene.fbx( + filepath=os.path.join(verzeichnis, t.name + ".fbx"), + use_selection=True, object_types={'MESH'}, + use_mesh_modifiers=False, mesh_smooth_type='OFF', + use_triangles=True, use_custom_props=True, colors_type='SRGB', + add_leaf_bones=False, bake_anim=False, path_mode='AUTO', + apply_scale_options='FBX_SCALE_ALL') + geschrieben += 1 + if hull is not None: + hm = hull.data + bpy.data.objects.remove(hull, do_unlink=True) + if hm.users == 0: + bpy.data.meshes.remove(hm) + + if not s.piece_keep: + for t in teile: + me = t.data + bpy.data.objects.remove(t, do_unlink=True) + if me.users == 0: + bpy.data.meshes.remove(me) + for n in temp: + o = bpy.data.objects.get(n) + if o is not None: + me = o.data + bpy.data.objects.remove(o, do_unlink=True) + if me is not None and me.users == 0: + bpy.data.meshes.remove(me) + return geschrieben + + class TREEGEN_OT_randomize(Operator): bl_idname = "object.treegen_randomize" bl_label = "Zufaellig" @@ -2083,6 +2509,17 @@ class VIEW3D_PT_tree_export(_TreeSub): layout.prop(s, "sphere_normals") layout.prop(s, "export_dir") layout.operator("treegen.export_game", icon='EXPORT') + layout.separator() + box = layout.box() + box.label(text="Faell-Teile", icon='MOD_BUILD') + box.prop(s, "piece_trunk_segments") + box.prop(s, "piece_ucx") + row = box.row() + row.active = s.piece_ucx + row.prop(s, "piece_ucx_verts") + box.prop(s, "piece_export_combined") + box.prop(s, "piece_keep") + box.operator("treegen.export_pieces", icon='MOD_BUILD') layout.label(text="Slot 0 = %s, Slot 1 = %s" % (TREE_MATERIALS[0], TREE_MATERIALS[1]), icon='MATERIAL') @@ -2091,6 +2528,7 @@ classes = ( TreeGenSettings, TREEGEN_OT_randomize, TREEGEN_OT_export_game, + TREEGEN_OT_export_pieces, TREEGEN_OT_leaves, TREEGEN_OT_create, # Eltern-Panel zuerst, sonst findet Blender die bl_parent_id nicht. diff --git a/tests/test_faell_teile.py b/tests/test_faell_teile.py new file mode 100644 index 0000000..ca688d9 --- /dev/null +++ b/tests/test_faell_teile.py @@ -0,0 +1,193 @@ +"""Punkt 4: Faell-Teile (Stammsegmente + Aeste mit ihren Blatt-Cards). + +Gemessen wird am REIMPORTIERTEN FBX, nicht in der Szene - gleiche Messlatte wie +bei den Punkten 1-3. + +Die Kernaussage ist die Card-Bilanz: die Summe der Blatt-Faces ueber alle Teile +muss der Blatt-Zahl des Stand-Baums entsprechen. Das ist nur pruefbar, weil die +Ast-Zugehoerigkeit als Attribut `ast_id` durch die Instanzierung geschleift wird +- mit einer raeumlichen Nearest-Neighbor-Schaetzung waere "stimmt die Zuordnung" +keine beantwortbare Frage, sondern Ansichtssache. + +Aufruf: blender --background --factory-startup --python tests/test_faell_teile.py +""" +import os +import sys +import tempfile +import importlib.util +from collections import Counter + +import bpy +import bmesh +from mathutils import Vector + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +spec = importlib.util.spec_from_file_location( + "tg", os.path.join(ROOT, "stylized_tree_generator.py")) +tg = importlib.util.module_from_spec(spec) +sys.modules["tg"] = tg +spec.loader.exec_module(tg) +tg.register() + +fails = [] +OUT = tempfile.mkdtemp() + +for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) + +me = bpy.data.meshes.new("Leaf_Card") +me.from_pydata([(-0.5, 0, 0), (0, 0, 0.7), (0.5, 0, 0), (0, 0, -0.1)], + [], [(0, 1, 2), (0, 2, 3)]) +me.update() +card = bpy.data.objects.new("Leaf_Card", me) +bpy.context.scene.collection.objects.link(card) + +s = bpy.context.scene.tree_gen_settings +s.preset = 'baum' +s.use_growth = False +s.apply_modifier = True +s.art_name = "Eiche" +s.variant = 1 +s.export_dir = OUT +s.piece_trunk_segments = 3 +s.piece_ucx = True +s.piece_export_combined = True + +bpy.ops.object.treegen_create() +baum = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and o.name.startswith("Eiche_01") + and not o.name.endswith(("_Leaf", "_Frucht"))][0] +for o in bpy.context.scene.objects: + o.select_set(o is baum) +bpy.context.view_layer.objects.active = baum +s.leaf_card = card +bpy.ops.object.treegen_leaves() +blatt = bpy.data.objects.get(baum.name + "_Leaf") + +# Referenz: wie viele Blatt-Faces hat der Stand-Baum? +dg = bpy.context.evaluated_depsgraph_get() +ev = blatt.evaluated_get(dg) +mb = ev.to_mesh() +cards_gesamt = len(mb.polygons) +attr = mb.attributes.get("ast_id") +ids_stand = Counter(d.value for d in attr.data) if attr else Counter() +ev.to_mesh_clear() +print("Stand-Baum: %d Blatt-Faces, ast_id auf %d Gruppen" + % (cards_gesamt, len(ids_stand))) +if not ids_stand: + fails.append("Die Blatt-Cards tragen kein ast_id - die Zuordnung waere " + "nur zu raten") + +for o in bpy.context.scene.objects: + o.select_set(o is baum) +bpy.context.view_layer.objects.active = baum +namen_vorher = sorted(o.name for o in bpy.context.scene.objects) + +bpy.ops.treegen.export_pieces() + +if sorted(o.name for o in bpy.context.scene.objects) != namen_vorher: + fails.append("Szene veraendert - die Arbeitsebene soll bleiben") + +dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx")) +einzeln = [f for f in dateien if "_Piece_" in f and not f.endswith("_All.fbx")] +gesamt = [f for f in dateien if f.endswith("_Pieces_All.fbx")] +stamm = [f for f in einzeln if "_Stamm" in f] +aeste = [f for f in einzeln if "_Ast" in f] +print("Export: %d Teile (%d Stamm, %d Ast), Gesamtdatei: %s" + % (len(einzeln), len(stamm), len(aeste), "ja" if gesamt else "NEIN")) + +if len(stamm) != s.piece_trunk_segments: + fails.append("%d Stammsegmente, erwartet %d" + % (len(stamm), s.piece_trunk_segments)) +if not aeste: + fails.append("Keine Ast-Teile exportiert") +if not gesamt: + fails.append("Keine Gesamtdatei mit gemeinsamem Ursprung") + +print("") +print("%-30s %6s %6s %6s %7s %s" % + ("Teil", "Rinde", "Blatt", "UCX", "|Pivot|", "Topologie")) + +blatt_summe = 0 +for f in sorted(einzeln): + bpy.ops.wm.read_factory_settings(use_empty=True) + bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, f)) + meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] + hull = [o for o in meshes if o.name.startswith("UCX_")] + koerper = [o for o in meshes if not o.name.startswith("UCX_")] + if not koerper: + fails.append("%s: kein Mesh" % f) + continue + ob = koerper[0] + m = ob.data + rinde = sum(1 for p in m.polygons if p.material_index == 0) + laub = sum(1 for p in m.polygons if p.material_index == 1) + blatt_summe += laub + + # Pivot = Massenmittelpunkt: der Schwerpunkt muss NAHE am Ursprung liegen, + # sonst torkelt das Teil als Physik-Actor um eine Ecke. + schwer = tg._mass_center(m) + abstand = (ob.matrix_world @ schwer).length + diag = max((max(v.co[i] for v in m.vertices) - min(v.co[i] for v in m.vertices)) + for i in range(3)) if m.vertices else 1.0 + + bm = bmesh.new(); bm.from_mesh(m) + nm = sum(1 for e in bm.edges if len(e.link_faces) > 2) + bm.free() + + nv = len(hull[0].data.vertices) if hull else 0 + print("%-30s %6d %6d %6s %7.3f %s" + % (f[:-4], rinde, laub, nv or "-", abstand, + "ok" if nm == 0 else "%d non-manifold" % nm)) + + if len(m.materials) != 2: + fails.append("%s: %d Slots, erwartet 2" % (f, len(m.materials))) + if not hull: + fails.append("%s: keine UCX-Huelle" % f) + else: + if hull[0].name != "UCX_" + f[:-4]: + fails.append("%s: Huelle heisst '%s', Unreal erwartet 'UCX_%s'" + % (f, hull[0].name, f[:-4])) + if nv > s.piece_ucx_verts: + fails.append("%s: UCX hat %d Verts, Budget %d" + % (f, nv, s.piece_ucx_verts)) + # Der Schwerpunkt darf nur einen Bruchteil der groessten Ausdehnung + # vom Ursprung entfernt liegen. + if abstand > diag * 0.15: + fails.append("%s: Schwerpunkt %.3f m vom Pivot entfernt (Ausdehnung " + "%.2f m) - als Physik-Actor wuerde das Teil torkeln" + % (f, abstand, diag)) + +# --- Die Bilanz: Summe der Cards ueber alle Teile == Stand-Baum ---------- +print("") +print("Blatt-Bilanz: %d Cards in den Teilen, %d im Stand-Baum" + % (blatt_summe, cards_gesamt)) +if blatt_summe != cards_gesamt: + fails.append("Blatt-Bilanz stimmt nicht: %d in den Teilen, %d im Stand-Baum " + "- es gehen Cards verloren oder werden doppelt zugeordnet" + % (blatt_summe, cards_gesamt)) + +# --- Gesamtdatei: Teile stehen an ihrer Originalposition ---------------- +if gesamt: + bpy.ops.wm.read_factory_settings(use_empty=True) + bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, gesamt[0])) + teile = [o for o in bpy.context.scene.objects if o.type == 'MESH'] + alle = [o.matrix_world @ v.co for o in teile for v in o.data.vertices] + hoehe = max(c.z for c in alle) - min(c.z for c in alle) + print("Gesamtdatei: %d Teile, Hoehe %.2f m" % (len(teile), hoehe)) + if len(teile) != len(einzeln): + fails.append("Gesamtdatei hat %d Teile, einzeln waren es %d" + % (len(teile), len(einzeln))) + # Zusammengesetzt muss der Baum wieder seine volle Hoehe haben - das ist + # der Beleg, dass die Teile deckungsgleich rekonstruierbar sind. + if hoehe < 3.0: + fails.append("Gesamtdatei nur %.2f m hoch - die Teile stehen nicht an " + "ihrer Originalposition" % hoehe) + +print("") +if fails: + print("ERGEBNIS: %d FEHLER" % len(fails)) + for x in fails: + print(" - " + x) + sys.exit(1) +print("ERGEBNIS: ALLE CHECKS OK")