diff --git a/README.md b/README.md index 3516918..d50b8c2 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,47 @@ 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. +### Kronenfuellung: es zaehlt die Flaeche, nicht die Card-Zahl + +Der Obstbaum las sich in der Engine als einzelne belaubte Ast-Schlaeuche mit +Himmel dazwischen - "behaarte Finger". Die Card-Zahl erklaert das nicht: 400 +Cards koennen eine geschlossene Haube sein oder fuenf Schlaeuche. Was es +erklaert, ist die Blattflaeche pro Kronenvolumen: + +| | Cards | Krone | Flaeche/Volumen | +|---|---|---|---| +| Eiche (schliesst) | 444 | r 1.26 x 3.78 = 18.9 m3 | **24.6 m2/m3** | +| Obstbaum vorher | 399 | r 2.00 x 2.88 = 36.2 m3 | **11.4 m2/m3** | + +Die Obstbaum-Krone hat fast das doppelte Volumen bei gleicher Card-Zahl. + +**Durchgemessen und verworfen: mehr Zweigwerk.** Sub Count 5 und 7, Sub Length +1.0 und 1.3, Sub Start 0.20 und 0.30, Branch Count 12 und 14 - der Fuellgrad +bewegte sich um hoechstens 7 Punkte, die Dreiecke stiegen auf 4310-5162. Der +Grund ist strukturell: neue Zweige wachsen aus denselben wenigen Hauptaesten +und vergroessern dabei die Krone. Das Volumen waechst mit, die gedeckelte +Card-Zahl nicht. + +**Ebenfalls verworfen: mehr Cards.** Ziel 740 (rechnerisch noetig fuer 25/m3) +ergab 5426 Tris - 26 % ueber Budget - und hob den Fuellgrad nur auf 75 %. +Cards zaehlen ist nicht dasselbe wie Flaeche decken. + +**Ebenfalls verworfen: kleinere Krone.** Branch Length 2.35 sah ueber drei +Seeds mit Kronenradius 1.83 m gut aus - im Neun-Seed-Test fielen dann SECHS +Seeds unter die 1.80 m, die die Frucht-Streuung braucht (1.73-1.79). + +Was bleibt, ist die Card-GROESSE (`LEAF_SIZE_PRESET`): sie kostet kein einziges +Dreieck, weil die Zahl gedeckelt ist. Sie kostet OVERDRAW, und der ist bei +Bueschel-Cards der eigentliche Engpass. Faktor 1.47 hebt die Deckung von 46 auf +87-114 % der Eiche und verdoppelt die Card-Flaeche (414 -> 893 m2), bei 3697 +statt 3714 Tris. + +Der Test vergleicht gegen die **im selben Lauf gemessene Eiche**, nicht gegen +eine feste Zahl. Die Deckung haengt an der Card-Flaeche - eine 6-Tri-Card +bringt das Dreifache einer flachen Raute -, und der erste Anlauf des Checks ist +genau daran gescheitert: die Schwelle stammte aus einem Messskript mit anderer +Card und meldete neun Fehler auf einem korrekten Baum. + ### Zwei Knoepfe, ein Weg Blattwerk entsteht an zwei Stellen: ueber das Haekchen "_Leaf anlegen" beim diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 9897898..607d446 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.38.0" +version = "1.39.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 6f7f08e..b1e777e 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.38.0 + stylized_tree_generator-1.39.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 45.0KB + 46.0KB -

Built 2026-08-06, 11:22

+

Built 2026-08-06, 12:09

