diff --git a/README.md b/README.md index 1ad51e7..fff4b45 100644 --- a/README.md +++ b/README.md @@ -416,6 +416,24 @@ ohne Anhebung bekaeme er null Cards. Was zaehlt, ist die Anzahl: | S3 | 67 | 0.37 | 946 | 4300 | | S4 | 130 | 0.45 | 1396 | 4300 | +**Der Hoehen-Filter haelt den Fuss 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 Baumhoehe, die +tiefste bei **6 %** — eine belaubte Saeule statt einer Krone. Der Kronenansatz +steht deshalb je Stufe in `LEAF_STUFEN` (Anteil der Baumhoehe, aus der Bounding +Box abgeleitet — ein fester Meterwert waere beim Setzling die halbe Pflanze): + +| Stufe | Ansatz | unter Ansatz vorher | nachher | tiefste Card | +|---|---|---|---|---| +| S1 | 40 % | 44 % | **0 %** | 6 % → **46 %** | +| S2 | 35 % | 32 % | **0 %** | 24 % → 36 % | +| S3 | 30 % | 9 % | **0 %** | 31 % → 34 % | +| S4 | 25 % | 3 % | **0 %** | 34 % (unveraendert) | + +Beim Altbaum ist der Filter praktisch ein Sicherheitsnetz — dort sitzt ohnehin +nichts tief. Die Dichte von S1/S2 wurde nachgezogen, weil der Filter dem +Setzling sonst die Haelfte der Cards nimmt. + **Der Radius-Filter skaliert mit.** Er trennt duenne Zweige (Laub) von dickem Geaest — aber ein Setzling ist *komplett* duenner als eine feste Schwelle. Mit starren 0.04 m sassen bei S2 **81 %** der Cards am Stamm, bei S3 58 %; jetzt diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 2972e06..502981a 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.32.0" +version = "1.33.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 ce3e649..9803a52 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.32.0 + stylized_tree_generator-1.33.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 37.8KB + 38.5KB -

Built 2026-08-06, 07:44

+

Built 2026-08-06, 08:08

diff --git a/dist/index.json b/dist/index.json index 2860f12..3449975 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.32.0", + "version": "1.33.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.32.0.zip", - "archive_size": 38712, - "archive_hash": "sha256:b92b7caccf18b7690b63cee35b8597484a0a789d47a9df529857d23bbf57be8f" + "archive_url": "./stylized_tree_generator-1.33.0.zip", + "archive_size": 39431, + "archive_hash": "sha256:757cecfccbac8f1a50eadd62d47b9439d98550ad2f0677604ed6832610fd74f8" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.32.0.zip b/dist/stylized_tree_generator-1.32.0.zip deleted file mode 100644 index 0b4e4e8..0000000 Binary files a/dist/stylized_tree_generator-1.32.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.33.0.zip b/dist/stylized_tree_generator-1.33.0.zip new file mode 100644 index 0000000..8e4a01d Binary files /dev/null and b/dist/stylized_tree_generator-1.33.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index b57639f..0dafb36 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, 32, 0), + "version": (1, 33, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -1202,6 +1202,7 @@ LEAF_DEFAULTS = { # UE gegengeprueft, nicht geschaetzt. "Max Radius": 0.04, "Size": 0.85, + "Min Height": 0.25, # Kronenansatz als Anteil der Baumhoehe "Size Var": 0.35, "Tilt": 0.9, # mehr Rotations-Chaos schliesst Sichtluecken "Seed": 0, @@ -1248,6 +1249,11 @@ def build_leaf_group(rebuild=False): add_in("Use Collection", 'NodeSocketBool', False) add_in("Density", 'NodeSocketFloat', LEAF_DEFAULTS["Density"], 0.0, 20000.0) add_in("Max Radius", 'NodeSocketFloat', LEAF_DEFAULTS["Max Radius"], 0.001, 0.5) + # Kronenansatz als ANTEIL der Baumhoehe. 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. + add_in("Min Height", 'NodeSocketFloat', LEAF_DEFAULTS["Min Height"], 0.0, 0.95) add_in("Size", 'NodeSocketFloat', LEAF_DEFAULTS["Size"], 0.01, 5.0) add_in("Size Var", 'NodeSocketFloat', LEAF_DEFAULTS["Size Var"], 0.0, 1.0) add_in("Tilt", 'NodeSocketFloat', LEAF_DEFAULTS["Tilt"], 0.0, 3.14159) @@ -1289,9 +1295,39 @@ def build_leaf_group(rebuild=False): L(_out(na, "Attribute"), cmp.inputs[0]) L(V["Max Radius"], cmp.inputs[1]) + # HOEHEN-FILTER: erst ab dem Kronenansatz. Die Schwelle wird aus der + # Bounding Box des Baums abgeleitet, damit sie fuer jede Groesse stimmt - + # ein fester Meterwert waere beim Setzling die halbe Pflanze. + bb = N("GeometryNodeBoundBox"); bb.location = (-700, -60) + L(_out(tree, "Geometry"), _sock(bb, "Geometry")) + bmin = N("ShaderNodeSeparateXYZ"); bmin.location = (-560, -60) + L(_out(bb, "Min"), bmin.inputs[0]) + bmax = N("ShaderNodeSeparateXYZ"); bmax.location = (-560, -140) + L(_out(bb, "Max"), bmax.inputs[0]) + bh = N("ShaderNodeMath"); bh.location = (-420, -100); bh.operation = 'SUBTRACT' + L(bmax.outputs["Z"], bh.inputs[0]); L(bmin.outputs["Z"], bh.inputs[1]) + bmul = N("ShaderNodeMath"); bmul.location = (-280, -100); bmul.operation = 'MULTIPLY' + L(bh.outputs[0], bmul.inputs[0]); L(V["Min Height"], bmul.inputs[1]) + bschwelle = N("ShaderNodeMath"); bschwelle.location = (-140, -100) + bschwelle.operation = 'ADD' + L(bmin.outputs["Z"], bschwelle.inputs[0]); L(bmul.outputs[0], bschwelle.inputs[1]) + + hpos = N("GeometryNodeInputPosition"); hpos.location = (-560, -220) + hsep = N("ShaderNodeSeparateXYZ"); hsep.location = (-420, -220) + L(hpos.outputs[0], hsep.inputs[0]) + hcmp = N("FunctionNodeCompare"); hcmp.location = (-140, -220) + hcmp.data_type = 'FLOAT'; hcmp.operation = 'GREATER_THAN' + L(hsep.outputs["Z"], hcmp.inputs[0]) + L(bschwelle.outputs[0], hcmp.inputs[1]) + + beide = N("FunctionNodeBooleanMath"); beide.location = (0, 20) + beide.operation = 'AND' + L(cmp.outputs[0], beide.inputs[0]) + L(hcmp.outputs[0], beide.inputs[1]) + dist = N("GeometryNodeDistributePointsOnFaces"); dist.location = (-320, 200) L(_out(tree, "Geometry"), _sock(dist, "Mesh")) - L(cmp.outputs[0], _sock(dist, "Selection")) + L(beide.outputs[0], _sock(dist, "Selection")) L(V["Density"], _sock(dist, "Density")) L(V["Seed"], _sock(dist, "Seed")) @@ -1398,16 +1434,20 @@ def leaf_socket_ids(ng): LEAF_RADIUS_YOUNG = 0.20 # Dichte-Anteil Groessen-Anteil +# Dichte Groesse Kronenansatz LEAF_STUFEN = ( - (6.00, 0.45), # S1 - wenige, kleine Bueschel auf winziger Zweigflaeche - (1.60, 0.63), # S2 - 15 % unter der vollen Deckung (nach Flaeche gerechnet) - (1.00, 0.82), # S3 - (1.00, 1.00), # S4 - volle Dichte + # 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 ) def _stufen_anteile(growth_t): - """Dichte- und Groessen-Anteil fuer einen Wachstums-Fortschritt. + """Dichte-, 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 @@ -1417,9 +1457,11 @@ 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 = LEAF_STUFEN[i] - d1, g1 = LEAF_STUFEN[i + 1] - return d0 + (d1 - d0) * f, g0 + (g1 - g0) * f + d0, g0, h0 = LEAF_STUFEN[i] + d1, g1, h1 = LEAF_STUFEN[i + 1] + return (d0 + (d1 - d0) * f, + g0 + (g1 - g0) * f, + h0 + (h1 - h0) * f) def _stufen_budget(s, growth_t): @@ -2554,7 +2596,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 = _stufen_anteile(gt) + d_anteil, 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 @@ -2575,6 +2617,7 @@ class TREEGEN_OT_leaves(Operator): 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}) except Exception as exc: # noqa: BLE001 self.report({'WARNING'}, "%s: %s" % (t.name, exc)) diff --git a/tests/test_blatt_bueschel.py b/tests/test_blatt_bueschel.py index 9903ba7..9915e65 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 = tg._stufen_anteile(0.0) -_d1, _g1 = tg._stufen_anteile(1.0) +_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)) @@ -193,6 +193,61 @@ if kanten and kanten[0] > 0: "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"))