From 46b4e9ee348a4271c4c50260047b72c5ec7e082e Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 6 Aug 2026 06:42:59 +0200 Subject: [PATCH] Tree Gen 1.26.0: spielfertiger Stand-Baum + Kugel-Normalen (Punkt 1 + 2) Punkt 1 - "Spielfertig exportieren" Stamm und _Leaf-Ebene werden zu EINEM Mesh mit zwei Material-Slots verschmolzen (Slot 0 = M_Bark, Slot 1 = M_Foliage). Die getrennten Objekte im Blend bleiben die Arbeitsebene - verschmolzen wird nur eine Export-Kopie. Ursprung Bbox-Unterseite Mitte und FBX-Einstellungen wie in der Fels-Pipeline (use_selection, FBX_SCALE_ALL). Punkt 2 - Kugel-Normalen auf die Blatt-Cards Alle Blatt-Normalen zeigen radial vom Kronen-Schwerpunkt nach aussen, damit die Krone als EIN weiches Volumen liest statt als Haufen flacher Bleche. Der Schwerpunkt wird nur aus den BLATT-Faces gebildet - der Stamm wuerde die Mitte nach unten ziehen und die Kugel verkippen. Bewusst direkt ueber normals_split_custom_set statt ueber den Normal-Edit-Modifier: der Modifier muesste vor dem Export angewendet werden, und angewendete Modifier plus Custom Normals ist genau die Kombination, bei der der FBX-Export gerne flachbuegelt. Am REIMPORTIERTEN FBX gemessen: mittlere Radialitaet 1.000, 100 % der 510 Blatt-Loops ueber 0.8. Gegenprobe Rinde 0.043 - die bleibt unangetastet. Drei Fallen unterwegs: - Ueber bmesh.from_mesh zusammenfuegen haengt die Geometrie zwar an, wirft aber die MATERIAL-INDIZES weg: danach lagen alle 748 Faces auf Index 0, Blaetter und Rinde ununterscheidbar. Blenders join fuehrt die Slot-Listen zusammen und rechnet die Indizes korrekt um. - join loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach tot. Die Aufraeumliste fuehrt deshalb NAMEN statt Objekte. - Die Export-Kopie muss exakt so heissen wie der Baum (Unreal leitet den Asset-Namen davon ab). Solange das Original den Namen haelt, haengt Blender ".001" an - Original wird fuer die Exportdauer beiseite benannt, wie bei den Felsen. Neu: tests/test_baum_export.py misst die Normalen im EXPORTIERTEN FBX, nicht am Szenenobjekt - nur so faellt ein plattgebuegelter Custom-Normal auf. 19 Tests bestehen. Co-Authored-By: Claude Opus 4.8 --- blender_manifest_tree.toml | 2 +- dist/index.html | 6 +- dist/index.json | 8 +- dist/stylized_tree_generator-1.25.0.zip | Bin 24729 -> 0 bytes dist/stylized_tree_generator-1.26.0.zip | Bin 0 -> 28299 bytes stylized_tree_generator.py | 288 +++++++++++++++++++++++- tests/test_baum_export.py | 196 ++++++++++++++++ 7 files changed, 491 insertions(+), 9 deletions(-) delete mode 100644 dist/stylized_tree_generator-1.25.0.zip create mode 100644 dist/stylized_tree_generator-1.26.0.zip create mode 100644 tests/test_baum_export.py diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 0b45f60..76ac871 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.25.0" +version = "1.26.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 a6ae283..24a80ba 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.25.0 + stylized_tree_generator-1.26.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 24.1KB + 27.6KB -

Built 2026-08-03, 12:20

+

Built 2026-08-06, 04:42

