diff --git a/README.md b/README.md index fff4b45..b098139 100644 --- a/README.md +++ b/README.md @@ -403,6 +403,24 @@ gemessen und abgenommen: Zweigspitzen Laub, die Krone hatte nur Kontur und war innen nackt. Bei 0.04 wachsen Bueschel auch am mittleren Geaest und die Krone bekommt Tiefe. +**Ziel-Cardzahl statt Dichte-Konstanten.** Dichte-Faktoren mussten je Preset neu +geeicht werden: „Dichte" ist Punkte pro m² *Zweigflaeche*, und die verteilt sich +je Baum voellig anders — ein Faktor, der am Standardbaum 11 Cards ergab, +ueberschoss auf der Eiche um mehr als das Doppelte. In `LEAF_STUFEN` steht +deshalb die gewuenschte **Stueckzahl**, und der Generator regelt die Dichte per +Bisektion dagegen ein (seed-stabil, der Seed wird nicht angefasst). Damit ist die +Kalibrierung preset-unabhaengig und der Test prueft die Anzahl **direkt**: + +| Preset | S1 (Ziel 10) | S2 (Ziel 40) | S3 (Ziel 120) | S4 (voll) | +|---|---|---|---|---| +| baum | 9 · 518 Tris | 41 · 914 | 104 · 1436 | 130 · 1916 | +| eiche | 10 · 576 | 43 · 998 | 138 · 1872 | 463 · 4206 | +| birke | 9 · 570 | 36 · 1028 | 136 · 1952 | 207 · 2894 | +| tanne | 9 · 778 | 38 · 1160 | 125 · 2282 | 235 · 3302 | + +S4 wird **nicht** geregelt — der Altbaum ist die Referenz, an der die Dichte +ueberhaupt eingestellt wurde. + **Deckung pro Stufe, nicht per Formel.** Card-Anteil und Groesse stehen als Tabelle (`LEAF_STUFEN`). Der Dichte-Anteil bei S1/S2 sieht hoch aus — das taeuscht: „Dichte" ist Punkte pro m² *Zweigflaeche*, und die waechst ueber die diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 502981a..5ea4525 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.33.0" +version = "1.34.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 9803a52..b7b91ca 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.33.0 + stylized_tree_generator-1.34.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 38.5KB + 40.0KB -

Built 2026-08-06, 08:08

+

Built 2026-08-06, 08:32