diff --git a/dist/index.json b/dist/index.json index 02212d0..40706be 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.38.0", + "version": "1.39.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.38.0.zip", - "archive_size": 46032, - "archive_hash": "sha256:0d65140f04d1ba8289a72c69c44c95b8e7b7300681695b43c7cba7a8f2ec6c9b" + "archive_url": "./stylized_tree_generator-1.39.0.zip", + "archive_size": 47076, + "archive_hash": "sha256:44a7601ffb6cfb8004d213416cdc0fdd5592e0c2596a89ed6a32b2c06f12bb4d" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.38.0.zip b/dist/stylized_tree_generator-1.38.0.zip deleted file mode 100644 index 3902d39..0000000 Binary files a/dist/stylized_tree_generator-1.38.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.39.0.zip b/dist/stylized_tree_generator-1.39.0.zip new file mode 100644 index 0000000..246fc1e Binary files /dev/null and b/dist/stylized_tree_generator-1.39.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 9681929..32a26e7 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, 38, 0), + "version": (1, 39, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -197,6 +197,11 @@ PRESETS = { "obstbaum": { "Height": 3.8, "Trunk Radius": 0.13, "Bend": 0.3, "Taper": 0.7, "Branch Count": 9, # offene Krone: wenige Leitaeste, Licht durch + # Bleibt bei 2.55. Die Krone zu verkleinern waere der zweite Hebel gegen + # den niedrigen Fuellgrad gewesen - bei 2.35 fielen aber SECHS der neun + # Seeds unter den Kronenradius von 1.80 m, den die Frucht-Streuung + # braucht (1.73-1.79). Ueber drei Seeds gemittelt sah es mit 1.83 noch + # gut aus; erst der Neun-Seed-Test hat es aufgedeckt. "Branch Length": 2.55, "Branch Up": 0.7, "Branch Start": 0.52, "Branch End": 0.90, "Crown Taper": -0.30, "Crown Bulge": 0.60, # rund, Bauch in der Mitte @@ -1627,6 +1632,41 @@ LEAF_STUFEN_PRESET = { "obstbaum": {3: 420}, } +# Card-GROESSE je Preset, als Faktor auf die eingestellte Groesse. +# +# Warum das ueberhaupt eine eigene Tabelle braucht: die Kronenfuellung haengt an +# der Card-FLAECHE, nicht an der Card-Zahl - und wie viel Flaeche eine Krone +# braucht, haengt an ihrem Volumen. Gemessen (S4, Cards je m3 Kronenzylinder / +# Anteil belegter 0.4-m-Voxel): +# +# Eiche 444 Cards, Krone r 1.26 x 3.78 = 18.9 m3 -> 23.7/m3, 88 % belegt +# Obstbaum 399 Cards, Krone r 2.00 x 2.88 = 36.2 m3 -> 11.1/m3, 60 % belegt +# +# Die Obstbaum-Krone hat fast das doppelte Volumen bei gleicher Card-Zahl - +# daher las sie sich als einzelne belaubte Ast-Schlaeuche mit Himmel dazwischen +# ("behaarte Finger", Abnahme der Spielseite). +# +# Durchgemessen und VERWORFEN: mehr und laengere Sub-Aeste als Fuellwerk. Sub +# Count 5/7, Sub Length 1.0/1.3, Sub Start 0.20/0.30, Branch Count 12/14 - der +# Fuellgrad bewegte sich um hoechstens 7 Punkte, die Dreiecke stiegen auf +# 4310-5162. Der Grund ist strukturell: neue Zweige wachsen aus denselben wenigen +# Hauptaesten und vergroessern dabei die Krone, waehrend die Card-Zahl gedeckelt +# bleibt. Das Volumen waechst also mit. +# +# Ebenfalls verworfen: mehr Cards. Bei Ziel 740 (was rechnerisch fuer 25/m3 +# noetig waere) kamen 5426 Tris heraus - 26 % ueber Budget - und der Fuellgrad +# stieg nur auf 75 %. Cards zaehlen ist nicht dasselbe wie Flaeche decken. +# +# Was wirkt, ist die Card-Groesse: sie kostet KEIN einziges Dreieck, weil die +# Zahl gedeckelt ist. Sie kostet aber OVERDRAW - und der ist bei Bueschel-Cards +# der eigentliche Engpass, nicht der Dreieckszaehler. Deshalb der maessige +# Faktor: 1.47 hebt den Fuellgrad von 60 auf 74 % und verdoppelt die Card- +# Flaeche (414 -> 832 m2). 1.71 brachte 79 % bei 2.7-facher Flaeche - das waere +# eine Overdraw-Entscheidung der Spielseite, keine Generator-Frage. +LEAF_SIZE_PRESET = { + "obstbaum": 1.47, +} + def _stufen_anteile(growth_t, preset=None): """Ziel-Cardzahl, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt. @@ -1803,7 +1843,10 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, basis = dict(LEAF_DEFAULTS) basis.update(overrides) - overrides["Size"] = basis["Size"] * g_anteil + # Preset-Faktor auf die Card-Groesse: eine weite Krone braucht mehr + # Card-FLAECHE, um geschlossen zu wirken (siehe LEAF_SIZE_PRESET). + p_faktor = LEAF_SIZE_PRESET.get(tree_obj.get("preset") or "", 1.0) + overrides["Size"] = basis["Size"] * g_anteil * p_faktor overrides["Max Radius"] = basis["Max Radius"] * r_faktor overrides["Min Height"] = h_anteil diff --git a/tests/test_obstbaum.py b/tests/test_obstbaum.py index 41f416c..765e4ec 100644 --- a/tests/test_obstbaum.py +++ b/tests/test_obstbaum.py @@ -74,14 +74,65 @@ def inseln(m): return out +def deckung_messen(preset, seed): + """Blattflaeche je m3 Kronenzylinder - fuer beliebiges Preset. + + Der Wert haengt an der verwendeten Card (Flaeche!), taugt also NICHT als + absolute Schwelle. Verglichen wird deshalb gegen die Eiche, die in + demselben Lauf mit DERSELBEN Card gemessen wird. + """ + 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 = "Ref" + s.variant = 1 + s.seed = seed + s.use_growth = False + bpy.ops.object.treegen_create() + baum = [o for o in bpy.context.scene.objects + if o.type == 'MESH' and o is not card + 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 + bpy.ops.object.treegen_leaves() + bl = bpy.data.objects.get(baum.name + "_Leaf") + dg = bpy.context.evaluated_depsgraph_get() + ev = bl.evaluated_get(dg) + m = ev.to_mesh() + flaeche = sum(p.area for p in m.polygons) + mitten = [] + for grp in inseln(m): + pts = [m.vertices[i].co for i in grp] + mitten.append((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))) + ev.to_mesh_clear() + lr = max(math.hypot(x, y) for x, y, _z in mitten) + lz = [z for _x, _y, z in mitten] + vol = math.pi * lr ** 2 * (max(lz) - min(lz)) + return flaeche / vol if vol else 0.0 + + +# Die EICHE ist der Massstab: sie schliesst laut Abnahme der Spielseite, der +# Obstbaum tat es nicht. Hier gemessen statt als Konstante hingeschrieben, +# weil der Wert an der Card-Flaeche haengt - eine feste Zahl waere nur fuer +# genau diese Testkarte richtig. +EICHE = sum(deckung_messen("eiche", sd) for sd in (1, 2, 3)) / 3.0 +print("Massstab Eiche: %.2f m2 Blatt je m3 Krone (mit DIESER Testcard)" % EICHE) +print("") + # Die Seeds sind die der Spielseite: Apfel 1/15/22, Kirsche 2/16/23, # Birne 3/17/24. Ein Preset, das nur bei einem Seed passt, ist keines. SEEDS = (1, 15, 22, 2, 16, 23, 3, 17, 24) -print("%-6s %7s %6s %9s %9s %9s %7s" - % ("Seed", "Hoehe", "B/H", "KronR", "Card-Max", "KronR/Card", "Ansatz")) +print("%-6s %7s %6s %9s %9s %9s %7s %9s" + % ("Seed", "Hoehe", "B/H", "KronR", "Card-Max", "KronR/Card", "Ansatz", + "m2/m3")) verh = [] +deckungen = [] for seed in SEEDS: for o in list(bpy.context.scene.objects): if o is not card: @@ -115,19 +166,57 @@ for seed in SEEDS: ansatz = 100.0 * (min(astz) - z0) / hoehe if astz else 0.0 card_max = 0.0 + blatt_flaeche = 0.0 + kron_zyl = 0.0 if bl is not None: ev = bl.evaluated_get(dg) m2 = ev.to_mesh() + blatt_flaeche = sum(p.area for p in m2.polygons) + mitten = [] for grp in inseln(m2): pts = [m2.vertices[i].co for i in grp] card_max = max(card_max, max((a - b).length for a in pts for b in pts)) + mitten.append((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))) ev.to_mesh_clear() + if mitten: + lr = max(math.hypot(x, y) for x, y, _z in mitten) + lz = [z for _x, _y, z in mitten] + kron_zyl = math.pi * lr ** 2 * (max(lz) - min(lz)) v = kron_r / card_max if card_max else 0.0 verh.append(v) - print("%-6d %7.2f %6.2f %9.2f %9.2f %9.2f %6.0f%%" - % (seed, hoehe, 2 * kron_r / hoehe, kron_r, card_max, v, ansatz)) + deckung = blatt_flaeche / kron_zyl if kron_zyl else 0.0 + deckungen.append(deckung) + print("%-6d %7.2f %6.2f %9.2f %9.2f %9.2f %6.0f%% %9.1f" + % (seed, hoehe, 2 * kron_r / hoehe, kron_r, card_max, v, ansatz, + deckung)) + + # Kronen-DECKUNG: Blattflaeche je m3 Kronenvolumen. + # + # Die Card-ZAHL sagt das nicht: 400 Cards koennen eine geschlossene Haube + # sein oder fuenf belaubte Schlaeuche mit Himmel dazwischen ("behaarte + # Finger", von der Spielseite abgenommen). Was es sagt, ist die Flaeche pro + # Volumen - und die trennt die beiden Faelle sauber: + # Eiche (schliesst) 465 m2 / 18.9 m3 = 24.6 + # Obstbaum vorher (offen) 414 m2 / 36.2 m3 = 11.4 + # Die Obstbaum-Krone hat fast das doppelte Volumen; bei gleicher Card-Zahl + # blieb die halbe Deckung uebrig. + # + # Verglichen wird gegen die im selben Lauf gemessene Eiche, NICHT gegen + # eine feste Zahl: die Deckung haengt an der Card-Flaeche, und eine + # 6-Tri-Buescheldkarte bringt das Dreifache einer flachen Raute. Eine + # hart notierte 20 waere nur fuer genau eine Testcard richtig gewesen - + # der erste Anlauf dieses Checks ist genau daran gescheitert. + # 60 % der Eiche: der Obstbaum liegt bauartbedingt darunter (weitere + # Krone bei gleichem Budget), lag vorher aber bei 46 %. + if deckung < EICHE * 0.60: + fails.append("Seed %d: nur %.1f m2 Blatt je m3 Krone, Eiche %.1f " + "(%.0f %%) - die Krone liest sich als einzelne " + "Ast-Schlaeuche" % (seed, deckung, EICHE, + 100 * deckung / EICHE)) # DIE funktionale Bedingung. if v < 1.3: @@ -152,6 +241,10 @@ for seed in SEEDS: # funktionale (Kronenradius), weil an ihr die Frucht-Streuung haengt. print("") print("Kronenradius/Card ueber alle Seeds: %.2f - %.2f" % (min(verh), max(verh))) +print("Kronen-Deckung ueber alle Seeds: %.1f - %.1f m2/m3, Eiche %.1f " + "(%.0f-%.0f %%)" + % (min(deckungen), max(deckungen), EICHE, + 100 * min(deckungen) / EICHE, 100 * max(deckungen) / EICHE)) if fails: print("ERGEBNIS: %d FEHLER" % len(fails))