Tree Gen 1.30.0: Bueschel-Defaults, Wachstums-Kopplung, Stufen-Budget

Blatt-Defaults auf die Bueschel-Atlas-Aera umgestellt. Eine Card ist ein ganzer
Zweigbueschel, kein einzelnes Blatt - die alten Werte streuten hunderte winziger
Cards, wo ein paar Dutzend grosse hingehoeren. Werte an einer S4-Eiche in UE
gemessen und abgenommen:

    Groesse      0.22 -> 0.85
    Dichte        500 -> 170
    Max Radius   0.02 -> 0.04     <- der Volumen-Hebel
    Kippung       0.5 -> 0.9

Max Radius ist der unscheinbare Held: bei 0.02 trugen nur die aeussersten
Zweigspitzen Laub, die Krone hatte nur Kontur und war innen nackt.

Bueschelgroesse haengt jetzt am Wachstum: make_object merkt growth_t am Objekt
(aus dem Namen zu lesen waere bruechig), der Blattwerk-Operator skaliert die
Cards damit. Gemessen ueber vier Stufen: Card-Kante 0.19/0.28/0.37/0.45.

Falle dabei: "Dichte" ist Punkte pro m2 Zweigflaeche, und ein Setzling hat davon
fast keine - bei Stufe 1 kamen exakt NULL Cards heraus, der Setzling stand kahl
da. Da die Bueschel um denselben Faktor kleiner sind, deckt jeder nur faktor^2
der Flaeche; die Dichte gegenlaeufig anzuheben haelt die Blattdeckung gleich.

Tri-Budget PRO STUFE (ASSETS.md Teil 6): S1 <= 800, S2 <= 1200, S3/S4 <= 4000.
Bewusst Stufen und keine Interpolation; die Schwellen greifen auch bei drei
Stufen richtig (t = 0 / 0.5 / 1.0). Gemessen 494 / 878 / 1032 / 1396 - alle im
Rahmen.