diff --git a/dist/index.json b/dist/index.json index 3449975..4fcc035 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.33.0", + "version": "1.34.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.33.0.zip", - "archive_size": 39431, - "archive_hash": "sha256:757cecfccbac8f1a50eadd62d47b9439d98550ad2f0677604ed6832610fd74f8" + "archive_url": "./stylized_tree_generator-1.34.0.zip", + "archive_size": 40964, + "archive_hash": "sha256:1cb548b5fef9bb79342ef92f7bc40f7ecb66e21f6356c8e00623ada5c842b0d6" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.33.0.zip b/dist/stylized_tree_generator-1.33.0.zip deleted file mode 100644 index 8e4a01d..0000000 Binary files a/dist/stylized_tree_generator-1.33.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.34.0.zip b/dist/stylized_tree_generator-1.34.0.zip new file mode 100644 index 0000000..2928779 Binary files /dev/null and b/dist/stylized_tree_generator-1.34.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 0dafb36..e28012f 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, 33, 0), + "version": (1, 34, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -1434,20 +1434,30 @@ def leaf_socket_ids(ng): LEAF_RADIUS_YOUNG = 0.20 # Dichte-Anteil Groessen-Anteil -# Dichte Groesse Kronenansatz +# ZIEL-ANZAHL statt Dichte-Anteil. +# +# Dichte-Faktoren waren preset-abhaengig und mussten fuer jeden Baum neu +# geeicht werden: "Dichte" ist Punkte pro m2 Zweigflaeche, und die verteilt sich +# je Preset voellig anders. Ein Faktor, der am Standardbaum 11 Cards ergab, +# lieferte auf der Eiche mehr als das Doppelte - die Kompensation ueberschoss. +# +# Jetzt steht die gewuenschte Card-ANZAHL da, und der Generator regelt die +# Dichte per Bisektion dagegen ein. Das ist preset-unabhaengig, und der Test +# kann die Anzahl DIREKT pruefen statt ueber Dichte x Anteil x Flaeche zu +# rechnen. None = nicht regeln, volle Dichte (der Altbaum ist die Referenz, +# an der die Dichte ueberhaupt eingestellt wurde). +# +# Ziel-Cards Groesse Kronenansatz LEAF_STUFEN = ( - # S1/S2 Dichte nachgezogen, nachdem der Hoehen-Filter dazukam: er nimmt dem - # Setzling die Haelfte der Cards (9 -> 4), weil dessen Laub vorher ueber die - # ganze Stammhoehe verteilt war. - (13.0, 0.45, 0.40), # S1 - wenige kleine Bueschel, Fuss bleibt frei - (2.10, 0.63, 0.35), # S2 - (1.00, 0.82, 0.30), # S3 - (1.00, 1.00, 0.25), # S4 - Sicherheitsnetz, hier sitzt ohnehin nichts tief + (10, 0.45, 0.40), # S1 - wenige kleine Bueschel, Fuss bleibt frei + (40, 0.63, 0.35), # S2 + (120, 0.82, 0.30), # S3 + (None, 1.00, 0.25), # S4 - volle Dichte, keine Regelung ) def _stufen_anteile(growth_t): - """Dichte-, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt. + """Ziel-Cardzahl, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt. Die Stufen liegen bei t = 0 / 1/3 / 2/3 / 1 (vier Stufen). Zwischen den Stuetzstellen wird linear gemischt, damit auch drei oder fuenf Stufen @@ -1457,9 +1467,12 @@ def _stufen_anteile(growth_t): x = max(0.0, min(1.0, growth_t)) * (n - 1) i = min(int(x), n - 2) f = x - i - d0, g0, h0 = LEAF_STUFEN[i] - d1, g1, h1 = LEAF_STUFEN[i + 1] - return (d0 + (d1 - d0) * f, + z0, g0, h0 = LEAF_STUFEN[i] + z1, g1, h1 = LEAF_STUFEN[i + 1] + # Groesse und Kronenansatz werden gemischt, die Ziel-ANZAHL nicht: sie ist + # eine Stueckzahl, kein Verlauf. Es gilt die naeher liegende Stuetzstelle. + ziel = z0 if f < 0.5 else z1 + return (ziel, g0 + (g1 - g0) * f, h0 + (h1 - h0) * f) @@ -1492,6 +1505,88 @@ def _modifier_anstossen(md): md.show_viewport = sichtbar + +def _cards_pro_stueck(card_obj, card_coll): + """Wie viele Faces hat EINE Card? Fuer die Umrechnung Faces -> Cardzahl.""" + quellen = [] + if card_coll is not None: + quellen = [o for o in card_coll.objects if o.type == 'MESH'] + elif card_obj is not None and card_obj.type == 'MESH': + quellen = [card_obj] + zahlen = [len(o.data.polygons) for o in quellen if o.data.polygons] + if not zahlen: + return 1 + return max(1, int(round(sum(zahlen) / float(len(zahlen))))) + + +def _card_zahl(ob, pro_stueck, dg=None): + """Anzahl gestreuter Cards auf einer ausgewerteten Blatt-Ebene.""" + dg = dg or bpy.context.evaluated_depsgraph_get() + ev = ob.evaluated_get(dg) + me = ev.to_mesh() + n = len(me.polygons) + ev.to_mesh_clear() + return int(round(n / float(max(pro_stueck, 1)))) + + +def _dichte_einregeln(context, ob, md, ids, ziel, start_dichte, pro_stueck, + schritte=14, toleranz=0.15): + """Regelt die Dichte per Bisektion auf eine Ziel-Cardzahl ein. + + Warum ueberhaupt regeln: "Dichte" ist Punkte pro m2 Zweigflaeche, und die + faellt je Preset voellig anders aus. Ein Faktor, der am Standardbaum passte, + ueberschoss auf der Eiche um mehr als das Doppelte. Eine Stueckzahl ist + dagegen preset-unabhaengig - und direkt pruefbar. + + Seed-stabil: der Seed wird nicht angefasst, nur die Dichte. Gleicher Baum + und gleicher Seed ergeben deshalb dieselbe Verteilung. + """ + if not ziel or ziel <= 0: + return start_dichte, _card_zahl(ob, pro_stueck) + + def _messen(dichte): + md[ids["Density"]] = float(dichte) + _modifier_anstossen(md) + context.view_layer.update() + return _card_zahl(ob, pro_stueck) + + lo, hi = start_dichte, start_dichte + n = _messen(start_dichte) + if n == ziel: + return start_dichte, n + + # Klammer suchen: verdoppeln bzw. halbieren, bis das Ziel eingeschlossen ist. + if n < ziel: + for _ in range(12): + hi = hi * 2.0 + if _messen(hi) >= ziel: + break + lo = start_dichte + else: + for _ in range(12): + lo = lo * 0.5 + if lo < 1e-3 or _messen(lo) <= ziel: + break + hi = start_dichte + + bestes = (abs(n - ziel), start_dichte, n) + for _ in range(schritte): + mitte = (lo + hi) * 0.5 + n = _messen(mitte) + if abs(n - ziel) < bestes[0]: + bestes = (abs(n - ziel), mitte, n) + if abs(n - ziel) <= max(1, ziel * toleranz): + return mitte, n + if n < ziel: + lo = mitte + else: + hi = mitte + + # Beste gefundene Naeherung wiederherstellen. + _messen(bestes[1]) + return bestes[1], bestes[2] + + def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overrides): """Erzeugt/aktualisiert ein _Leaf-Objekt mit dem Blattwerk-Modifier.""" name = name or (tree_obj.name + "_Leaf") @@ -2219,9 +2314,23 @@ def _origin_bottom_center(ob): ob.matrix_parent_inverse = Matrix.Identity(4) if not me.vertices: 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] + + # Der Ursprung kommt aus der STAMM-Geometrie (Slot 0), nicht aus der + # Gesamt-Bbox. Sonst hebt eine einzige tief haengende Blatt-Card den ganzen + # Baum an und er steht in UE in der Luft - gemeldet am S1-Setzling, wo die + # Krone relativ zum kurzen Stamm weit ausladet. + stamm_verts = None + if len(me.materials) > 1: + idx = {vi for p in me.polygons if p.material_index == 0 + for vi in p.vertices} + if idx: + stamm_verts = [me.vertices[i].co for i in idx] + if stamm_verts is None: + stamm_verts = [v.co for v in me.vertices] + + xs = [c.x for c in stamm_verts] + ys = [c.y for c in stamm_verts] + zs = [c.z for c in stamm_verts] versatz = Matrix.Translation(( -(min(xs) + max(xs)) * 0.5, -(min(ys) + max(ys)) * 0.5, @@ -2596,7 +2705,7 @@ class TREEGEN_OT_leaves(Operator): # traegt kleinere Bueschel. Baeume ohne Wachstums-Stufe haben # growth_t = 1.0 und bleiben unveraendert. gt = float(t.get("growth_t", 1.0)) - d_anteil, g_anteil, h_anteil = _stufen_anteile(gt) + ziel_cards, g_anteil, h_anteil = _stufen_anteile(gt) # Der RADIUS-FILTER muss mit dem Baum schrumpfen. Er trennt # duenne Zweige (Laub) von dickem Geaest (kein Laub) - aber ein @@ -2613,12 +2722,26 @@ class TREEGEN_OT_leaves(Operator): lf = make_leaves(context, t, s.leaf_card, card_coll=s.leaf_cards, - Density=s.leaf_density * d_anteil, + Density=s.leaf_density, Size=s.leaf_size * g_anteil, **{"Size Var": s.leaf_size_var, "Max Radius": s.leaf_max_radius * r_faktor, "Min Height": h_anteil, "Tilt": s.leaf_tilt, "Seed": s.seed}) + # Dichte auf die ZIEL-CARDZAHL einregeln. Muss nach dem Anlegen + # passieren, weil dafuer gemessen werden muss, was tatsaechlich + # herauskommt - die Zweigflaeche faellt je Preset voellig + # anders aus. + if ziel_cards: + md = lf.modifiers.get("GN_Leaves") + ng = md.node_group if md is not None else None + if ng is not None: + pro = _cards_pro_stueck(s.leaf_card, s.leaf_cards) + dichte, ist = _dichte_einregeln( + context, lf, md, leaf_socket_ids(ng), + ziel_cards, s.leaf_density, pro) + print("[TreeGen] %s: %d Cards (Ziel %d) bei Dichte %.0f" + % (t.name, ist, ziel_cards, dichte)) except Exception as exc: # noqa: BLE001 self.report({'WARNING'}, "%s: %s" % (t.name, exc)) continue diff --git a/tests/test_blatt_bueschel.py b/tests/test_blatt_bueschel.py index 9915e65..f6c5b4a 100644 --- a/tests/test_blatt_bueschel.py +++ b/tests/test_blatt_bueschel.py @@ -52,8 +52,8 @@ if not (130.0 <= s.leaf_density <= 200.0): % 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) +_z0, _g0, _h0 = tg._stufen_anteile(0.0) +_z1, _g1, _h1 = tg._stufen_anteile(1.0) if _g0 >= _g1: fails.append("Groessen-Anteil steigt nicht ueber die Stufen (%.2f -> %.2f)" % (_g0, _g1)) @@ -275,6 +275,34 @@ 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])) +# --- 3c) Ziel-Cardzahl wird preset-UNABHAENGIG eingeregelt --------------- +# Frueher standen hier Dichte-Faktoren. Die mussten je Preset neu geeicht +# werden: "Dichte" ist Punkte pro m2 Zweigflaeche, und die verteilt sich je +# Baum voellig anders - ein Faktor, der am Standardbaum 11 Cards ergab, +# ueberschoss auf der Eiche um mehr als das Doppelte. Jetzt steht die +# Stueckzahl da und wird eingeregelt; der Test prueft sie DIREKT. +print("") +print("%-10s %8s %8s %8s %8s" % ("Preset", "S1", "S2", "S3", "S4")) +for pname in ("baum", "eiche", "birke"): + reihe = bauen(pname, wachstum=True, stufen=4) + pro = tg._cards_pro_stueck(card, None) + dg = bpy.context.evaluated_depsgraph_get() + zellen = [] + for st in reihe: + bl = bpy.data.objects.get(st.name + "_Leaf") + n = tg._card_zahl(bl, pro, dg) if bl else 0 + gt = float(st.get("growth_t", 1.0)) + ziel, _, _ = tg._stufen_anteile(gt) + zellen.append((n, ziel)) + if ziel: + # 25 % Toleranz: die Bisektion arbeitet auf einer diskreten + # Punktverteilung, exakt treffen kann sie nicht. + if abs(n - ziel) > max(2, ziel * 0.25): + fails.append("%s/%s: %d Cards, Ziel %d - die Regelung trifft " + "nicht" % (pname, st.name, n, ziel)) + print("%-10s %8s %8s %8s %8s" + % (pname, *["%d/%s" % (n, z if z else "voll") for n, z in zellen])) + print("") if fails: print("ERGEBNIS: %d FEHLER" % len(fails))