Tree Gen 1.33.0: Hoehen-Filter - der Setzling bekommt einen freien Fuss

Befund aus der Turnhalle: die Setzlings-Blaetter verteilen sich ueber die ganze
Stammhoehe, es entsteht eine belaubte Saeule statt einer Krone. Nachgemessen:
44 % der Cards sassen unter 40 % der Baumhoehe, die tiefste bei 6 %.

Der Radius-Filter kann das nicht loesen - er trennt duenn von dick, aber ein
junger Stamm ist UEBERALL duenn. Also ein zweiter Filter ueber die Hoehe: Laub
erst ab dem Kronenansatz, als Anteil der Baumhoehe. Die Schwelle kommt aus der
Bounding Box des Baums, nicht als Meterwert - fest waere sie beim Setzling die
halbe Pflanze und beim Altbaum wirkungslos.

Kronenansatz je Stufe (LEAF_STUFEN): S1 40 %, S2 35 %, S3 30 %, S4 25 %.

               unter Ansatz          tiefste Card
        vorher      nachher      vorher    nachher
  S1      44 %          0 %        6 %       46 %
  S2      32 %          0 %       24 %       36 %
  S3       9 %          0 %       31 %       34 %
  S4       3 %          0 %       34 %       34 %   (unveraendert)

Beim Altbaum ist der Filter praktisch ein Sicherheitsnetz - dort sitzt ohnehin
nichts tief, genau wie gemeldet.

Dichte von S1/S2 nachgezogen: der Filter nimmt dem Setzling sonst die Haelfte
der Cards (9 -> 4), weil dessen Laub vorher ueber die ganze Stammhoehe verteilt
war. Jetzt 11 Cards bei S1, und 486 Tris gegen 800 Budget.

23 Tests bestehen (Suite zweimal durchlaufen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 10:09:04 +02:00
co-authored by Claude Opus 4.8
parent d170e4067f
commit d02562fb53
8 changed files with 137 additions and 21 deletions
+54 -11
View File
@@ -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))