"""Blatt-Defaults fuer BUESCHEL-Atlanten (eine Card = ein ganzer Zweigbueschel). Die alten Werte (Size 0.22, Density 150-500) stammen aus der Blaettchen-Aera: damit streute der Generator hunderte winziger Cards, wo ein paar Dutzend grosse hingehoeren. Diese Datei nagelt die neuen Groessenordnungen fest und prueft die Kopplung an die Wachstums-Stufen. Aufruf: blender --background --factory-startup --python tests/test_blatt_bueschel.py """ import os import sys import math import importlib.util import bpy 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 = [] 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 # --- 1) Auslieferungszustand --------------------------------------------- print("Defaults: Dichte %.0f, Groesse %.2f, Setzlings-Anteil %.2f" % (s.leaf_density, s.leaf_size, s.leaf_size_growth)) if not (0.6 <= s.leaf_size <= 1.1): fails.append("leaf_size ist %.2f - fuer Bueschel-Atlanten werden ~0.85 " "gebraucht, 0.2 waere ein einzelnes Blaettchen" % s.leaf_size) # S3/S4 stehen auf 4300, nicht mehr auf 4000: die Spielseite hebt das Budget # in ASSETS.md an, nachdem der 170er-Export abgenommen wurde. Die Doku folgt dem # Auge, aber erst nach dem Export - deshalb steht die Zahl hier schon. if s.leaf_budget_s1 > 800 or s.leaf_budget_s2 > 1200 or s.leaf_budget > 4300: fails.append("Stufen-Budgets (%d/%d/%d) ueber der Vorgabe (800/1200/4300)" % (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget)) if not (130.0 <= s.leaf_density <= 200.0): fails.append("leaf_density ist %.0f - der Sweetspot liegt bei 130-170" % s.leaf_density) # Die Deckung steht als Tabelle pro Stufe, nicht als Formel. Card-ANZAHL muss # ueber die Stufen steigen, Card-Groesse auch. _d0, _g0, _h0 = tg._stufen_anteile(0.0) _d1, _g1, _h1 = tg._stufen_anteile(1.0) if _g0 >= _g1: fails.append("Groessen-Anteil steigt nicht ueber die Stufen (%.2f -> %.2f)" % (_g0, _g1)) # Der Volumen-Hebel: bei 0.02 trugen nur die aeussersten Zweigspitzen Laub und # die Krone war innen nackt. if s.leaf_max_radius < 0.035: fails.append("leaf_max_radius ist %.3f - unter 0.035 tragen nur die " "Zweigspitzen Laub und die Krone bleibt innen nackt" % s.leaf_max_radius) if s.leaf_tilt < 0.7: fails.append("leaf_tilt ist %.2f - unter 0.7 fehlt das Rotations-Chaos, " "das die Sichtluecken schliesst" % s.leaf_tilt) if abs(tg.LEAF_DEFAULTS["Size"] - s.leaf_size) > 0.2: fails.append("LEAF_DEFAULTS['Size'] (%.2f) und die UI (%.2f) laufen " "auseinander" % (tg.LEAF_DEFAULTS["Size"], s.leaf_size)) def bauen(preset, wachstum=False, stufen=4): for o in list(bpy.context.scene.objects): if o is not card: bpy.data.objects.remove(o, do_unlink=True) s.preset = preset s.art_name = preset.capitalize() s.variant = 1 s.apply_modifier = True s.use_growth = wachstum s.stages = stufen s.leaf_card = card bpy.ops.object.treegen_create() staemme = sorted((o for o in bpy.context.scene.objects if o.type == 'MESH' and o is not card and not o.name.endswith(("_Leaf", "_Frucht"))), key=lambda o: o.name) for o in bpy.context.scene.objects: o.select_set(o in staemme) bpy.context.view_layer.objects.active = staemme[0] bpy.ops.object.treegen_leaves() return staemme def messen(stamm): bl = bpy.data.objects.get(stamm.name + "_Leaf") if bl is None: return 0, 0.0, 0 dg = bpy.context.evaluated_depsgraph_get() ev = bl.evaluated_get(dg) m = ev.to_mesh() cards = len(m.polygons) // 2 kante = math.sqrt(sum(p.area for p in m.polygons) / cards) if cards else 0.0 ev.to_mesh_clear() ev2 = stamm.evaluated_get(dg) m2 = ev2.to_mesh() tris = sum(len(p.vertices) - 2 for p in m2.polygons) + cards * 2 ev2.to_mesh_clear() return cards, kante, tris # --- 2) Alle Presets: Card-Zahl plausibel, Budget gehalten --------------- print("") # BUDGET: ASSETS.md nennt 2500 Tris fuer Baum (Stamm + Krone). Mit # Bueschel-Atlanten liegt LOD0 darueber - das ist eine bewusste Entscheidung der # Spielseite: der dichtere LOD0 traegt den Look aus der Naehe, und die LOD-Kette # dampft ihn auf 16 % ein. Die harte Grenze liegt deshalb hier bei 3000; alles # zwischen 2500 und 3000 wird als Hinweis ausgegeben, damit es nicht unbemerkt # weiterwaechst. # Die urspruenglichen 2500 waren eine konservative Setzung. Bei ISM-Instanzen # mit LOD-Kette sind Roh-Dreiecke nicht der Engpass, sondern der OVERDRAW der # Cards (die GPU zahlt pro Bildschirmflaeche, nicht pro Dreieck) und die # Schattenpaesse. Genau deshalb sind 170 grosse Bueschel guenstiger als 500 # kleine - bei gleichem Tri-Zaehler. Die Zahl hier ist die Hartgrenze fuer die # ausgewachsene Stufe; die endgueltige Freigabe kommt aus einer Messung im # Level (Testwald, stat unit / stat gpu), nicht aus diesem Test. LOD0_HART = s.leaf_budget LOD0_HINWEIS = int(s.leaf_budget * 0.8) ueber = [] print("%-12s %6s %11s %9s" % ("Preset", "Cards", "Card-Kante", "Tris ges")) for p in tg.PRESETS: staemme = bauen(p) cards, kante, tris = messen(staemme[0]) marke = "" if tris > LOD0_HART: marke = "UEBER HARTGRENZE" elif tris > LOD0_HINWEIS: marke = "ueber 2500 (LOD0 ok)" ueber.append((p, tris)) print("%-12s %6d %11.2f %9d %s" % (p, cards, kante, tris, marke)) if tris > LOD0_HART: fails.append("%s: %d Tris ueber der LOD0-Hartgrenze von %d" % (p, tris, LOD0_HART)) if p == "kaktus": continue # kein Ast duenner als Max Radius - richtig so if cards == 0: fails.append("%s: keine Cards gestreut" % p) # Ob es Bueschel oder Blaettchen sind, entscheidet die GROESSE, nicht die # Anzahl: ein grosser Baum darf 600 Bueschel tragen. Die Menge deckelt das # Tri-Budget oben ohnehin. elif kante < 0.30: fails.append("%s: Card-Kante nur %.2f m - das sind Blaettchen, keine " "Bueschel" % (p, kante)) if ueber: print("Hinweis: %s liegen ueber dem Stufenbudget - fuer LOD0 abgesegnet, bei " "Bedarf ist die Dichte der Feinregler (130-170)." % ", ".join("%s (%d)" % (p, t) for p, t in ueber)) # --- 3) Wachstum: Bueschel wachsen mit, Setzling ist NICHT kahl ---------- staemme = bauen("baum", wachstum=True, stufen=4) print("") print("%-18s %6s %6s %11s" % ("Stufe", "t", "Cards", "Card-Kante")) kanten, zahlen = [], [] for st in staemme: cards, kante, _ = messen(st) gt = float(st.get("growth_t", 1.0)) print("%-18s %6.2f %6d %11.2f" % (st.name, gt, cards, kante)) kanten.append(kante) zahlen.append(cards) if cards == 0: # Genau das war der Fehler: "Dichte" ist Punkte pro m2 Zweigflaeche, # und ein Setzling hat davon fast keine. fails.append("%s: keine Cards - ein Setzling darf nicht kahl sein" % st.name) for i in range(1, len(kanten)): if kanten[i] <= kanten[i - 1]: fails.append("Stufe %d hat Card-Kante %.2f, Stufe %d hatte %.2f - die " "Bueschel wachsen nicht mit" % (i + 1, kanten[i], i, kanten[i - 1])) if kanten and kanten[0] > 0: anteil = kanten[0] / kanten[-1] print("Setzlings-Bueschel: %.0f%% der Endgroesse (Regler steht auf %.0f%%)" % (100 * anteil, 100 * s.leaf_size_growth)) # Der gemessene Anteil darf nicht ueber dem Regler liegen - dann haette die # Skalierung nicht gegriffen. if anteil > s.leaf_size_growth + 0.15: fails.append("Setzlings-Bueschel sind %.0f%% der Endgroesse, der Regler " "steht auf %.0f%% - die Wachstums-Kopplung greift nicht" % (100 * anteil, 100 * s.leaf_size_growth)) # --- 3a) Hoehen-Filter: der Fuss bleibt frei ----------------------------- # Der Radius-Filter allein reicht nicht. Er trennt duenn von dick, aber ein # junger Stamm ist UEBERALL duenn - gemessen sassen beim Setzling 44 % der # Cards unter 40 % der Hoehe, die tiefste bei 6 %. Das ergibt eine belaubte # Saeule statt einer Krone. Der Hoehen-Filter setzt den Kronenansatz je Stufe. print("") print("%-18s %7s %6s %11s %11s" % ("Stufe", "Hoehe", "Cards", "unter Ansatz", "tiefste Card")) for st in staemme: bl = bpy.data.objects.get(st.name + "_Leaf") if bl is None: continue zs = [v.co.z for v in st.data.vertices] z0, h = min(zs), max(zs) - min(zs) gt = float(st.get("growth_t", 1.0)) _, _, ansatz = tg._stufen_anteile(gt) dg = bpy.context.evaluated_depsgraph_get() ev = bl.evaluated_get(dg) m = ev.to_mesh() # Card-Mitten ueber die Face-Gruppen (die Testkarte hat 2 Faces je Card) proc = 2 mitten = [] for i in range(0, len(m.polygons), proc): gruppe = m.polygons[i:i + proc] mitten.append(sum(p.center.z for p in gruppe) / len(gruppe)) ev.to_mesh_clear() if not mitten: continue unter = 100.0 * sum(1 for z in mitten if (z - z0) / h < ansatz) / len(mitten) tiefste = 100.0 * (min(mitten) - z0) / h print("%-18s %7.2f %6d %9.0f%% %10.0f%%" % (st.name, h, len(mitten), unter, tiefste)) # Ein paar Prozent Toleranz: die Card-Mitte darf leicht unter den Ansatz # rutschen, weil die Card um ihren Punkt herum sitzt. if unter > 15.0: fails.append("%s: %.0f%% der Cards unter dem Kronenansatz (%.0f%% der " "Hoehe) - der Fuss ist belaubt" % (st.name, unter, 100 * ansatz)) if tiefste < ansatz * 100 * 0.7: fails.append("%s: tiefste Card bei %.0f%% der Hoehe, Kronenansatz liegt " "bei %.0f%% - Laub klebt am Stamm" % (st.name, tiefste, 100 * ansatz)) # Der Ansatz muss beim Setzling HOEHER liegen als beim Altbaum: dessen # Verteilung passt schon von selbst, der Setzling braucht die Hilfe. _, _, a_jung = tg._stufen_anteile(0.0) _, _, a_alt = tg._stufen_anteile(1.0) print("Kronenansatz: S1 %.0f%%, S4 %.0f%%" % (100 * a_jung, 100 * a_alt)) if a_jung <= a_alt: fails.append("Kronenansatz beim Setzling (%.2f) nicht hoeher als beim " "Altbaum (%.2f)" % (a_jung, a_alt)) # --- 3b) Budget je Stufe statt global ----------------------------------- print("") print("%-18s %6s %8s %8s" % ("Stufe", "t", "Tris", "Budget")) for st in staemme: cards, _, tris = messen(st) gt = float(st.get("growth_t", 1.0)) budget = tg._stufen_budget(s, gt) print("%-18s %6.2f %8d %8d %s" % (st.name, gt, tris, budget, "UEBER" if tris > budget else "")) if tris > budget: fails.append("%s: %d Tris ueber dem Stufenbudget %d - ein Setzling mit " "Altbaum-Dichte ist Verschwendung" % (st.name, tris, budget)) # Die Budgets muessen nach oben gehen, sonst ist die Staffelung sinnlos. if not (s.leaf_budget_s1 < s.leaf_budget_s2 <= s.leaf_budget): fails.append("Stufen-Budgets steigen nicht: %d / %d / %d" % (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget)) # --- 4) Ohne Wachstum bleibt alles unveraendert ------------------------- staemme = bauen("baum", wachstum=False) cards_ohne, kante_ohne, _ = messen(staemme[0]) print("") print("Ohne Wachstum: %d Cards, Kante %.2f (growth_t=%.2f)" % (cards_ohne, kante_ohne, float(staemme[0].get("growth_t", 1.0)))) if abs(kante_ohne - kanten[-1]) > 0.02: fails.append("Ein Baum ohne Wachstums-Stufen (%.2f) sieht anders aus als " "die letzte Stufe (%.2f)" % (kante_ohne, kanten[-1])) print("") if fails: print("ERGEBNIS: %d FEHLER" % len(fails)) for x in fails: print(" - " + x) sys.exit(1) print("ERGEBNIS: ALLE CHECKS OK")