diff --git a/dist/index.json b/dist/index.json index dea8d7e..857183e 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.25.0", + "version": "1.26.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.25.0.zip", - "archive_size": 24729, - "archive_hash": "sha256:51006e9a2968cfce6dcb7ce13888a5e410b25119916c300013a7105ee83f78e5" + "archive_url": "./stylized_tree_generator-1.26.0.zip", + "archive_size": 28299, + "archive_hash": "sha256:8e6cf3131a36efe840d0f59802003a3cb198b611a405c53e6428f2a99029b0c9" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.25.0.zip b/dist/stylized_tree_generator-1.25.0.zip deleted file mode 100644 index c09c9ee215290772747b039eaa0a2a633e471d39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24729 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 P3HR^%{i^_z|GfSO#+$pj diff --git a/dist/stylized_tree_generator-1.26.0.zip b/dist/stylized_tree_generator-1.26.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..a87412e8a067298a0848d11d0db7ae0d1805193c GIT binary patch literal 28299 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 Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -18,6 +18,9 @@ bl_info = { # im Viewport ziehen, der Baum aktualisiert sich sofort. import bpy +import bmesh +import os +from mathutils import Matrix, Vector import sys import os @@ -1311,6 +1314,130 @@ def make_leaves(context, tree_obj, card_obj, name=None, **overrides): + +# --------------------------------------------------------------------------- +# Spielfertiger Export: verschmolzener Stand-Baum +# --------------------------------------------------------------------------- + +# 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. +TREE_MATERIALS = ("M_Bark", "M_Foliage") + + +def _get_or_create_mat(name, color): + mat = bpy.data.materials.get(name) + if mat is None: + mat = bpy.data.materials.new(name) + mat.diffuse_color = color + return mat + + +def _sphere_normals(ob, slot_index, center=None): + """Setzt die Normalen der Faces in `slot_index` RADIAL von einem Mittelpunkt. + + Der wichtigste Stylized-Trick fuer Blatt-Cards: statt der flachen + Card-Normalen (jede Card wirkt wie ein eigenes flaches Blech, die Krone + flimmert im Licht) zeigen alle Blatt-Normalen nach aussen wie bei einer + Kugel. Die Krone liest sich dadurch als EIN weiches Volumen. + + Bewusst direkt ueber `normals_split_custom_set` statt ueber den + Normal-Edit-Modifier: der Modifier muesste vor dem Export angewendet werden, + und angewendete Modifier plus Custom Normals ist genau die Kombination, bei + der der FBX-Export gerne flachbuegelt. Direkt gesetzte Loop-Normalen gehen + unveraendert in die Datei. + """ + me = ob.data + if not me.polygons: + return None + if center is None: + # Schwerpunkt NUR der Blatt-Faces - der Stamm wuerde die Mitte nach + # unten ziehen und die Kugel verkippen. + pts = [me.vertices[vi].co for p in me.polygons + if p.material_index == slot_index for vi in p.vertices] + if not pts: + return None + center = 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))) + + # Vorhandene Loop-Normalen als Ausgangslage, dann nur die Blatt-Loops ersetzen. + me.calc_normals_split() if hasattr(me, "calc_normals_split") else None + normalen = [Vector(l.normal) for l in me.loops] + for poly in me.polygons: + if poly.material_index != slot_index: + continue + for li in poly.loop_indices: + v = me.vertices[me.loops[li].vertex_index].co + richtung = v - center + if richtung.length_squared < 1e-12: + richtung = Vector((0.0, 0.0, 1.0)) + normalen[li] = richtung.normalized() + + me.normals_split_custom_set([tuple(n) for n in normalen]) + return center + + +def _merge_for_export(context, tree_ob, leaf_ob, name): + """Baut EINE Export-Kopie aus Stamm + Blattebene mit zwei Material-Slots. + + Die getrennten Objekte im Blend bleiben unangetastet - das ist die + Arbeits-Ebene. Verschmolzen wird nur diese Kopie. + """ + # Die Aufraeumliste fuehrt NAMEN, keine Objekte: `join` loescht das zweite + # Objekt, und danach wirft schon der Zugriff auf `.name` einer gehaltenen + # Referenz ("StructRNA of type Object has been removed"). + made = [] + + def _kopie(src): + dg = context.evaluated_depsgraph_get() + 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 + "__exp", me) + ob.matrix_world = src.matrix_world.copy() + context.scene.collection.objects.link(ob) + made.append(ob.name) + return ob + + stamm = _kopie(tree_ob) + 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)) + laub = _get_or_create_mat(TREE_MATERIALS[1], (0.18, 0.34, 0.14, 1.0)) + + stamm.data.materials.clear() + stamm.data.materials.append(rinde) + for p in stamm.data.polygons: + p.material_index = 0 + + if blatt is not None and blatt.data.polygons: + # FALLE: Ueber `bmesh.from_mesh` zusammenzufuegen haengt die Geometrie + # zwar an, wirft dabei aber die MATERIAL-INDIZES weg - gemessen lagen + # danach alle 748 Faces auf Index 0, Blaetter und Rinde ununterscheidbar. + # Blenders `join` macht genau das richtig: es fuehrt die Slot-Listen + # zusammen und rechnet die Indizes um. Das Blatt-Mesh traegt nur seinen + # eigenen Slot und landet nach dem Join automatisch auf Index 1. + blatt.data.materials.clear() + blatt.data.materials.append(laub) + for p in blatt.data.polygons: + p.material_index = 0 + + # join LOESCHT das zweite Objekt. Danach ist jede Referenz darauf tot + # ("StructRNA of type Object has been removed") - also vorher merken. + blatt_name = blatt.name + with context.temp_override(active_object=stamm, + selected_editable_objects=[stamm, blatt]): + bpy.ops.object.join() + if blatt_name not in bpy.data.objects: + made = [n for n in made if n != blatt_name] + + # Der Stamm wird umbenannt - die Aufraeumliste muss mitziehen. + made = [name if n == stamm.name else n for n in made] + stamm.name = name + stamm.data.name = name + return stamm, made + + # --------------------------------------------------------------------------- # Addon-UI # --------------------------------------------------------------------------- @@ -1382,6 +1509,17 @@ class TreeGenSettings(PropertyGroup): name="_Frucht-Ebene anlegen", default=False, description="Zusaetzlich _Frucht (gleicher Ursprung) fuer die Frucht-Ebene", ) + export_dir: StringProperty( + name="Export-Ordner", default="", subtype='DIR_PATH', + description="Zielordner fuer die spielfertigen FBX", + ) + sphere_normals: BoolProperty( + name="Kugel-Normalen auf Blaetter", default=True, + description=("Setzt die Blatt-Normalen radial vom Kronen-Schwerpunkt nach " + "aussen. Ohne das wirkt jede Card wie ein eigenes flaches " + "Blech und die Krone flimmert im Licht"), + ) + apply_modifier: BoolProperty( name="Modifier anwenden", default=False, description="Ergebnis als normales Mesh einfrieren (fuer den Export)", @@ -1447,6 +1585,147 @@ def _add_layer(context, ob, suffix): return new + +def _safe_name(name): + keep = "-_.() " + return "".join(c for c in name if c.isalnum() or c in keep).strip() or "Tree" + + +def _deselect(context): + for o in list(context.view_layer.objects): + try: + o.select_set(False) + except (ReferenceError, RuntimeError): + pass + + +def _origin_bottom_center(ob): + """Ursprung auf Bbox-Unterseite Mitte - identisch zur Fels-Pipeline. + + Die Transformation wandert dabei in die Mesh-Daten und das Objekt steht + danach auf Identitaet: der FBX-Export schreibt die Objekt-Transformation + sonst MIT, und das Asset traegt seine Szenenposition mit ins Spiel. + """ + me = ob.data + me.transform(ob.matrix_world) + ob.matrix_basis = Matrix.Identity(4) + ob.matrix_parent_inverse = Matrix.Identity(4) + if not me.vertices: + return + 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] + versatz = Matrix.Translation(( + -(min(xs) + max(xs)) * 0.5, + -(min(ys) + max(ys)) * 0.5, + -min(zs), + )) + me.transform(versatz) + + + +class TREEGEN_OT_export_game(Operator): + """Exportiert den ausgewaehlten Baum spielfertig: EIN Mesh, zwei Slots. + + Stamm und _Leaf-Ebene bleiben im Blend getrennt - das ist die Arbeitsebene. + Verschmolzen wird nur die Export-Kopie. + """ + bl_idname = "treegen.export_game" + bl_label = "Spielfertig exportieren" + bl_description = ("Verschmilzt Stamm + Blattebene zu einem Mesh mit zwei " + "Material-Slots, setzt Kugel-Normalen auf die Blaetter " + "und schreibt eine FBX") + 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 = ausgewaehlte Meshes, die KEINE Zusatzebene sind. + 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 (Stamm-Objekt waehlen).") + return {'CANCELLED'} + + geschrieben, fehler = 0, 0 + for baum in baeume: + blatt = bpy.data.objects.get(baum.name + "_Leaf") + ziel = os.path.join(verzeichnis, _safe_name(baum.name) + ".fbx") + aufraeumen = [] + umbenannt = [] + try: + # Die Export-Kopie muss EXAKT so heissen wie der Baum - Unreal + # leitet den Asset-Namen davon ab. Solange das Original den + # Namen haelt, haengt Blender ".001" an die Kopie. Also das + # Original fuer die Dauer des Exports beiseite benennen. + for quelle in (baum, blatt): + if quelle is not None: + umbenannt.append((quelle, quelle.name)) + quelle.name = quelle.name + ".__src" + + merge, aufraeumen = _merge_for_export( + context, baum, blatt, umbenannt[0][1]) + + # 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) + + # Ursprung: Bbox-Unterseite Mitte, wie in der Fels-Pipeline. + _origin_bottom_center(merge) + + _deselect(context) + merge.select_set(True) + context.view_layer.objects.active = merge + + bpy.ops.export_scene.fbx( + filepath=ziel, + use_selection=True, + object_types={'MESH'}, + use_mesh_modifiers=False, # ist bereits ausgewertet + mesh_smooth_type='OFF', # OFF = Custom Normals bleiben + 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 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)) + import traceback + traceback.print_exc() + finally: + for n in aufraeumen: + 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) + # Erst die Kopien weg, DANN die Namen zurueck - sonst kollidieren sie. + for quelle, alt_name in umbenannt: + quelle.name = alt_name + + if fehler: + self.report({'WARNING'}, "%d FBX geschrieben, %d fehlgeschlagen " + "(Konsole)." % (geschrieben, fehler)) + else: + self.report({'INFO'}, "%d spielfertige FBX nach %s." + % (geschrieben, verzeichnis)) + return {'FINISHED'} + + class TREEGEN_OT_randomize(Operator): bl_idname = "object.treegen_randomize" bl_label = "Zufaellig" @@ -1725,11 +2004,18 @@ class VIEW3D_PT_tree_export(_TreeSub): row = layout.row() row.active = s.apply_modifier row.prop(s, "generate_lightmap_uv") + layout.separator() + layout.prop(s, "sphere_normals") + layout.prop(s, "export_dir") + layout.operator("treegen.export_game", icon='EXPORT') + layout.label(text="Slot 0 = %s, Slot 1 = %s" + % (TREE_MATERIALS[0], TREE_MATERIALS[1]), icon='MATERIAL') classes = ( TreeGenSettings, TREEGEN_OT_randomize, + TREEGEN_OT_export_game, TREEGEN_OT_leaves, TREEGEN_OT_create, # Eltern-Panel zuerst, sonst findet Blender die bl_parent_id nicht. diff --git a/tests/test_baum_export.py b/tests/test_baum_export.py new file mode 100644 index 0000000..cabf858 --- /dev/null +++ b/tests/test_baum_export.py @@ -0,0 +1,196 @@ +"""Spielfertiger Stand-Baum: ein Mesh, zwei Slots, Kugel-Normalen. + +Der wichtigste Teil ist die NORMALEN-Messung am reimportierten FBX. Custom +Normals sind die Sorte Daten, die der Exporter gerne stillschweigend +plattbuegelt - dann sieht der Baum in Blender richtig aus und in UE flimmert +die Krone. Am Objekt in der Szene zu messen wuerde das nicht zeigen. + +Aufruf: blender --background --factory-startup --python tests/test_baum_export.py +""" +import os +import sys +import math +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) + + +def diamant(name="Leaf_Card"): + """Diamond-Card wie im Projekt: vier Ecken, zwei Tris.""" + me = bpy.data.meshes.new(name) + me.from_pydata( + [(-0.5, 0.0, 0.0), (0.0, 0.0, 0.7), (0.5, 0.0, 0.0), (0.0, 0.0, -0.1)], + [], [(0, 1, 2), (0, 2, 3)]) + me.update() + ob = bpy.data.objects.new(name, me) + bpy.context.scene.collection.objects.link(ob) + return ob + + +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 + +card = diamant() +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"))] +if not baum: + print("ERGEBNIS: FEHLER: kein Baum erzeugt") + sys.exit(1) +baum = baum[0] + +# Der Blattwerk-Operator arbeitet auf der Auswahl. +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") +if blatt is None: + fails.append("Keine _Leaf-Ebene erzeugt") + +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() + +# Die Arbeitsebene muss unangetastet bleiben. +namen_nachher = sorted(o.name for o in bpy.context.scene.objects) +if namen_nachher != namen_vorher: + fehlt = [n for n in namen_vorher if n not in namen_nachher] + neu = [n for n in namen_nachher if n 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")) +if not dateien: + print("ERGEBNIS: FEHLER: kein FBX geschrieben") + sys.exit(1) + +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, dateien[0])) +meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] +print("") +print("FBX %s: %d Mesh(es)" % (dateien[0], len(meshes))) +if len(meshes) != 1: + fails.append("%d Meshes im FBX, erwartet genau eines (verschmolzen)" + % len(meshes)) + +ob = meshes[0] +me = ob.data +slots = [m.name if m else "-" for m in me.materials] +print("Slots: %s" % slots) +if len(slots) != 2: + fails.append("%d Material-Slots, erwartet 2 (Rinde + Blaetter)" % len(slots)) +else: + if tg.TREE_MATERIALS[0] not in slots[0]: + fails.append("Slot 0 heisst '%s', erwartet '%s' (Rinde)" + % (slots[0], tg.TREE_MATERIALS[0])) + if tg.TREE_MATERIALS[1] not in slots[1]: + fails.append("Slot 1 heisst '%s', erwartet '%s' (Blaetter)" + % (slots[1], tg.TREE_MATERIALS[1])) + +# --- Ursprung: Bbox-Unterseite Mitte ------------------------------------- +co = [ob.matrix_world @ v.co for v in me.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)] +mid = ((lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5) +print("Ursprung: Mitte %.3f/%.3f, min.Z %.3f, Hoehe %.2f m" + % (mid[0], mid[1], lo[2], hi[2] - lo[2])) +if abs(mid[0]) > 0.10 or abs(mid[1]) > 0.10: + fails.append("Bbox nicht zentriert (%.3f/%.3f)" % mid) +if abs(lo[2]) > 0.01: + fails.append("min.Z = %.3f, erwartet 0" % lo[2]) + +# --- Der eigentliche Punkt: Kugel-Normalen im FBX ------------------------ +blatt_polys = [p for p in me.polygons if p.material_index == 1] +rinde_polys = [p for p in me.polygons if p.material_index == 0] +print("Faces: %d Rinde, %d Blaetter" % (len(rinde_polys), len(blatt_polys))) +if not blatt_polys: + fails.append("Keine Blatt-Faces im FBX (Slot 1 leer)") +else: + pts = [me.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(me, "calc_normals_split"): + me.calc_normals_split() + treffer, gesamt, summe = 0, 0, 0.0 + for p in blatt_polys: + for li in p.loop_indices: + n = Vector(me.loops[li].normal) + if n.length_squared < 1e-9: + continue + radial = me.vertices[me.loops[li].vertex_index].co - mitte + if radial.length_squared < 1e-9: + continue + d = n.normalized().dot(radial.normalized()) + summe += d + gesamt += 1 + if d > 0.8: + treffer += 1 + quote = 100.0 * treffer / gesamt if gesamt else 0.0 + print("Blatt-Normalen: %d Loops, mittlere Radialitaet %.3f, " + "%.0f%% ueber 0.8" % (gesamt, summe / gesamt if gesamt else 0, quote)) + # Bei echten Kugel-Normalen liegt die Radialitaet nahe 1. Flache + # Card-Normalen streuen um 0 - das waere der Zustand, den wir vermeiden. + if gesamt == 0: + fails.append("Keine auswertbaren Blatt-Normalen im FBX") + elif quote < 90.0: + fails.append("Nur %.0f%% der Blatt-Normalen zeigen radial nach aussen " + "(mittlere Radialitaet %.3f) - der FBX-Export hat die " + "Custom Normals plattgebuegelt" + % (quote, summe / gesamt)) + + # Gegenprobe: die Rinde darf NICHT radial sein, sonst haben wir die + # Normalen des ganzen Baums ueberschrieben. + if rinde_polys: + rs, rn = 0.0, 0 + for p in rinde_polys: + for li in p.loop_indices: + n = Vector(me.loops[li].normal) + radial = me.vertices[me.loops[li].vertex_index].co - mitte + if n.length_squared < 1e-9 or radial.length_squared < 1e-9: + continue + rs += n.normalized().dot(radial.normalized()) + rn += 1 + mittel = rs / rn if rn else 0.0 + print("Rinden-Normalen: mittlere Radialitaet %.3f (soll deutlich " + "niedriger sein)" % mittel) + if mittel > 0.75: + fails.append("Auch die Rinde ist radial (%.3f) - die Kugel-Normalen " + "wurden auf den ganzen Baum gelegt" % mittel) + +print("") +if fails: + print("ERGEBNIS: %d FEHLER" % len(fails)) + for x in fails: + print(" - " + x) + sys.exit(1) +print("ERGEBNIS: ALLE CHECKS OK")