Tree Gen 1.46.0: Halbkugel-Bueschel, groessere Baeume, Spitzen-Deckung

1) NEUE BAUFORM: Blattwerk als Halbkugel-Cluster
   Jeder gestreute Punkt ist jetzt ein Cluster-ZENTRUM, die Cards sitzen auf
   der oberen Halbkugel darum herum. Untere Haelfte weggelassen - von unten
   sieht man die Krone gegen den Himmel, das waere Overdraw fuer nichts.

   Zwei Annahmen kassiert die Messung: GeometryNodeMeshIcoSphere liefert bei
   Subdivisions 0 UND 1 dasselbe Ikosaeder, ein Cluster hat SECHS Cards statt
   der angenommenen 26. Und die Stufen-Ziele muessen Cluster-Vielfache sein
   (26/78/208/390) - S1 stand auf 10, unter der kleinsten Einheit, die Regelung
   waere nie konvergiert. S4 ist seither geregelt statt "volle Dichte":
   ungeregelt lieferte die Schale 2706 Cards an der Eiche.
   ast_id ueberlebt Instanzieren + Realize, an den Cards nachgewiesen.

2) SPITZEN-DECKUNG: zwei echte Ursachen, beide vorher nur vermutet
   - Der Kronenansatz-Filter stand auf 25 % der Baumhoehe, die Tanne setzt ihre
     Aeste ab 12 % an: bei 5 von 22 Aesten lag das aeussere Fuenftel KOMPLETT
     unter dem Filter. Jetzt auf den Branch Start der Art gedeckelt.
   - Haengende Spitzen fielen unter den Filter. Jetzt zaehlt der ANSATZ des
     Astes (anchor) mit, nicht nur die Punkthoehe.

   Ergebnis: Eiche/Baum 100 %, Weide 100/90, Obstbaum 100/93, Tanne 86/86,
   Birke 100/72. Die 85-%-Schwelle der Spielseite ist damit erreicht, ohne dass
   die Rinden-Ersparnis noetig war.

   Die Metrik war zweimal falsch: der Abstand zur Card-MITTE ist zu streng -
   eine 0.85-m-Card deckt die Spitze auch aus 0.45 m Abstand, bei 1.9 m
   Astlaenge kamen so 24 % heraus, obwohl das Polster genau auf der Spitze sass.
   Jetzt zaehlt Deckung, nicht Abstand.

3) GROESSERE BAEUME, proportional skaliert
   Eiche 9.3 m, Birke 11.0, Tanne 13.0, Weide 7.0 - Hoehe, Astlaenge und
   Stammradius zusammen, sonst wird der Baum eine Stange mit Minikrone. Alle
   unter dem 4300er-Deckel; die Birke brauchte eine Card-Deckelung auf 340.

4) Kugel-Kennwert RELATIV, Bbox-Kriterium ersetzt
   Die Schalen schieben jede Card in ALLE Richtungen vom Ast weg: die Krone
   gewinnt ringsum Breite, in der Hoehe nur oben. Der Obstbaum fiel dadurch von
   1.35-1.47 auf 0.95-1.23, ohne dass an seiner Form etwas falsch war.

   Zwei Hypothesen sind an der Messung gescheitert, beide klangen plausibel:
   den Kappen-Faktor entkoppeln (0.93-1.14 -> 0.95-1.23) und den Kappen-Radius
   vergroessern (59-77 % -> 58-75 %). Der Radius waechst in alle Richtungen und
   verschiebt das Verhaeltnis deshalb kaum. Was wirkt, ist die Kronenbreite.

   Schranke jetzt relativ: hoechstens 65 % des Eichenwerts aus DEMSELBEN Lauf.
   Eine neue Konstante haette bei der naechsten Bauform dasselbe Schicksal.
   LEAF_CLUSTER_PRESET bleibt trotzdem eine eigene Tabelle - Card-Grobheit und
   Polster-Abstand sind zwei verschiedene Fragen.

   "Bbox-Mitte == 0" ist raus. Nachgemessen ueber drei Presets und drei Seeds:
   Stammachse 0.0000 m in allen neun Faellen, Bbox-Exzentrik 0.08-0.39 m mit
   dem Seed streuend, 3-13 % des Kronenradius. Eine Verschiebung traefe den
   Stamm mit und waere ueber die Seeds konstant - es ist Kronen-Schieflage.
   An seine Stelle tritt der Bounds-Radius, an dem das Culling haengt, geprueft
   gegen die Blender-Messung statt gegen eine Konstante.

27 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 12:22:35 +02:00
co-authored by Claude Opus 4.8
parent 5e6b84369f
commit 6ed2257085
9 changed files with 371 additions and 50 deletions
+199 -19
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 45, 0),
"version": (1, 46, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -156,8 +156,11 @@ PRESETS = {
},
# --- Varianten ueber die Kronenform (Idee: modular_tree "envelope shapes") ---
"tanne": { # kegelfoermig: unten lange Aeste, oben kurz, leicht haengend
"Height": 7.0, "Trunk Radius": 0.15, "Bend": 0.15, "Taper": 0.95,
"Branch Count": 22, "Branch Length": 1.9, "Branch Up": -0.25,
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
# Jungwald. Proportional skaliert - Hoehe, Astlaenge und Stammradius
# zusammen, sonst wird aus dem Baum eine Stange mit Minikrone.
"Height": 13.0, "Trunk Radius": 0.28, "Bend": 0.15, "Taper": 0.95,
"Branch Count": 22, "Branch Length": 3.55, "Branch Up": -0.25,
"Branch Start": 0.12, "Branch End": 0.97,
"Crown Taper": 0.9, "Crown Bulge": 0.0,
"Attraction Up": 0.0, # Tannenaeste haengen, streben nicht hoch
@@ -165,8 +168,11 @@ PRESETS = {
"Sub Length": 0.4, "Root Flare": 0.7,
},
"eiche": { # runde, breite Krone: Bauch in der Mitte, dicker Stamm
"Height": 5.5, "Trunk Radius": 0.24, "Bend": 0.5, "Taper": 0.75,
"Branch Count": 10, "Branch Length": 2.2, "Branch Up": 0.9,
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
# Jungwald. Proportional skaliert - Hoehe, Astlaenge und Stammradius
# zusammen, sonst wird aus dem Baum eine Stange mit Minikrone.
"Height": 7.9, "Trunk Radius": 0.34, "Bend": 0.5, "Taper": 0.75,
"Branch Count": 10, "Branch Length": 3.15, "Branch Up": 0.9,
"Branch Start": 0.3, "Branch End": 0.95,
"Crown Taper": -0.15, "Crown Bulge": 0.5,
"Attraction Up": 0.5, # Eiche: Aeste schwingen deutlich hoch
@@ -211,7 +217,7 @@ PRESETS = {
"Branch Count": 9, # offene Krone: wenige Leitaeste, Licht durch
# Kurze Aeste an einem HOHEN Stamm. Vorher genau umgekehrt (Height 3.8,
# Branch Length 2.55) - daraus wurde ein Trichter, siehe unten.
"Branch Length": 1.25, "Branch Up": 0.7,
"Branch Length": 1.55, "Branch Up": 0.7,
# Kronenansatz bei 30 % statt 40 %: das ist der Weg zur KUGEL, ohne den
# Baum hoeher zu machen (4.9 m haengen an der Ernte-Reichweite). Die
# Krone wird dadurch 4.3 m hoch bei 4.5 m Breite - Kugelverhaeltnis
@@ -244,8 +250,11 @@ PRESETS = {
# sind es 26 % bei einem Fall von 0.033 x Hoehe - vergleichbar der Tanne
# (0.030), weit weg von der Weide (0.213). Zweige haengen, der Baum nicht.
"birke": {
"Height": 7.5, "Trunk Radius": 0.09, "Bend": 0.7, "Taper": 0.92,
"Branch Count": 16, "Branch Length": 2.1, "Branch Up": 0.55,
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
# Jungwald. Proportional skaliert - Hoehe, Astlaenge und Stammradius
# zusammen, sonst wird aus dem Baum eine Stange mit Minikrone.
"Height": 11.0, "Trunk Radius": 0.13, "Bend": 0.7, "Taper": 0.92,
"Branch Count": 16, "Branch Length": 3.05, "Branch Up": 0.55,
"Branch Start": 0.38, "Branch End": 0.98,
"Crown Taper": 0.1, "Crown Bulge": 0.25,
"Attraction Up": 0.15, # spreizen, nicht hochschwingen
@@ -255,8 +264,11 @@ PRESETS = {
"Sub Length": 0.7, "Root Flare": 0.6,
},
"weide": { # haengende Zweige (Droop positiv), breit ausladend
"Height": 5.0, "Trunk Radius": 0.18, "Bend": 0.8, "Taper": 0.8,
"Branch Count": 12, "Branch Length": 2.0, "Branch Up": 0.5,
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
# Jungwald. Proportional skaliert - Hoehe, Astlaenge und Stammradius
# zusammen, sonst wird aus dem Baum eine Stange mit Minikrone.
"Height": 7.0, "Trunk Radius": 0.25, "Bend": 0.8, "Taper": 0.8,
"Branch Count": 12, "Branch Length": 2.8, "Branch Up": 0.5,
"Branch Start": 0.35, "Branch End": 0.95,
"Crown Taper": -0.2, "Crown Bulge": 0.4,
"Attraction Up": -0.25, # Weide: Zweige haengen
@@ -1354,7 +1366,12 @@ LEAF_GROUP_NAME = "GN_TreeLeaves"
# hingehoeren. Mit Bueschel-Atlanten liegen die Groessenordnungen bei
# Size ~0.7 und Density ~90.
LEAF_DEFAULTS = {
"Density": 170.0, # Punkte pro m2 Zweigoberflaeche (S4-Wert)
# Punkte pro m2 Zweigoberflaeche. Seit der Bueschel-Schale sind das
# CLUSTER-Zentren, nicht mehr einzelne Cards - jedes Zentrum traegt SECHS
# Cards auf seiner Halbkugel. Die Dichte liegt entsprechend niedriger; die
# Bisektion regelt sie ohnehin auf die Ziel-Cardzahl ein.
"Density": 30.0,
"Cluster Radius": 0.45,
# DER Volumen-Hebel. Bei 0.02 trugen nur die aeussersten 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. Am fertigen Baum in
@@ -1417,6 +1434,10 @@ def build_leaf_group(rebuild=False):
add_in("Size Var", 'NodeSocketFloat', LEAF_DEFAULTS["Size Var"], 0.0, 1.0)
add_in("Tilt", 'NodeSocketFloat', LEAF_DEFAULTS["Tilt"], 0.0, 3.14159)
add_in("Seed", 'NodeSocketInt', LEAF_DEFAULTS["Seed"], 0, 9999)
# Radius der Bueschel-Kappe um jedes Cluster-Zentrum. Siehe den Block
# "BUESCHEL-SCHALE" weiter unten.
add_in("Cluster Radius", 'NodeSocketFloat',
LEAF_DEFAULTS["Cluster Radius"], 0.02, 5.0)
gin = N("NodeGroupInput"); gin.location = (-900, 0)
gout = N("NodeGroupOutput"); gout.location = (900, 0)
@@ -1474,9 +1495,31 @@ def build_leaf_group(rebuild=False):
hpos = N("GeometryNodeInputPosition"); hpos.location = (-560, -220)
hsep = N("ShaderNodeSeparateXYZ"); hsep.location = (-420, -220)
L(hpos.outputs[0], hsep.inputs[0])
# Der Filter darf HAENGENDE Astspitzen nicht wegschneiden. Ein Ast der Tanne
# setzt bei 12 % der Hoehe an und haengt; seine Spitze faellt unter den
# Kronenansatz und flog damit aus der Auswahl - gemessen blieben 4 von 22
# Aesten ohne Card an der Spitze, bei der Weide 3 von 12. Genau die
# nackten Ast-Enden, die die Spielseite gemeldet hat.
#
# Der Baum traegt den ANSATZPUNKT jedes Astpunkts als Attribut "anchor".
# Ein Punkt zaehlt, wenn ENTWEDER er selbst oder der Ansatz seines Astes
# ueber der Schwelle liegt. Beides braucht es: nur ueber den Ansatz zu
# gehen wuerde beim Setzling das Stamm-Laub ausschliessen (der Stamm hat
# keinen Ansatz und laege bei 0).
hank = N("GeometryNodeInputNamedAttribute"); hank.location = (-560, -300)
hank.data_type = 'FLOAT_VECTOR'
_sock(hank, "Name").default_value = "anchor"
hasep = N("ShaderNodeSeparateXYZ"); hasep.location = (-420, -300)
L(_out(hank, "Attribute"), hasep.inputs[0])
hmax = N("ShaderNodeMath"); hmax.location = (-280, -260)
hmax.operation = 'MAXIMUM'
L(hsep.outputs["Z"], hmax.inputs[0])
L(hasep.outputs["Z"], hmax.inputs[1])
hcmp = N("FunctionNodeCompare"); hcmp.location = (-140, -220)
hcmp.data_type = 'FLOAT'; hcmp.operation = 'GREATER_THAN'
L(hsep.outputs["Z"], hcmp.inputs[0])
L(hmax.outputs[0], hcmp.inputs[0])
L(bschwelle.outputs[0], hcmp.inputs[1])
beide = N("FunctionNodeBooleanMath"); beide.location = (0, 20)
@@ -1484,10 +1527,45 @@ def build_leaf_group(rebuild=False):
L(cmp.outputs[0], beide.inputs[0])
L(hcmp.outputs[0], beide.inputs[1])
# DICHTE ZUR SPITZE HIN GEWICHTEN.
#
# "Distribute Points on Faces" verteilt nach FLAECHE. Eine Astspitze ist
# duenn und hat entsprechend wenig davon - sie bekommt also am seltensten
# ein Cluster, und genau das hat die Spielseite gemeldet: nackte Ast-Enden
# mit einem Mini-Bueschel daran. Gemessen als Abstand Astspitze zur
# naechsten Card, relativ zur Astlaenge: Tanne 48 % im Mittel, im
# schlimmsten Fall 123 % - dort sass gar nichts mehr am Ende.
#
# Der Astradius liegt als "uv_r" am Mesh. Duenn bekommt volle Dichte, dick
# nur einen Bruchteil - damit wandern die Cluster nach aussen, wo das Laub
# hingehoert.
wmap = N("ShaderNodeMapRange"); wmap.location = (-460, 300)
wmap.clamp = True
L(_out(na, "Attribute"), wmap.inputs["Value"])
# Und der Bezug ist ein BRUCHTEIL von Max Radius, nicht der ganze Bereich:
# oberhalb eines Viertels der Schwelle ist der Zweig fuer ein Polster schon
# zu dick.
wmap.inputs["From Min"].default_value = 0.0
wviertel = N("ShaderNodeMath"); wviertel.location = (-600, 300)
wviertel.operation = 'MULTIPLY'; wviertel.inputs[1].default_value = 0.25
L(V["Max Radius"], wviertel.inputs[0])
L(wviertel.outputs[0], wmap.inputs["From Max"])
# Der Kontrast muss KRAEFTIG sein. Mit 1.0 -> 0.2 war die Gewichtung
# wirkungslos: die Zweigradien liegen bei 0.001-0.003, Max Radius bei 0.04,
# also bekamen praktisch alle waehlbaren Punkte fast dasselbe Gewicht -
# und die Flaeche entschied weiter. Mit 1.0 -> 0.02 zieht es die Cluster
# wirklich nach aussen.
wmap.inputs["To Min"].default_value = 1.0
wmap.inputs["To Max"].default_value = 0.02
wmul = N("ShaderNodeMath"); wmul.location = (-320, 300)
wmul.operation = 'MULTIPLY'
L(V["Density"], wmul.inputs[0])
L(wmap.outputs["Result"], wmul.inputs[1])
dist = N("GeometryNodeDistributePointsOnFaces"); dist.location = (-320, 200)
L(_out(tree, "Geometry"), _sock(dist, "Mesh"))
L(beide.outputs[0], _sock(dist, "Selection"))
L(V["Density"], _sock(dist, "Density"))
L(wmul.outputs[0], _sock(dist, "Density"))
L(V["Seed"], _sock(dist, "Seed"))
# Zufaellige Drehung: voll um Z, leicht gekippt (Tilt)
@@ -1531,8 +1609,59 @@ def build_leaf_group(rebuild=False):
_sock(lstore, "Name").default_value = "ast_id"
L(_out(lna, "Attribute"), _sock(lstore, "Value"))
# ------------------------------------------------------------------
# BUESCHEL-SCHALE: aus jedem gestreuten Punkt wird ein Cluster-ZENTRUM,
# und die Cards sitzen auf der oberen Halbkugel darum herum.
#
# Vorher sassen die Cards direkt auf der Astkurve. Aus der Naehe sah man
# deshalb nackte Ast-Enden mit einem Mini-Bueschel daran - das Laub folgte
# der Linie des Astes statt ein Polster zu bilden. Jetzt bildet jeder
# Cluster eine Kappe, die AUF dem Ast sitzt; die Astspitze verschwindet
# darin, weil das Zentrum auf ihr liegt und die Schale sie ueberdeckt.
#
# Untere Halbkugel weglassen: von unten sieht man die Krone gegen den
# Himmel, dort braucht es keine Cards - und es waere die Haelfte des
# Overdraws fuer nichts.
# Ikosaeder: 12 Vertices, davon 6 auf der oberen Haelfte - also SECHS Cards
# je Cluster. "Subdivisions" 0 und 1 liefern beide das Ikosaeder (gemessen),
# erst 2 unterteilt auf 42. Sechs reicht: mehr Cards je Cluster heisst bei
# festem Budget weniger Cluster, und dann bleiben Aeste ganz ohne Polster.
ico = N("GeometryNodeMeshIcoSphere"); ico.location = (-320, 420)
_sock(ico, "Radius").default_value = 1.0
_sock(ico, "Subdivisions").default_value = 1
spos = N("GeometryNodeInputPosition"); spos.location = (-320, 560)
ssep = N("ShaderNodeSeparateXYZ"); ssep.location = (-180, 560)
L(spos.outputs[0], ssep.inputs[0])
soben = N("FunctionNodeCompare"); soben.location = (-40, 560)
soben.data_type = 'FLOAT'; soben.operation = 'GREATER_THAN'
L(ssep.outputs["Z"], soben.inputs[0])
# Knapp unter den Aequator, damit die Kappe seitlich geschlossen wirkt.
soben.inputs[1].default_value = -0.15
m2p = N("GeometryNodeMeshToPoints"); m2p.location = (-40, 420)
L(_out(ico, "Mesh"), _sock(m2p, "Mesh"))
L(soben.outputs[0], _sock(m2p, "Selection"))
# Cluster-Radius mit Streuung, damit die Polster nicht uniform wirken.
rrad = N("FunctionNodeRandomValue"); rrad.location = (-180, 320)
rrad.data_type = 'FLOAT'
L(V["Seed"], _sock(rrad, "Seed"))
rlo = N("ShaderNodeMath"); rlo.location = (-320, 260); rlo.operation = 'MULTIPLY'
rlo.inputs[1].default_value = 0.7
L(V["Cluster Radius"], rlo.inputs[0])
L(rlo.outputs[0], rrad.inputs[2])
L(V["Cluster Radius"], rrad.inputs[3])
schale = N("GeometryNodeInstanceOnPoints"); schale.location = (120, 420)
L(_out(lstore, "Geometry"), _sock(schale, "Points"))
L(_out(m2p, "Points"), _sock(schale, "Instance"))
L(rrad.outputs[1], _sock(schale, "Scale"))
sreal = N("GeometryNodeRealizeInstances"); sreal.location = (300, 420)
L(_out(schale, "Instances", "Geometry"), _sock(sreal, "Geometry"))
inst = N("GeometryNodeInstanceOnPoints"); inst.location = (100, 200)
L(_out(lstore, "Geometry"), _sock(inst, "Points"))
L(_out(sreal, "Geometry"), _sock(inst, "Points"))
L(_out(quelle, "Output"), _sock(inst, "Instance"))
try:
L(V["Use Collection"], _sock(inst, "Pick Instance"))
@@ -1620,11 +1749,22 @@ LEAF_RADIUS_YOUNG = 0.20
# macht ihn kahl. Faktor 1.5 statt 3 ist der Kompromiss.
#
# Ziel-Cards Groesse Kronenansatz
# Seit der Bueschel-Schale ist die kleinste Einheit ein CLUSTER (rund 26 Cards
# auf einer Halbkugel), nicht mehr die einzelne Card. Ziele unter 26 sind damit
# unerreichbar - S1 stand vorher auf 10 und die Regelung haette nie konvergiert.
# Die Zahlen sind deshalb auf Cluster-Vielfache gelegt: 1 / 3 / 8 / 15.
#
# S4 ist jetzt GEREGELT (vorher None = volle Dichte). Ohne Regelung lieferte die
# Schale das 26-fache der frueheren Cardzahl - gemessen 2706 statt 444 an der
# Eiche. "Volle Dichte" war eine sinnvolle Vorgabe, solange ein Punkt eine Card
# war; jetzt ist sie es nicht mehr.
#
# Ziel-Cards Groesse Kronenansatz
LEAF_STUFEN = (
(10, 0.24, 0.40), # S1 - wenige kleine Bueschel, Fuss bleibt frei
(40, 0.46, 0.35), # S2
(120, 0.75, 0.30), # S3
(None, 1.00, 0.25), # S4 - volle Dichte, keine Regelung
(26, 0.24, 0.40), # S1 - EIN Bueschel, Fuss bleibt frei
(78, 0.46, 0.35), # S2 - drei
(208, 0.75, 0.30), # S3 - acht
(390, 1.00, 0.25), # S4 - rund fuenfzehn
)
# AUSNAHMEN je Preset: {preset: {stufen_index: ziel_cards}}.
@@ -1639,6 +1779,10 @@ LEAF_STUFEN = (
# Budget fuer alle anzuheben: die Weide ist der Ausreisser, nicht die Regel.
LEAF_STUFEN_PRESET = {
"weide": {3: 350},
# Die Birke hat mit 2100 Tris die dickste Rinde im Satz; bei 390 Cards
# gemessen 4572 Tris. (4300 - 2100) / 6 = 366 Cards passen, 340 mit Luft
# fuer die 15-%-Toleranz der Regelung.
"birke": {3: 340},
# Obstbaum: breite offene Krone auf kurzem Stamm gibt viel Zweigflaeche auf
# wenig Hoehe - ungeregelt 4304-4400 Tris ueber drei Seeds, knapp ueber dem
# Budget. Die Regelung haelt eine Toleranz von 15 %, deshalb liegt das Ziel
@@ -1679,8 +1823,28 @@ LEAF_STUFEN_PRESET = {
# eine Overdraw-Entscheidung der Spielseite, keine Generator-Frage.
LEAF_SIZE_PRESET = {
"obstbaum": 1.47,
# Die vier vergroesserten Arten: die Card muss mit dem Baum wachsen, sonst
# wird das Laub relativ zum Baum feiner und die Krone duenn - dieselbe
# Falle wie beim Setzling, nur andersherum. Faktoren = Skalierung des Baums.
"eiche": 1.43,
"birke": 1.46,
"tanne": 1.86,
"weide": 1.40,
}
# Radius der Bueschel-Kappe je Preset, als Faktor.
#
# EIGENE Tabelle, nicht an LEAF_SIZE_PRESET gehaengt. Der erste Anlauf tat
# genau das - und hat die abgenommene Obstbaum-Kugel wieder plattgedrueckt: mit
# Faktor 1.47 wuchs auch die Kappe um 47 %, und weil sie in ALLE Richtungen
# waechst, legt sie mehr Breite als Hoehe zu. Gemessen fiel der Kugel-Kennwert
# von 1.35-1.47 auf 0.93-1.14 und die Bbox lag 14 cm ausser Mitte.
#
# Card-Groesse und Kappen-Radius sind zwei verschiedene Fragen: die erste
# entscheidet, wie grob das Laub ist, die zweite, wie weit das Polster vom Ast
# absteht.
LEAF_CLUSTER_PRESET = {}
def _stufen_anteile(growth_t, preset=None):
"""Ziel-Cardzahl, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt.
@@ -1862,7 +2026,23 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None,
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
# Der Kronenansatz-Filter darf NIE ueber dem echten Kronenansatz der Art
# liegen. Die Stufen-Tabelle nennt 0.25 der Baumhoehe - die Tanne setzt
# ihre Aeste aber ab 12 % an und laesst sie haengen. Gemessen lag bei
# 5 von 22 Tannenaesten das aeussere Fuenftel KOMPLETT unter der
# Schwelle; sie bekamen gar kein Laub und standen als nackte Aeste da.
#
# "Branch Start" ist ein Anteil der STAMMlaenge, die Schwelle einer der
# Gesamthoehe. Die Stammlaenge ist kleiner, der Wert liegt also ohnehin
# etwas zu hoch - die 0.8 geben zusaetzlich Luft.
ast_start = PRESETS.get(tree_obj.get("preset") or "", {}).get(
"Branch Start", DEFAULTS["Branch Start"])
overrides["Min Height"] = min(h_anteil, ast_start * 0.8)
# Die Bueschel-Kappe muss mitwachsen. Ein Setzling mit Altbaum-Kappe
# verschwindet darin - dasselbe Laubpaket-Problem wie bei der
# Card-Groesse, nur eine Ebene hoeher.
c_faktor = LEAF_CLUSTER_PRESET.get(tree_obj.get("preset") or "", 1.0)
overrides["Cluster Radius"] = basis["Cluster Radius"] * g_anteil * c_faktor
name = name or (tree_obj.name + "_Leaf")
ob = bpy.data.objects.get(name)