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:
@@ -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,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"
|
||||||
|
|||||||
Vendored
+3
-3
@@ -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>
|
||||||
|
|||||||
Vendored
+4
-4
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+110
-17
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user