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
+46
View File
@@ -385,6 +385,51 @@ Zwei Fallen beim Bauen:
- `join` loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach - `join` loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach
tot. Die Aufraeumliste fuehrt deshalb **Namen** statt Objekte. tot. Die Aufraeumliste fuehrt deshalb **Namen** statt Objekte.
### Blatt-Defaults: Bueschel, nicht Blaettchen
Eine Card ist ein ganzer **Zweigbueschel**, kein einzelnes Blatt. Die alten Werte
stammten aus der Blaettchen-Aera und streuten hunderte winziger Cards, wo ein
paar Dutzend grosse hingehoeren. Die neuen Werte sind an einer S4-Eiche in UE
gemessen und abgenommen:
| | vorher | jetzt | Wirkung |
|---|---|---|---|
| Groesse | 0.22 | **0.85** | Bueschel statt Bonsai |
| Dichte | 500 | **170** | wenige grosse statt viele Fussel |
| **Max Radius** | 0.02 | **0.04** | der Volumen-Hebel |
| Kippung | 0.5 | **0.9** | Rotations-Chaos schliesst Sichtluecken |
`Max Radius` ist der unscheinbare Held: 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.
**Bueschelgroesse haengt am Wachstum.** Der Generator merkt sich `growth_t` am
Objekt; der Blattwerk-Operator skaliert die Cards damit (`Setzlings-Anteil`,
Standard 45 %). Gemessen ueber vier Stufen: Card-Kante 0.19 → 0.28 → 0.37 → 0.45.
> **Falle dabei:** „Dichte" ist Punkte pro m² 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² der Flaeche; die Dichte entsprechend gegenlaeufig anzuheben haelt
> die Blattdeckung ueber alle Stufen gleich.
**Tri-Budget pro Stufe** (ASSETS.md Teil 6) statt global: 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). Die 2500 von frueher waren eine
konservative Setzung — bei ISM-Instanzen mit LOD-Kette sind Roh-Dreiecke nicht
der Engpass, sondern **Overdraw** der Cards (die GPU zahlt pro Bildschirmflaeche,
nicht pro Dreieck) und die Schattenpaesse. Genau deshalb sind 170 grosse Bueschel
guenstiger als 500 kleine, bei gleichem Tri-Zaehler.
> **Die wichtigste Code-Falle in diesem Addon.** `md[socket] = wert` loest
> **keine** Neuauswertung des Geometry-Nodes-Modifiers aus. Nachgestellt: 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 brachte das Ergebnis. 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.
> `_modifier_anstossen()` macht das jetzt explizit.
### LOD-Kette (Stand-Baum) ### LOD-Kette (Stand-Baum)
Haken `LOD-Kette erzeugen`: exportiert `<Name>_LODGroup.fbx` mit `_LOD0..3` unter Haken `LOD-Kette erzeugen`: exportiert `<Name>_LODGroup.fbx` mit `_LOD0..3` unter
@@ -699,6 +744,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`.
| Datei | prueft | | Datei | prueft |
|---|---| |---|---|
| `tests/test_baum_export.py` | Stand-Baum: ein Mesh, zwei Slots, Ursprung, **Kugel-Normalen im exportierten FBX** | | `tests/test_baum_export.py` | Stand-Baum: ein Mesh, zwei Slots, Ursprung, **Kugel-Normalen im exportierten FBX** |
| `tests/test_blatt_bueschel.py` | Bueschel-Defaults, Wachstums-Kopplung der Card-Groesse, Stufen-Budget 800/1200/4000 |
| `tests/test_baum_lod.py` | LOD-Kette: 4 Stufen unter LodGroup, Cards werden nach oben duenner, **Kugel-Normalen auf jeder Stufe**, kein Hoehenversatz | | `tests/test_baum_lod.py` | LOD-Kette: 4 Stufen unter LodGroup, Cards werden nach oben duenner, **Kugel-Normalen auf jeder Stufe**, kein Hoehenversatz |
| `tests/test_faell_teile.py` | Faell-Teile: **Blatt-Bilanz 170=170**, Pivot am Massenmittelpunkt, UCX je Teil, Gesamtdatei mit gemeinsamem Ursprung | | `tests/test_faell_teile.py` | Faell-Teile: **Blatt-Bilanz 170=170**, Pivot am Massenmittelpunkt, UCX je Teil, Gesamtdatei mit gemeinsamem Ursprung |
| `tests/test_blatt_varianten.py` | Card-Sammlung: alle Varianten werden gezogen, seed-stabil, Einzelobjekt weiter moeglich | | `tests/test_blatt_varianten.py` | Card-Sammlung: alle Varianten werden gezogen, seed-stabil, Einzelobjekt weiter moeglich |
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "stylized_tree_generator" id = "stylized_tree_generator"
version = "1.29.0" version = "1.30.0"
name = "Stylized Tree Generator" name = "Stylized Tree Generator"
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen" tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
maintainer = "D4rkst3r" maintainer = "D4rkst3r"
+3 -3
View File
@@ -50,17 +50,17 @@
<td>34.1KB</td> <td>34.1KB</td>
</tr> </tr>
<tr> <tr>
<td><tt><a href="./stylized_tree_generator-1.29.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.29.0</a></tt></td> <td><tt><a href="./stylized_tree_generator-1.30.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.30.0</a></tt></td>
<td>Stylized Tree Generator</td> <td>Stylized Tree Generator</td>
<td>Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen</td> <td>Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen</td>
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td> <td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
<td>4.2.0 - ~</td> <td>4.2.0 - ~</td>
<td>all</td> <td>all</td>
<td>all</td> <td>all</td>
<td>34.4KB</td> <td>36.0KB</td>
</tr> </tr>
</table> </table>
<center><p>Built 2026-08-06, 05:55</p></center> <center><p>Built 2026-08-06, 06:22</p></center>
</body> </body>
</html> </html>
+4 -4
View File
@@ -82,7 +82,7 @@
"id": "stylized_tree_generator", "id": "stylized_tree_generator",
"name": "Stylized Tree Generator", "name": "Stylized Tree Generator",
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
"version": "1.29.0", "version": "1.30.0",
"type": "add-on", "type": "add-on",
"maintainer": "D4rkst3r", "maintainer": "D4rkst3r",
"license": [ "license": [
@@ -98,9 +98,9 @@
"Mesh", "Mesh",
"Modeling" "Modeling"
], ],
"archive_url": "./stylized_tree_generator-1.29.0.zip", "archive_url": "./stylized_tree_generator-1.30.0.zip",
"archive_size": 35207, "archive_size": 36830,
"archive_hash": "sha256:faa1e629b1ee4cfd465db79246a6d4acfae12af61f8b65aafcabcc64ffdcde72" "archive_hash": "sha256:d54440c3d577440ff97b0415ef101801b0b7bf50b6df05e3645cc3d1e1eb15e5"
} }
] ]
} }
Binary file not shown.
Binary file not shown.
+110 -17
View File
@@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "Stylized Tree Generator", "name": "Stylized Tree Generator",
"author": "D4rkst3r", "author": "D4rkst3r",
"version": (1, 29, 0), "version": (1, 30, 0),
"blender": (4, 2, 0), "blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen", "location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", "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(): for k, v in values.items():
if k in ids: if k in ids:
md[ids[k]] = v 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 bpy.context.view_layer.objects.active = ob
return ob return ob
@@ -1183,15 +1189,21 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None):
LEAF_GROUP_NAME = "GN_TreeLeaves" 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 = { LEAF_DEFAULTS = {
"Density": 300.0, # Punkte pro m2 Zweigoberflaeche. Gemessen mit der "Density": 170.0, # Punkte pro m2 Zweigoberflaeche
# 6-Tri-Diamond-Card: 300 -> ~180 Cards / 1092 Tris, # DER Volumen-Hebel. Bei 0.02 trugen nur die aeussersten Zweigspitzen Laub -
# zusammen mit dem Baum 2228 von 2500 Tris. 900 waere # die Krone hatte nur Kontur und war innen nackt. Bei 0.04 wachsen Bueschel
# mit 4118 Tris deutlich ueber Budget. # auch am mittleren Geaest und die Krone bekommt Tiefe. Am fertigen Baum in
"Max Radius": 0.02, # nur wo der Ast duenner ist als das (m) -> Zweige # UE gegengeprueft, nicht geschaetzt.
"Size": 0.35, "Max Radius": 0.04,
"Size": 0.85,
"Size Var": 0.35, "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, "Seed": 0,
"Use Collection": False, "Use Collection": False,
} }
@@ -1369,6 +1381,34 @@ def leaf_socket_ids(ng):
if getattr(so, "in_out", "") == 'INPUT'} 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): def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overrides):
"""Erzeugt/aktualisiert ein <Baum>_Leaf-Objekt mit dem Blattwerk-Modifier.""" """Erzeugt/aktualisiert ein <Baum>_Leaf-Objekt mit dem Blattwerk-Modifier."""
name = name or (tree_obj.name + "_Leaf") 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(): for k, v in vals.items():
if k in ids: if k in ids:
md[ids[k]] = v md[ids[k]] = v
_modifier_anstossen(md)
return ob return ob
@@ -1820,23 +1861,50 @@ class TreeGenSettings(PropertyGroup):
"damit mischt die Krone 4-6 Varianten statt einer"), "damit mischt die Krone 4-6 Varianten statt einer"),
) )
leaf_density: FloatProperty( 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) " description=("Cards pro m2 Zweigoberflaeche. Presets mit viel Geaest (Eiche) "
"bekommen dadurch automatisch mehr Blaetter als schlanke. 150 haelt " "bekommen dadurch automatisch mehr Blaetter als schlanke. 150 haelt "
"auch die Eiche im 2500-Tri-Budget; der Button meldet die Summe"), "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( leaf_budget: IntProperty(
name="Tri-Budget", default=2500, min=0, max=100000, name="Budget S3/S4", default=4000, min=0, max=100000,
description="Bei Ueberschreitung warnt der Blattwerk-Button (0 = keine Pruefung)", 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_size_var: FloatProperty(name="Groessen-Streuung", default=0.35, min=0.0, max=1.0)
leaf_max_radius: FloatProperty( 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). " description=("Blaetter nur dort, wo der Ast duenner als dieser Radius ist (m). "
"Verhindert Blaetter am Stamm"), "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( make_frucht: BoolProperty(
name="_Frucht-Ebene anlegen", default=False, name="_Frucht-Ebene anlegen", default=False,
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene", description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
@@ -2350,9 +2418,23 @@ class TREEGEN_OT_leaves(Operator):
total_tree = total_leaf = 0 total_tree = total_leaf = 0
for t in trees: for t in trees:
try: 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, lf = make_leaves(context, t, s.leaf_card,
card_coll=s.leaf_cards, 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, **{"Size Var": s.leaf_size_var,
"Max Radius": s.leaf_max_radius, "Max Radius": s.leaf_max_radius,
"Tilt": s.leaf_tilt, "Seed": s.seed}) "Tilt": s.leaf_tilt, "Seed": s.seed})
@@ -2374,7 +2456,14 @@ class TREEGEN_OT_leaves(Operator):
total = total_tree + total_leaf total = total_tree + total_leaf
msg = ("Blattwerk fuer %d Baum/Baeume: %d Stamm + %d Blatt = %d Tris" msg = ("Blattwerk fuer %d Baum/Baeume: %d Stamm + %d Blatt = %d Tris"
% (len(trees), total_tree, total_leaf, total)) % (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: if total_leaf == 0:
# Passiert regulaer beim Kaktus (kein Ast duenner als Max Radius) - # Passiert regulaer beim Kaktus (kein Ast duenner als Max Radius) -
# aber auch, wenn Max Radius fuer den Baum zu klein steht. Beides # 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.active = s.leaf_card is not None or s.leaf_cards is not None
col.prop(s, "leaf_density") col.prop(s, "leaf_density")
col.prop(s, "leaf_size") col.prop(s, "leaf_size")
col.prop(s, "leaf_size_growth")
col.prop(s, "leaf_size_var") col.prop(s, "leaf_size_var")
col.prop(s, "leaf_max_radius") col.prop(s, "leaf_max_radius")
col.prop(s, "leaf_tilt") 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.separator()
layout.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD') layout.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD')
if s.leaf_cards is not None: if s.leaf_cards is not None:
+219
View File
@@ -0,0 +1,219 @@
"""Blatt-Defaults fuer BUESCHEL-Atlanten (eine Card = ein ganzer Zweigbueschel).
Die alten Werte (Size 0.22, Density 150-500) stammen aus der Blaettchen-Aera:
damit streute der Generator hunderte winziger Cards, wo ein paar Dutzend grosse
hingehoeren. Diese Datei nagelt die neuen Groessenordnungen fest und prueft die
Kopplung an die Wachstums-Stufen.
Aufruf: blender --background --factory-startup --python tests/test_blatt_bueschel.py
"""
import os
import sys
import math
import importlib.util
import bpy
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
spec = importlib.util.spec_from_file_location(
"tg", os.path.join(ROOT, "stylized_tree_generator.py"))
tg = importlib.util.module_from_spec(spec)
sys.modules["tg"] = tg
spec.loader.exec_module(tg)
tg.register()
fails = []
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
me = bpy.data.meshes.new("Leaf_Card")
me.from_pydata([(-0.5, 0, 0), (0, 0, 0.7), (0.5, 0, 0), (0, 0, -0.1)],
[], [(0, 1, 2), (0, 2, 3)])
me.update()
card = bpy.data.objects.new("Leaf_Card", me)
bpy.context.scene.collection.objects.link(card)
s = bpy.context.scene.tree_gen_settings
# --- 1) Auslieferungszustand ---------------------------------------------
print("Defaults: Dichte %.0f, Groesse %.2f, Setzlings-Anteil %.2f"
% (s.leaf_density, s.leaf_size, s.leaf_size_growth))
if not (0.6 <= s.leaf_size <= 1.1):
fails.append("leaf_size ist %.2f - fuer Bueschel-Atlanten werden ~0.85 "
"gebraucht, 0.2 waere ein einzelnes Blaettchen" % s.leaf_size)
if s.leaf_budget_s1 > 800 or s.leaf_budget_s2 > 1200 or s.leaf_budget > 4000:
fails.append("Stufen-Budgets (%d/%d/%d) ueber ASSETS.md Teil 6 (800/1200/4000)"
% (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget))
if not (130.0 <= s.leaf_density <= 200.0):
fails.append("leaf_density ist %.0f - der Sweetspot liegt bei 130-170"
% s.leaf_density)
# Der Volumen-Hebel: bei 0.02 trugen nur die aeussersten Zweigspitzen Laub und
# die Krone war innen nackt.
if s.leaf_max_radius < 0.035:
fails.append("leaf_max_radius ist %.3f - unter 0.035 tragen nur die "
"Zweigspitzen Laub und die Krone bleibt innen nackt"
% s.leaf_max_radius)
if s.leaf_tilt < 0.7:
fails.append("leaf_tilt ist %.2f - unter 0.7 fehlt das Rotations-Chaos, "
"das die Sichtluecken schliesst" % s.leaf_tilt)
if abs(tg.LEAF_DEFAULTS["Size"] - s.leaf_size) > 0.2:
fails.append("LEAF_DEFAULTS['Size'] (%.2f) und die UI (%.2f) laufen "
"auseinander" % (tg.LEAF_DEFAULTS["Size"], s.leaf_size))
def bauen(preset, wachstum=False, stufen=4):
for o in list(bpy.context.scene.objects):
if o is not card:
bpy.data.objects.remove(o, do_unlink=True)
s.preset = preset
s.art_name = preset.capitalize()
s.variant = 1
s.apply_modifier = True
s.use_growth = wachstum
s.stages = stufen
s.leaf_card = card
bpy.ops.object.treegen_create()
staemme = sorted((o for o in bpy.context.scene.objects
if o.type == 'MESH' and o is not card
and not o.name.endswith(("_Leaf", "_Frucht"))),
key=lambda o: o.name)
for o in bpy.context.scene.objects:
o.select_set(o in staemme)
bpy.context.view_layer.objects.active = staemme[0]
bpy.ops.object.treegen_leaves()
return staemme
def messen(stamm):
bl = bpy.data.objects.get(stamm.name + "_Leaf")
if bl is None:
return 0, 0.0, 0
dg = bpy.context.evaluated_depsgraph_get()
ev = bl.evaluated_get(dg)
m = ev.to_mesh()
cards = len(m.polygons) // 2
kante = math.sqrt(sum(p.area for p in m.polygons) / cards) if cards else 0.0
ev.to_mesh_clear()
ev2 = stamm.evaluated_get(dg)
m2 = ev2.to_mesh()
tris = sum(len(p.vertices) - 2 for p in m2.polygons) + cards * 2
ev2.to_mesh_clear()
return cards, kante, tris
# --- 2) Alle Presets: Card-Zahl plausibel, Budget gehalten ---------------
print("")
# BUDGET: ASSETS.md nennt 2500 Tris fuer Baum (Stamm + Krone). Mit
# Bueschel-Atlanten liegt LOD0 darueber - das ist eine bewusste Entscheidung der
# Spielseite: der dichtere LOD0 traegt den Look aus der Naehe, und die LOD-Kette
# dampft ihn auf 16 % ein. Die harte Grenze liegt deshalb hier bei 3000; alles
# zwischen 2500 und 3000 wird als Hinweis ausgegeben, damit es nicht unbemerkt
# weiterwaechst.
# Die urspruenglichen 2500 waren eine konservative Setzung. Bei ISM-Instanzen
# mit LOD-Kette sind Roh-Dreiecke nicht der Engpass, sondern der OVERDRAW der
# Cards (die GPU zahlt pro Bildschirmflaeche, nicht pro Dreieck) und die
# Schattenpaesse. Genau deshalb sind 170 grosse Bueschel guenstiger als 500
# kleine - bei gleichem Tri-Zaehler. Die Zahl hier ist die Hartgrenze fuer die
# ausgewachsene Stufe; die endgueltige Freigabe kommt aus einer Messung im
# Level (Testwald, stat unit / stat gpu), nicht aus diesem Test.
LOD0_HART = s.leaf_budget
LOD0_HINWEIS = int(s.leaf_budget * 0.8)
ueber = []
print("%-12s %6s %11s %9s" % ("Preset", "Cards", "Card-Kante", "Tris ges"))
for p in tg.PRESETS:
staemme = bauen(p)
cards, kante, tris = messen(staemme[0])
marke = ""
if tris > LOD0_HART:
marke = "UEBER HARTGRENZE"
elif tris > LOD0_HINWEIS:
marke = "ueber 2500 (LOD0 ok)"
ueber.append((p, tris))
print("%-12s %6d %11.2f %9d %s" % (p, cards, kante, tris, marke))
if tris > LOD0_HART:
fails.append("%s: %d Tris ueber der LOD0-Hartgrenze von %d"
% (p, tris, LOD0_HART))
if p == "kaktus":
continue # kein Ast duenner als Max Radius - richtig so
if cards == 0:
fails.append("%s: keine Cards gestreut" % p)
# Ob es Bueschel oder Blaettchen sind, entscheidet die GROESSE, nicht die
# Anzahl: ein grosser Baum darf 600 Bueschel tragen. Die Menge deckelt das
# Tri-Budget oben ohnehin.
elif kante < 0.30:
fails.append("%s: Card-Kante nur %.2f m - das sind Blaettchen, keine "
"Bueschel" % (p, kante))
if ueber:
print("Hinweis: %s liegen ueber dem Stufenbudget - fuer LOD0 abgesegnet, bei "
"Bedarf ist die Dichte der Feinregler (130-170)."
% ", ".join("%s (%d)" % (p, t) for p, t in ueber))
# --- 3) Wachstum: Bueschel wachsen mit, Setzling ist NICHT kahl ----------
staemme = bauen("baum", wachstum=True, stufen=4)
print("")
print("%-18s %6s %6s %11s" % ("Stufe", "t", "Cards", "Card-Kante"))
kanten, zahlen = [], []
for st in staemme:
cards, kante, _ = messen(st)
gt = float(st.get("growth_t", 1.0))
print("%-18s %6.2f %6d %11.2f" % (st.name, gt, cards, kante))
kanten.append(kante)
zahlen.append(cards)
if cards == 0:
# Genau das war der Fehler: "Dichte" ist Punkte pro m2 Zweigflaeche,
# und ein Setzling hat davon fast keine.
fails.append("%s: keine Cards - ein Setzling darf nicht kahl sein"
% st.name)
for i in range(1, len(kanten)):
if kanten[i] <= kanten[i - 1]:
fails.append("Stufe %d hat Card-Kante %.2f, Stufe %d hatte %.2f - die "
"Bueschel wachsen nicht mit"
% (i + 1, kanten[i], i, kanten[i - 1]))
if kanten and kanten[0] > 0:
anteil = kanten[0] / kanten[-1]
print("Setzlings-Bueschel: %.0f%% der Endgroesse (Regler steht auf %.0f%%)"
% (100 * anteil, 100 * s.leaf_size_growth))
# Der gemessene Anteil darf nicht ueber dem Regler liegen - dann haette die
# Skalierung nicht gegriffen.
if anteil > s.leaf_size_growth + 0.15:
fails.append("Setzlings-Bueschel sind %.0f%% der Endgroesse, der Regler "
"steht auf %.0f%% - die Wachstums-Kopplung greift nicht"
% (100 * anteil, 100 * s.leaf_size_growth))
# --- 3b) Budget je Stufe statt global -----------------------------------
print("")
print("%-18s %6s %8s %8s" % ("Stufe", "t", "Tris", "Budget"))
for st in staemme:
cards, _, tris = messen(st)
gt = float(st.get("growth_t", 1.0))
budget = tg._stufen_budget(s, gt)
print("%-18s %6.2f %8d %8d %s"
% (st.name, gt, tris, budget, "UEBER" if tris > budget else ""))
if tris > budget:
fails.append("%s: %d Tris ueber dem Stufenbudget %d - ein Setzling mit "
"Altbaum-Dichte ist Verschwendung" % (st.name, tris, budget))
# Die Budgets muessen nach oben gehen, sonst ist die Staffelung sinnlos.
if not (s.leaf_budget_s1 < s.leaf_budget_s2 <= s.leaf_budget):
fails.append("Stufen-Budgets steigen nicht: %d / %d / %d"
% (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget))
# --- 4) Ohne Wachstum bleibt alles unveraendert -------------------------
staemme = bauen("baum", wachstum=False)
cards_ohne, kante_ohne, _ = messen(staemme[0])
print("")
print("Ohne Wachstum: %d Cards, Kante %.2f (growth_t=%.2f)"
% (cards_ohne, kante_ohne, float(staemme[0].get("growth_t", 1.0))))
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]))
print("")
if fails:
print("ERGEBNIS: %d FEHLER" % len(fails))
for x in fails:
print(" - " + x)
sys.exit(1)
print("ERGEBNIS: ALLE CHECKS OK")
+4 -1
View File
@@ -77,7 +77,10 @@ def streuen(collection, einzelcard, seed):
dg = bpy.context.evaluated_depsgraph_get() dg = bpy.context.evaluated_depsgraph_get()
ev = bl.evaluated_get(dg) ev = bl.evaluated_get(dg)
me = ev.to_mesh() me = ev.to_mesh()
flaechen = sorted({round(p.area, 5) for p in me.polygons}) # Auf 4 Stellen runden, nicht auf 5: die Cards werden gedreht, und seit die
# Kippung bei 0.9 steht streuen die Flaechen in der sechsten Stelle. Mit
# 5 Stellen zerfiel eine einzige Card-Sorte in vier Eimer.
flaechen = sorted({round(p.area, 4) for p in me.polygons})
n = len(me.polygons) n = len(me.polygons)
ev.to_mesh_clear() ev.to_mesh_clear()
return n, flaechen return n, flaechen