WICHTIGSTE FALLE, von der UE-Seite gemeldet und hier nachgestellt:
md[socket] = wert loest KEINE Neuauswertung des GN-Modifiers aus. Die
Blattebene blieb bei 118 Faces stehen, obwohl die Dichte von 90 auf 400
geschrieben war - auch view_layer.update() half nicht, erst ein
show_viewport-Toggle. Der Knopf-Weg funktionierte bisher nur ZUFAELLIG, weil
make_leaves jedes Mal auch die Objekt-Zeiger neu schreibt und das Aendern eines
ID-Zeigers die Depsgraph-Beziehungen umbaut. Wer das spaeter optimiert ("nur
geaenderte Werte schreiben"), haette den Fehler still zurueck.
_modifier_anstossen() macht es jetzt explizit - in make_leaves UND make_object.

Nebenbei: der Variantentest rundete Card-Flaechen auf 5 Stellen; seit die
Kippung bei 0.9 steht streuen sie in der sechsten und eine einzige Card-Sorte
zerfiel in vier Eimer.

Neu: tests/test_blatt_bueschel.py. 23 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 08:23:27 +02:00
co-authored by Claude Opus 4.8
parent 914a1af967
commit b1fdad8106
9 changed files with 387 additions and 26 deletions
+110 -17
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 29, 0),
"version": (1, 30, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -1171,6 +1171,12 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None):
for k, v in values.items():
if k in ids:
md[ids[k]] = v
_modifier_anstossen(md)
# Wachstums-Fortschritt am Objekt merken. Der Blattwerk-Operator laeuft
# spaeter und getrennt - er kann sonst nicht wissen, ob er einen Setzling
# oder einen Altbaum vor sich hat. Aus dem Namen zu lesen waere bruechig.
ob["growth_t"] = 1.0 if growth is None else max(0.0, min(1.0, float(growth)))
bpy.context.view_layer.objects.active = ob
return ob
@@ -1183,15 +1189,21 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None):
LEAF_GROUP_NAME = "GN_TreeLeaves"
# Eine Card ist ein ganzer ZWEIGBUESCHEL, kein einzelnes Blaettchen. Die alten
# Werte (Size 0.22, Density 300-500) stammen aus der Blaettchen-Aera: damit
# streute der Generator hunderte winziger Cards, wo ein paar Dutzend grosse
# hingehoeren. Mit Bueschel-Atlanten liegen die Groessenordnungen bei
# Size ~0.7 und Density ~90.
LEAF_DEFAULTS = {
"Density": 300.0, # Punkte pro m2 Zweigoberflaeche. Gemessen mit der
# 6-Tri-Diamond-Card: 300 -> ~180 Cards / 1092 Tris,
# zusammen mit dem Baum 2228 von 2500 Tris. 900 waere
# mit 4118 Tris deutlich ueber Budget.
"Max Radius": 0.02, # nur wo der Ast duenner ist als das (m) -> Zweige
"Size": 0.35,
"Density": 170.0, # Punkte pro m2 Zweigoberflaeche
# 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
# UE gegengeprueft, nicht geschaetzt.
"Max Radius": 0.04,
"Size": 0.85,
"Size Var": 0.35,
"Tilt": 0.5, # wie stark die Cards aus der Senkrechten kippen (rad)
"Tilt": 0.9, # mehr Rotations-Chaos schliesst Sichtluecken
"Seed": 0,
"Use Collection": False,
}
@@ -1369,6 +1381,34 @@ def leaf_socket_ids(ng):
if getattr(so, "in_out", "") == 'INPUT'}
def _stufen_budget(s, growth_t):
"""Tri-Budget fuer einen Baum nach seiner Wachstums-Stufe."""
if growth_t < 0.15:
return s.leaf_budget_s1
if growth_t <= 0.55:
return s.leaf_budget_s2
return s.leaf_budget
def _modifier_anstossen(md):
"""Erzwingt die Neuauswertung nach direktem Schreiben von Socket-Werten.
FALLE, die von der UE-Seite gemeldet und hier nachgestellt wurde: `md[socket]
= wert` loest KEINE Neuauswertung aus. Gemessen blieb die Blattebene bei 118
Faces stehen, obwohl die Dichte von 90 auf 400 geschrieben war - auch
`view_layer.update()` half nicht. Erst ein show_viewport-Toggle brachte das
Ergebnis.
Der Knopf-Weg funktionierte bisher nur ZUFAELLIG: make_leaves schreibt jedes
Mal auch die Objekt-Zeiger neu, und das Aendern eines ID-Zeigers baut die
Depsgraph-Beziehungen um, was den Cache verwirft. Wer das spaeter optimiert
("nur geaenderte Werte schreiben"), haette den Fehler still zurueck.
"""
sichtbar = md.show_viewport
md.show_viewport = not sichtbar
md.show_viewport = sichtbar
def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overrides):
"""Erzeugt/aktualisiert ein <Baum>_Leaf-Objekt mit dem Blattwerk-Modifier."""
name = name or (tree_obj.name + "_Leaf")
@@ -1396,6 +1436,7 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overri
for k, v in vals.items():
if k in ids:
md[ids[k]] = v
_modifier_anstossen(md)
return ob
@@ -1820,23 +1861,50 @@ class TreeGenSettings(PropertyGroup):
"damit mischt die Krone 4-6 Varianten statt einer"),
)
leaf_density: FloatProperty(
name="Dichte", default=150.0, min=0.0, max=20000.0,
name="Dichte", default=170.0, min=0.0, max=20000.0,
description=("Cards pro m2 Zweigoberflaeche. Presets mit viel Geaest (Eiche) "
"bekommen dadurch automatisch mehr Blaetter als schlanke. 150 haelt "
"auch die Eiche im 2500-Tri-Budget; der Button meldet die Summe"),
)
leaf_budget_s1: IntProperty(
name="Budget S1", default=800, min=100, max=20000,
description="Tri-Budget der Setzlings-Stufe (ASSETS.md Teil 6)",
)
leaf_budget_s2: IntProperty(
name="Budget S2", default=1200, min=100, max=20000,
description="Tri-Budget der zweiten Stufe (ASSETS.md Teil 6)",
)
leaf_budget: IntProperty(
name="Tri-Budget", default=2500, min=0, max=100000,
description="Bei Ueberschreitung warnt der Blattwerk-Button (0 = keine Pruefung)",
name="Budget S3/S4", default=4000, min=0, max=100000,
description=("Tri-Budget der ausgewachsenen Stufe. Die urspruenglichen "
"2500 waren eine konservative Setzung - bei ISM-Instanzen "
"mit LOD-Kette sind Roh-Dreiecke nicht der Engpass, "
"sondern Overdraw und Schatten der Cards. Bei "
"Ueberschreitung warnt der Blattwerk-Button (0 = aus)"),
)
leaf_size: FloatProperty(
name="Groesse", default=0.85, min=0.01, max=5.0,
description=("Kantenlaenge einer Card. Eine Card ist ein ganzer "
"Zweigbueschel, kein einzelnes Blatt - deshalb 0.7 und "
"nicht 0.2"),
)
leaf_size_growth: FloatProperty(
name="Setzlings-Anteil", default=0.45, min=0.05, max=1.0,
description=("Wie gross die Bueschel auf der KLEINSTEN Wachstumsstufe "
"sind, als Anteil der Endgroesse. 1.0 = keine Skalierung. "
"Ein Setzling traegt kleinere Bueschel als der Altbaum"),
)
leaf_size: FloatProperty(name="Groesse", default=0.22, min=0.01, max=5.0)
leaf_size_var: FloatProperty(name="Groessen-Streuung", default=0.35, min=0.0, max=1.0)
leaf_max_radius: FloatProperty(
name="Nur Zweige duenner als", default=0.02, min=0.001, max=0.5,
name="Nur Zweige duenner als", default=0.04, min=0.001, max=0.5,
description=("Blaetter nur dort, wo der Ast duenner als dieser Radius ist (m). "
"Verhindert Blaetter am Stamm"),
)
leaf_tilt: FloatProperty(name="Kippung", default=0.5, min=0.0, max=3.14159)
leaf_tilt: FloatProperty(
name="Kippung", default=0.9, min=0.0, max=3.14159,
description=("Wie stark die Cards aus der Senkrechten kippen. Mehr "
"Rotations-Chaos schliesst Sichtluecken in der Krone"),
)
make_frucht: BoolProperty(
name="_Frucht-Ebene anlegen", default=False,
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
@@ -2350,9 +2418,23 @@ class TREEGEN_OT_leaves(Operator):
total_tree = total_leaf = 0
for t in trees:
try:
# Bueschelgroesse mit dem Wachstum skalieren: ein Setzling
# traegt kleinere Bueschel. Baeume ohne Wachstums-Stufe haben
# growth_t = 1.0 und bleiben unveraendert.
gt = float(t.get("growth_t", 1.0))
faktor = s.leaf_size_growth + (1.0 - s.leaf_size_growth) * gt
# Dichte GEGENLAEUFIG mitziehen. "Dichte" ist Punkte pro m2
# Zweigoberflaeche, und ein Setzling hat davon fast keine -
# gemessen kam bei Stufe 1 exakt NULL Cards heraus, der
# Setzling stand kahl da. Da die Bueschel um `faktor` kleiner
# sind, deckt jeder nur faktor^2 der Flaeche; die Dichte
# entsprechend anzuheben haelt die Blattdeckung ueber alle
# Stufen gleich, statt den Setzling auszuduennen.
dichte = s.leaf_density / max(faktor * faktor, 1e-3)
lf = make_leaves(context, t, s.leaf_card,
card_coll=s.leaf_cards,
Density=s.leaf_density, Size=s.leaf_size,
Density=dichte,
Size=s.leaf_size * faktor,
**{"Size Var": s.leaf_size_var,
"Max Radius": s.leaf_max_radius,
"Tilt": s.leaf_tilt, "Seed": s.seed})
@@ -2374,7 +2456,14 @@ class TREEGEN_OT_leaves(Operator):
total = total_tree + total_leaf
msg = ("Blattwerk fuer %d Baum/Baeume: %d Stamm + %d Blatt = %d Tris"
% (len(trees), total_tree, total_leaf, total))
limit = s.leaf_budget * max(len(trees), 1)
# Budget PRO STUFE statt global (ASSETS.md Teil 6): S1 <= 800,
# S2 <= 1200, S3/S4 <= 4000. Bewusst STUFEN und keine Interpolation -
# so steht es in den Unterlagen, und ein Setzling steht nie im
# Vordergrund. Die Schwellen liegen so, dass sie auch bei 3 Stufen
# (t = 0 / 0.5 / 1.0) noch richtig greifen.
limit = 0
for t in trees:
limit += _stufen_budget(s, float(t.get("growth_t", 1.0)))
if total_leaf == 0:
# Passiert regulaer beim Kaktus (kein Ast duenner als Max Radius) -
# aber auch, wenn Max Radius fuer den Baum zu klein steht. Beides
@@ -2570,10 +2659,14 @@ class VIEW3D_PT_tree_leaves(_TreeSub):
col.active = s.leaf_card is not None or s.leaf_cards is not None
col.prop(s, "leaf_density")
col.prop(s, "leaf_size")
col.prop(s, "leaf_size_growth")
col.prop(s, "leaf_size_var")
col.prop(s, "leaf_max_radius")
col.prop(s, "leaf_tilt")
col.prop(s, "leaf_budget")
row = col.row(align=True)
row.prop(s, "leaf_budget_s1", text="S1")
row.prop(s, "leaf_budget_s2", text="S2")
row.prop(s, "leaf_budget", text="S3/S4")
layout.separator()
layout.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD')
if s.leaf_cards is not None: