diff --git a/README.md b/README.md index 0a65a5b..066d4a6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ nummerierten, eingeklappten Unter-Panels in der Reihenfolge des Arbeitsablaufs: | Addon | Unter-Panels | |---|---| | Rock Gen | 1 Benennung & Groesse · 2 Form · 3 Detail & Cleanup · 4 Shading · 5 Vertex-Farben · 6 UV/LOD/Material · 7 Batch & Transform · 8 FBX-Export · 9 Textur-Bake | -| Tree Gen | 1 Wachstum · 2 Blattwerk · 3 Zusatz-Ebenen · 4 Export | +| Tree Gen | 1 Wachstum · 2 Blattwerk · 3 Zusatz-Ebenen · 4 Export · 5 Faell-Teile | | Grass Gen | 1 Form · 2 Haltung · 3 Batch & Budget · 4 Kanal-Belegung | | Asset Utils | 1 Vorbereiten · 2 Vertex-Farben · 3 Benennung · 4 Pruefen · 5 Export | @@ -385,6 +385,64 @@ Zwei Fallen beim Bauen: - `join` loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach tot. Die Aufraeumliste fuehrt deshalb **Namen** statt Objekte. +### LOD-Kette (Stand-Baum) + +Haken `LOD-Kette erzeugen`: exportiert `_LODGroup.fbx` mit `_LOD0..3` unter +einem `fbx_type=LodGroup`-Empty — dasselbe Schema wie die Fels-Pipeline. **Nur +der Stand-Baum**; die Faell-Teile leben Sekunden als Physik-Actor, dort reicht LOD0. + +Die Blatt-Reduktion laeuft ueber die **Dichte**, nicht ueber Decimate: eine Card +besteht aus zwei Dreiecken und laesst sich nicht vereinfachen — man muss weniger +davon setzen. Damit die Krone dabei nicht ausduennt, werden die verbleibenden +Cards groesser. LOD3 ist der Impostor-Ersatz. + +| Stufe | Dichte | Card-Groesse | Rinde | gemessen (Rinde/Blatt/Tris) | +|---|---|---|---|---| +| LOD0 | 100 % | 100 % | 100 % | 578 / 170 / 1306 | +| LOD1 | 60 % | 115 % | 70 % | 477 / 118 / 912 | +| LOD2 | 30 % | 145 % | 45 % | 364 / 66 / 575 | +| LOD3 | 12 % | 200 % | 25 % | 221 / 28 / 302 | + +**Kugel-Normalen auf jeder Stufe** — LOD1–3 haben eine eigene Card-Verteilung, +die Normalen muessen dazu passen. Am reimportierten FBX gemessen: **100 % +radial auf allen vier Stufen**. LOD3 traegt 16 % der Cards von LOD0. + +Der Ursprung kommt aus **LOD0** (Unreal platziert danach); alle Stufen werden um +denselben Betrag verschoben, damit der Baum beim LOD-Wechsel nicht springt. +Das Decimate der Rinde laeuft **vor** dem Verschmelzen — danach wuerde es die +Blatt-Cards mit einschmelzen, und eine halbe Card ist keine Card. + +### Blatt-Streuung: nachgemessen (Punkt 6) + +`Max Radius` filtert die dicken Aeste heraus, damit keine Blaetter am Stamm +kleben. Ueber alle Presets gemessen, wie viele Cards tatsaechlich am Stamm +landen (moeglich, weil `ast_id` mitlaeuft): + +| Preset | Cards | am Stamm | Tris gesamt | +|---|---|---|---| +| baum | 85 | 19 % | 1306 | +| palme | 246 | 0 % | 1710 | +| busch | 118 | 2 % | 876 | +| tanne | 176 | 6 % | 2244 | +| eiche | 172 | 2 % | 1772 | +| birke | 133 | 9 % | 1918 | +| weide | 297 | 1 % | 2246 | +| toter_baum | 49 | 22 % | 838 | +| kaktus | **0** | – | 740 | + +Der Filter arbeitet also richtig, und **alle Presets bleiben unter dem +2500-Tri-Budget** (Maximum weide 2246). + +> **Messfalle dabei:** Zuerst hatte ich den Anteil der Vertices unter der +> Radius-Schwelle gemessen — der lag beim Busch bei 95 % und sah nach einem +> Problem aus. Das misst aber die Vertex-Verteilung, nicht wo Blaetter landen: +> ein Busch besteht nun mal fast nur aus duennen Zweigen. Die richtige Frage +> beantwortet erst `ast_id`. + +Der Kaktus erzeugt regulaer **0 Cards** — kein Ast ist duenner als `Max Radius`, +und das ist botanisch richtig. Vorher lief das still durch und man stand vor +einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler. + ### Faell-Teile (Punkt 4) Kein Laufzeit-Zerlegen: beim Faellen tauscht das Spiel die ISM-Instanz gegen @@ -641,6 +699,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_baum_lod.py` | LOD-Kette: 4 Stufen unter LodGroup, Cards werden nach oben duenner, **Kugel-Normalen auf jeder Stufe**, kein Hoehenversatz | | `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 | diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index a214939..081a372 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.28.0" +version = "1.29.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 9fea14a..f715bdd 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.28.0 + stylized_tree_generator-1.29.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 33.0KB + 34.4KB -

Built 2026-08-06, 05:40

+

Built 2026-08-06, 05:55

diff --git a/dist/index.json b/dist/index.json index b53dc80..4eb5a59 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.28.0", + "version": "1.29.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.28.0.zip", - "archive_size": 33772, - "archive_hash": "sha256:84ff026fecccddc944a1f847d832f3d8b45b37f142bb9e1cf342a103e2ca290f" + "archive_url": "./stylized_tree_generator-1.29.0.zip", + "archive_size": 35207, + "archive_hash": "sha256:faa1e629b1ee4cfd465db79246a6d4acfae12af61f8b65aafcabcc64ffdcde72" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.28.0.zip b/dist/stylized_tree_generator-1.28.0.zip deleted file mode 100644 index cd2af8a..0000000 Binary files a/dist/stylized_tree_generator-1.28.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.29.0.zip b/dist/stylized_tree_generator-1.29.0.zip new file mode 100644 index 0000000..5c2e353 Binary files /dev/null and b/dist/stylized_tree_generator-1.29.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 446d7a1..85b8b30 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, 28, 0), + "version": (1, 29, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -1405,6 +1405,21 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overri # Spielfertiger Export: verschmolzener Stand-Baum # --------------------------------------------------------------------------- + +# LOD-Stufen fuer den Stand-Baum. Die Blatt-Reduktion laeuft ueber die DICHTE, +# nicht ueber Decimate: eine Card ist ein Billboard aus zwei Dreiecken - die +# laesst sich nicht sinnvoll vereinfachen, man muss weniger davon setzen. Damit +# die Krone dabei nicht ausduennt, werden die verbleibenden Cards groesser. +# LOD3 ist der Impostor-Ersatz: wenige, dafuer sehr grosse Cards. +# +# Dichte Card-Groesse Rinden-Decimate +LOD_STUFEN = ( + (1.00, 1.00, 1.00), # LOD0 - wie der Stand-Baum + (0.60, 1.15, 0.70), # LOD1 + (0.30, 1.45, 0.45), # LOD2 + (0.12, 2.00, 0.25), # LOD3 +) + # Slot 0 = Rinde, Slot 1 = Blaetter. Die Reihenfolge ist Teil des Vertrags - # die UE-Skripte mappen ueber den Slot-Namen, aber der Index entscheidet, welche # Faces welches Material bekommen. @@ -1464,7 +1479,7 @@ def _sphere_normals(ob, slot_index, center=None): return center -def _merge_for_export(context, tree_ob, leaf_ob, name): +def _merge_for_export(context, tree_ob, leaf_ob, name, bark_decimate=1.0): """Baut EINE Export-Kopie aus Stamm + Blattebene mit zwei Material-Slots. Die getrennten Objekte im Blend bleiben unangetastet - das ist die @@ -1486,6 +1501,22 @@ def _merge_for_export(context, tree_ob, leaf_ob, name): return ob stamm = _kopie(tree_ob) + + # Rinde VOR dem Verschmelzen vereinfachen. Danach ginge es nicht mehr: ein + # Decimate ueber das fertige Mesh wuerde auch die Blatt-Cards einschmelzen, + # und eine halbe Card ist keine Card mehr. + if bark_decimate < 0.999 and len(stamm.data.polygons) > 8: + dec = stamm.modifiers.new("LODdec", 'DECIMATE') + dec.ratio = max(0.02, bark_decimate) + dg = context.evaluated_depsgraph_get() + neu = bpy.data.meshes.new_from_object( + stamm.evaluated_get(dg), preserve_all_data_layers=True, depsgraph=dg) + alt = stamm.data + stamm.modifiers.remove(dec) + stamm.data = neu + if alt.users == 0: + bpy.data.meshes.remove(alt) + blatt = _kopie(leaf_ob) if leaf_ob is not None else None rinde = _get_or_create_mat(TREE_MATERIALS[0], (0.29, 0.21, 0.14, 1.0)) @@ -1837,6 +1868,14 @@ class TreeGenSettings(PropertyGroup): name="Export-Ordner", default="", subtype='DIR_PATH', description="Zielordner fuer die spielfertigen FBX", ) + generate_lods: BoolProperty( + name="LOD-Kette erzeugen", default=False, + description=("Exportiert LOD0-LOD3 als LOD-Gruppe (Empty mit " + "fbx_type=LodGroup) wie in der Fels-Pipeline. Die Blaetter " + "werden je Stufe duenner gestreut und dafuer groesser - " + "eine Card aus zwei Dreiecken laesst sich nicht vereinfachen"), + ) + sphere_normals: BoolProperty( name="Kugel-Normalen auf Blaetter", default=True, description=("Setzt die Blatt-Normalen radial vom Kronen-Schwerpunkt nach " @@ -1940,7 +1979,7 @@ def _origin_bottom_center(ob): ob.matrix_basis = Matrix.Identity(4) ob.matrix_parent_inverse = Matrix.Identity(4) if not me.vertices: - return + return Matrix.Identity(4) xs = [v.co.x for v in me.vertices] ys = [v.co.y for v in me.vertices] zs = [v.co.z for v in me.vertices] @@ -1950,6 +1989,7 @@ def _origin_bottom_center(ob): -min(zs), )) me.transform(versatz) + return versatz @@ -1997,25 +2037,78 @@ class TREEGEN_OT_export_game(Operator): umbenannt.append((quelle, quelle.name)) quelle.name = quelle.name + ".__src" - merge, aufraeumen = _merge_for_export( - context, baum, blatt, umbenannt[0][1]) + basis_name = umbenannt[0][1] + stufen = LOD_STUFEN if s.generate_lods else LOD_STUFEN[:1] + gruppe = None + lods = [] - # Kugel-Normalen auf die Blatt-Faces (Slot 1). - mitte = None - if s.sphere_normals and len(merge.data.materials) > 1: - mitte = _sphere_normals(merge, 1) + for i, (dichte, groesse, dec) in enumerate(stufen): + lod_name = ("%s_LOD%d" % (basis_name, i)) if s.generate_lods \ + else basis_name - # Ursprung: Bbox-Unterseite Mitte, wie in der Fels-Pipeline. - _origin_bottom_center(merge) + # Blatt-Ebene je Stufe NEU streuen statt sie zu dezimieren: + # eine Card besteht aus zwei Dreiecken, die laesst sich nicht + # vereinfachen - man muss weniger davon setzen. Die + # verbleibenden werden groesser, damit die Krone nicht + # ausduennt. + lod_blatt = blatt + if blatt is not None and s.generate_lods and i > 0: + lod_blatt = make_leaves( + context, baum, s.leaf_card, + name="%s__leaflod%d" % (basis_name, i), + card_coll=s.leaf_cards, + Density=s.leaf_density * dichte, + Size=s.leaf_size * groesse, + **{"Size Var": s.leaf_size_var, + "Max Radius": s.leaf_max_radius, + "Tilt": s.leaf_tilt, "Seed": s.seed}) + context.view_layer.update() + aufraeumen.append(lod_blatt.name) + + merge, gemacht = _merge_for_export( + context, baum, lod_blatt, lod_name, bark_decimate=dec) + aufraeumen.extend(gemacht) + + # Kugel-Normalen auf JEDER Stufe - LOD1..3 haben eine eigene + # Card-Verteilung, die Normalen muessen dazu passen. + mitte = None + if s.sphere_normals and len(merge.data.materials) > 1: + mitte = _sphere_normals(merge, 1) + if mitte is not None: + print("[TreeGen] %s: Kugel-Normalen um (%.2f, %.2f, %.2f)" + % (merge.name, mitte.x, mitte.y, mitte.z)) + lods.append(merge) + + # Ursprung aus LOD0 - Unreal platziert nach LOD0. Alle Stufen um + # denselben Betrag verschieben, damit sie deckungsgleich bleiben. + versatz = _origin_bottom_center(lods[0]) + for weitere in lods[1:]: + weitere.data.transform(versatz) + weitere.matrix_basis = Matrix.Identity(4) + weitere.matrix_parent_inverse = Matrix.Identity(4) _deselect(context) - merge.select_set(True) - context.view_layer.objects.active = merge + if s.generate_lods: + gruppe = bpy.data.objects.new(basis_name + "_LODGroup", None) + gruppe.empty_display_type = 'PLAIN_AXES' + gruppe["fbx_type"] = "LodGroup" # von Unreal ausgewertet + context.scene.collection.objects.link(gruppe) + aufraeumen.append(gruppe.name) + for lod in lods: + lod.parent = gruppe + lod.matrix_parent_inverse = Matrix.Identity(4) + lod.select_set(True) + gruppe.select_set(True) + context.view_layer.objects.active = gruppe + ziel = os.path.join(verzeichnis, gruppe.name + ".fbx") + else: + lods[0].select_set(True) + context.view_layer.objects.active = lods[0] bpy.ops.export_scene.fbx( filepath=ziel, use_selection=True, - object_types={'MESH'}, + object_types={'MESH', 'EMPTY'}, # EMPTY = die LodGroup use_mesh_modifiers=False, # ist bereits ausgewertet mesh_smooth_type='OFF', # OFF = Custom Normals bleiben use_custom_props=True, @@ -2026,9 +2119,6 @@ class TREEGEN_OT_export_game(Operator): apply_scale_options='FBX_SCALE_ALL', ) geschrieben += 1 - if mitte is not None: - print("[TreeGen] %s: Kugel-Normalen um (%.2f, %.2f, %.2f)" - % (merge.name, mitte.x, mitte.y, mitte.z)) except Exception as exc: # ein Baum darf den Rest nicht killen fehler += 1 print("[TreeGen] Export '%s' fehlgeschlagen: %s" % (baum.name, exc)) @@ -2285,7 +2375,16 @@ class TREEGEN_OT_leaves(Operator): msg = ("Blattwerk fuer %d Baum/Baeume: %d Stamm + %d Blatt = %d Tris" % (len(trees), total_tree, total_leaf, total)) limit = s.leaf_budget * max(len(trees), 1) - if s.leaf_budget and total > limit: + if total_leaf == 0: + # Passiert regulaer beim Kaktus (kein Ast duenner als Max Radius) - + # aber auch, wenn Max Radius fuer den Baum zu klein steht. Beides + # sieht in der Szene gleich aus: eine leere _Leaf-Ebene. Ohne + # Hinweis sucht man den Fehler im Material. + self.report({'WARNING'}, + "Keine Blaetter gestreut - kein Ast ist duenner als " + "Max Radius (%.3f m). Beim Kaktus ist das richtig, " + "sonst Max Radius erhoehen." % s.leaf_max_radius) + elif s.leaf_budget and total > limit: self.report({'WARNING'}, msg + " - UEBER Budget (%d). Dichte senken." % limit) else: self.report({'INFO'}, msg) @@ -2495,6 +2594,26 @@ class VIEW3D_PT_tree_layers(_TreeSub): layout.prop(s, "make_frucht") +class VIEW3D_PT_tree_pieces(_TreeSub): + """Eigenes Unter-Panel: das Export-Panel war mit Stand-Baum, LODs und + Faell-Teilen ueberladen - drei Themen in einem Kasten.""" + bl_label = "5 - Faell-Teile" + + def draw(self, context): + layout = self.layout + s = context.scene.tree_gen_settings + layout.prop(s, "piece_trunk_segments") + layout.prop(s, "piece_ucx") + row = layout.row() + row.active = s.piece_ucx + row.prop(s, "piece_ucx_verts") + layout.prop(s, "piece_export_combined") + layout.prop(s, "piece_keep") + layout.separator() + layout.operator("treegen.export_pieces", icon='MOD_BUILD') + layout.label(text="Pivot je Teil = Massenmittelpunkt", icon='INFO') + + class VIEW3D_PT_tree_export(_TreeSub): bl_label = "4 - Export" @@ -2507,19 +2626,10 @@ class VIEW3D_PT_tree_export(_TreeSub): row.prop(s, "generate_lightmap_uv") layout.separator() layout.prop(s, "sphere_normals") + layout.prop(s, "generate_lods") 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') @@ -2537,6 +2647,7 @@ classes = ( VIEW3D_PT_tree_leaves, VIEW3D_PT_tree_layers, VIEW3D_PT_tree_export, + VIEW3D_PT_tree_pieces, ) diff --git a/tests/test_baum_lod.py b/tests/test_baum_lod.py new file mode 100644 index 0000000..f9fbe3f --- /dev/null +++ b/tests/test_baum_lod.py @@ -0,0 +1,182 @@ +"""Punkt 5: LOD-Kette fuer den Stand-Baum. + +Die Blatt-Reduktion laeuft ueber die DICHTE, nicht ueber Decimate: eine Card +besteht aus zwei Dreiecken und laesst sich nicht vereinfachen - man muss weniger +davon setzen und die verbleibenden groesser machen. + +Gemessen wird am reimportierten FBX, und die Kugel-Normalen werden auf JEDER +Stufe geprueft: LOD1-3 haben eine eigene Card-Verteilung, die Normalen muessen +dazu passen. Nur LOD0 zu pruefen wuerde genau das verfehlen. + +Aufruf: blender --background --factory-startup --python tests/test_baum_lod.py +""" +import os +import sys +import tempfile +import importlib.util + +import bpy +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.sphere_normals = True +s.generate_lods = 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() + +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_game() + +if sorted(o.name for o in bpy.context.scene.objects) != namen_vorher: + fehlt = [n for n in namen_vorher + if n not in [o.name for o in bpy.context.scene.objects]] + neu = [o.name for o in bpy.context.scene.objects if o.name not in namen_vorher] + fails.append("Szene veraendert - fehlt: %s, neu: %s" % (fehlt, neu)) + +dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx")) +lodfile = [f for f in dateien if "_LODGroup" in f] +print("Export: %s" % dateien) +if not lodfile: + print("ERGEBNIS: FEHLER: keine _LODGroup-Datei") + sys.exit(1) + +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, lodfile[0])) + +meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] +empties = [o for o in bpy.context.scene.objects if o.type == 'EMPTY'] +stufen = sorted(meshes, key=lambda o: o.name) +print("") +print("%-24s %7s %7s %7s %9s %s" + % ("Stufe", "Rinde", "Blatt", "Tris", "Radial", "Slots")) + +if len(stufen) != len(tg.LOD_STUFEN): + fails.append("%d LOD-Meshes, erwartet %d" % (len(stufen), len(tg.LOD_STUFEN))) +if not empties: + fails.append("Kein LodGroup-Empty im FBX - Unreal erkennt die Kette nicht") + +blatt_zahlen = [] +rinde_zahlen = [] +for ob in stufen: + 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) + tris = sum(len(p.vertices) - 2 for p in m.polygons) + blatt_zahlen.append(laub) + rinde_zahlen.append(rinde) + + # Kugel-Normalen auf DIESER Stufe nachmessen. + quote = 0.0 + blatt_polys = [p for p in m.polygons if p.material_index == 1] + if blatt_polys: + pts = [m.vertices[vi].co for p in blatt_polys for vi in p.vertices] + mitte = Vector((sum(p.x for p in pts) / len(pts), + sum(p.y for p in pts) / len(pts), + sum(p.z for p in pts) / len(pts))) + if hasattr(m, "calc_normals_split"): + m.calc_normals_split() + treffer = gesamt = 0 + for p in blatt_polys: + for li in p.loop_indices: + n = Vector(m.loops[li].normal) + radial = m.vertices[m.loops[li].vertex_index].co - mitte + if n.length_squared < 1e-9 or radial.length_squared < 1e-9: + continue + gesamt += 1 + if n.normalized().dot(radial.normalized()) > 0.8: + treffer += 1 + quote = 100.0 * treffer / gesamt if gesamt else 0.0 + + print("%-24s %7d %7d %7d %8.0f%% %s" + % (ob.name, rinde, laub, tris, quote, + [x.name if x else "-" for x in m.materials])) + + if len(m.materials) != 2: + fails.append("%s: %d Slots, erwartet 2" % (ob.name, len(m.materials))) + if blatt_polys and quote < 90.0: + fails.append("%s: nur %.0f%% der Blatt-Normalen radial - die " + "Kugel-Normalen fehlen auf dieser Stufe" % (ob.name, quote)) + if not blatt_polys: + fails.append("%s: keine Blatt-Faces" % ob.name) + +# --- Die Ketten-Eigenschaft: es muss nach oben duenner werden ------------- +print("") +print("Blatt-Faces je Stufe: %s" % blatt_zahlen) +print("Rinden-Faces je Stufe: %s" % rinde_zahlen) +for i in range(1, len(blatt_zahlen)): + if blatt_zahlen[i] >= blatt_zahlen[i - 1]: + fails.append("LOD%d hat %d Blatt-Faces, LOD%d hatte %d - die Kette wird " + "nicht duenner" % (i, blatt_zahlen[i], i - 1, + blatt_zahlen[i - 1])) +for i in range(1, len(rinde_zahlen)): + if rinde_zahlen[i] > rinde_zahlen[i - 1]: + fails.append("LOD%d hat mehr Rinden-Faces als LOD%d" % (i, i - 1)) + +# LOD3 muss deutlich guenstiger sein als LOD0, sonst lohnt die Kette nicht. +if blatt_zahlen and blatt_zahlen[0] > 0: + anteil = 100.0 * blatt_zahlen[-1] / blatt_zahlen[0] + print("LOD3 traegt %.0f%% der Cards von LOD0" % anteil) + if anteil > 30.0: + fails.append("LOD3 hat noch %.0f%% der Cards - als Impostor-Ersatz zu " + "teuer" % anteil) + +# --- Alle Stufen deckungsgleich am Ursprung ------------------------------ +for ob in stufen: + co = [ob.matrix_world @ v.co for v in ob.data.vertices] + lo = [min(c[i] for c in co) for i in range(3)] + hi = [max(c[i] for c in co) for i in range(3)] + if abs((lo[0] + hi[0]) * 0.5) > 0.25 or abs((lo[1] + hi[1]) * 0.5) > 0.25: + fails.append("%s: Bbox nicht zentriert (%.2f/%.2f)" + % (ob.name, (lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5)) + # Die Stufen muessen auf derselben Hoehe stehen - sonst springt der Baum + # beim LOD-Wechsel. + if abs(lo[2]) > 0.15: + fails.append("%s: steht %.3f m ueber/unter dem Ursprung - der Baum " + "wuerde beim LOD-Wechsel springen" % (ob.name, lo[2])) + +print("") +if fails: + print("ERGEBNIS: %d FEHLER" % len(fails)) + for x in fails: + print(" - " + x) + sys.exit(1) +print("ERGEBNIS: ALLE CHECKS OK") diff --git a/tests/test_panels.py b/tests/test_panels.py index ec514c0..1db6f33 100644 --- a/tests/test_panels.py +++ b/tests/test_panels.py @@ -28,7 +28,7 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ADDONS = { "stylized_rock_generator.py": ("VIEW3D_PT_rock_generator", 9, "rock_gen_settings", set()), - "stylized_tree_generator.py": ("VIEW3D_PT_tree_generator", 4, + "stylized_tree_generator.py": ("VIEW3D_PT_tree_generator", 5, "tree_gen_settings", set()), "stylized_grass_generator.py": ("VIEW3D_PT_grass_generator", 4, "grass_gen_settings", set()),