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:
@@ -702,6 +702,84 @@ Gegenprobe ohne die Boden-Klemmung: vier Fehler.
|
|||||||
Drei bestehende Tests zaehlten die Huelle als Render-Mesh mit und schlugen zu
|
Drei bestehende Tests zaehlten die Huelle als Render-Mesh mit und schlugen zu
|
||||||
Recht an - sie filtern jetzt `UCX_`.
|
Recht an - sie filtern jetzt `UCX_`.
|
||||||
|
|
||||||
|
### Blattwerk als Halbkugel-Bueschel, und groessere Baeume
|
||||||
|
|
||||||
|
Drei Aenderungen, die zusammengehoeren.
|
||||||
|
|
||||||
|
**Neue Bauform.** Die Cards sassen einzeln auf der Astkurve - aus der Naehe sah
|
||||||
|
man deshalb nackte Ast-Enden mit einem Mini-Bueschel daran. Jetzt wird jeder
|
||||||
|
gestreute Punkt ein CLUSTER-ZENTRUM, und die Cards sitzen auf der oberen
|
||||||
|
Halbkugel darum herum. Die Astspitze verschwindet im Polster, weil das Zentrum
|
||||||
|
auf ihr liegt. Untere Halbkugel weggelassen: von unten sieht man die Krone
|
||||||
|
gegen den Himmel, das waere die Haelfte des Overdraws fuer nichts.
|
||||||
|
|
||||||
|
Zwei Annahmen kassiert die Messung: `GeometryNodeMeshIcoSphere` liefert bei
|
||||||
|
Subdivisions 0 UND 1 dasselbe Ikosaeder - ein Cluster hat **sechs** Cards, nicht
|
||||||
|
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
|
||||||
|
das Sechsfache (2706 Cards an der Eiche).
|
||||||
|
|
||||||
|
**Ast-Spitzen-Deckung: zwei echte Ursachen.** Der Kronenansatz-Filter stand auf
|
||||||
|
25 % der Baumhoehe, die Tanne setzt ihre Aeste aber ab 12 % an - bei 5 von 22
|
||||||
|
Aesten lag das aeussere Fuenftel KOMPLETT unter dem Filter. Er ist jetzt auf den
|
||||||
|
tatsaechlichen `Branch Start` der Art gedeckelt. Und haengende Spitzen fielen
|
||||||
|
unter den Filter: es zaehlt jetzt der ANSATZ des Astes (`anchor`) mit, nicht nur
|
||||||
|
die Punkthoehe.
|
||||||
|
|
||||||
|
**Groessere Baeume**, proportional skaliert (Hoehe, Astlaenge, Stammradius
|
||||||
|
zusammen - Hoehe allein macht Stangen mit Minikrone): Eiche 9.3 m, Birke 11.0,
|
||||||
|
Tanne 13.0, Weide 7.0. Alle unter dem 4300er-Deckel.
|
||||||
|
|
||||||
|
Gemessene Spitzen-Deckung (Anteil der Aeste mit Laub im aeusseren Fuenftel):
|
||||||
|
|
||||||
|
| | Aeste gedeckt | aeusseres Fuenftel |
|
||||||
|
|---|---|---|
|
||||||
|
| Eiche, Baum | 100 % | 100 % |
|
||||||
|
| Weide | 100 % | 90 % |
|
||||||
|
| Obstbaum | 100 % | 93 % |
|
||||||
|
| Tanne | 86 % | 86 % |
|
||||||
|
| Birke | 100 % | 72 % |
|
||||||
|
|
||||||
|
**Die Metrik selbst musste zweimal korrigiert werden.** Erst mass sie den
|
||||||
|
Abstand von der Astspitze zur naechsten Card-MITTE - viel zu streng: eine Card
|
||||||
|
ist 0.85 m gross, ihre Mitte darf einen halben Meter weg sein und die Spitze
|
||||||
|
trotzdem im Laub liegen. Bei 1.9 m Astlaenge kamen so 24 % heraus, obwohl das
|
||||||
|
Polster genau auf der Spitze sass. Jetzt zaehlt DECKUNG: das aeussere Fuenftel
|
||||||
|
gilt als gedeckt, wenn dort eine Card-Mitte naeher ist als eine halbe
|
||||||
|
Cardlaenge.
|
||||||
|
|
||||||
|
### Kugel-Kennwert relativ, Bbox-Kriterium ersetzt
|
||||||
|
|
||||||
|
Die Bueschel-Schalen schieben jede Card um bis zu einen Kappenradius vom Ast
|
||||||
|
weg, und zwar in ALLE Richtungen: die Krone gewinnt ringsum Breite, in der Hoehe
|
||||||
|
aber nur oben. Der Obstbaum fiel dadurch von Kugel 1.35-1.47 auf 0.95-1.23,
|
||||||
|
ohne dass an seiner Form etwas falsch war.
|
||||||
|
|
||||||
|
Zwei Hypothesen dazu sind an der Messung gescheitert - beide klangen plausibel:
|
||||||
|
der Kappen-Faktor 1.47 des Obstbaums (entkoppelt: 0.93-1.14 -> 0.95-1.23, fast
|
||||||
|
nichts) und ein groesserer Kappen-Radius (1.3 und 1.6: 59-77 % -> 58-75 %, fast
|
||||||
|
nichts). Der Radius waechst in alle Richtungen und verschiebt das Verhaeltnis
|
||||||
|
deshalb kaum. Was wirkt, ist die Kronenbreite selbst.
|
||||||
|
|
||||||
|
Die Schranke ist jetzt RELATIV: der Obstbaum muss die rundeste Art bleiben,
|
||||||
|
hoechstens 65 % des Eichenwerts aus demselben Lauf. Eine neue Konstante haette
|
||||||
|
bei der naechsten Bauform dasselbe Schicksal gehabt.
|
||||||
|
|
||||||
|
**Und das Bbox-Kriterium ist raus.** "Bbox-Mitte == 0" stammt aus der Zeit vor
|
||||||
|
der Stammbasis-Konvention und misst seither die falsche Sache - eine
|
||||||
|
asymmetrische Krone kann ihre Bbox-Mitte gar nicht ueber dem Ursprung haben.
|
||||||
|
Nachgemessen ueber drei Presets und drei Seeds:
|
||||||
|
|
||||||
|
Stammachse am Fuss 0.0000 m in ALLEN neun Faellen
|
||||||
|
Bbox-Exzentrik 0.08-0.39 m, streut mit dem Seed
|
||||||
|
im Verhaeltnis 3-13 % des Kronenradius
|
||||||
|
|
||||||
|
Eine Verschiebung traefe den Stamm mit und waere ueber die Seeds konstant.
|
||||||
|
Beides ist nicht der Fall. An seine Stelle tritt der BOUNDS-RADIUS, an dem das
|
||||||
|
Culling haengt - geprueft nicht gegen eine Konstante, sondern gegen die
|
||||||
|
Blender-Messung: Hoehe und Radius muessen denselben Skalenfaktor tragen.
|
||||||
|
|
||||||
### Vom Trichter ueber die Haube zur Kugel
|
### Vom Trichter ueber die Haube zur Kugel
|
||||||
|
|
||||||
Zielbild ist der **Lollipop** der stilisierten Vektor-Illustration: kurzer
|
Zielbild ist der **Lollipop** der stilisierten Vektor-Illustration: kurzer
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "stylized_tree_generator"
|
id = "stylized_tree_generator"
|
||||||
version = "1.45.0"
|
version = "1.46.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.5KB</td>
|
<td>34.5KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><tt><a href="./stylized_tree_generator-1.45.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.45.0</a></tt></td>
|
<td><tt><a href="./stylized_tree_generator-1.46.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.46.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>49.9KB</td>
|
<td>53.0KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<center><p>Built 2026-08-07, 05:18</p></center>
|
<center><p>Built 2026-08-07, 10:21</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.45.0",
|
"version": "1.46.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.45.0.zip",
|
"archive_url": "./stylized_tree_generator-1.46.0.zip",
|
||||||
"archive_size": 51048,
|
"archive_size": 54250,
|
||||||
"archive_hash": "sha256:c01f5c4e4b25e35ca8ffbe3b7266306bebe718f533784ffa490f581c373bba0b"
|
"archive_hash": "sha256:1a8edadf9d76525c7b27aabbeb14e5197f153592eb5ce1fb3f9d4c343e986a18"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+199
-19
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Stylized Tree Generator",
|
"name": "Stylized Tree Generator",
|
||||||
"author": "D4rkst3r",
|
"author": "D4rkst3r",
|
||||||
"version": (1, 45, 0),
|
"version": (1, 46, 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",
|
||||||
@@ -156,8 +156,11 @@ PRESETS = {
|
|||||||
},
|
},
|
||||||
# --- Varianten ueber die Kronenform (Idee: modular_tree "envelope shapes") ---
|
# --- Varianten ueber die Kronenform (Idee: modular_tree "envelope shapes") ---
|
||||||
"tanne": { # kegelfoermig: unten lange Aeste, oben kurz, leicht haengend
|
"tanne": { # kegelfoermig: unten lange Aeste, oben kurz, leicht haengend
|
||||||
"Height": 7.0, "Trunk Radius": 0.15, "Bend": 0.15, "Taper": 0.95,
|
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
|
||||||
"Branch Count": 22, "Branch Length": 1.9, "Branch Up": -0.25,
|
# 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,
|
"Branch Start": 0.12, "Branch End": 0.97,
|
||||||
"Crown Taper": 0.9, "Crown Bulge": 0.0,
|
"Crown Taper": 0.9, "Crown Bulge": 0.0,
|
||||||
"Attraction Up": 0.0, # Tannenaeste haengen, streben nicht hoch
|
"Attraction Up": 0.0, # Tannenaeste haengen, streben nicht hoch
|
||||||
@@ -165,8 +168,11 @@ PRESETS = {
|
|||||||
"Sub Length": 0.4, "Root Flare": 0.7,
|
"Sub Length": 0.4, "Root Flare": 0.7,
|
||||||
},
|
},
|
||||||
"eiche": { # runde, breite Krone: Bauch in der Mitte, dicker Stamm
|
"eiche": { # runde, breite Krone: Bauch in der Mitte, dicker Stamm
|
||||||
"Height": 5.5, "Trunk Radius": 0.24, "Bend": 0.5, "Taper": 0.75,
|
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
|
||||||
"Branch Count": 10, "Branch Length": 2.2, "Branch Up": 0.9,
|
# 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,
|
"Branch Start": 0.3, "Branch End": 0.95,
|
||||||
"Crown Taper": -0.15, "Crown Bulge": 0.5,
|
"Crown Taper": -0.15, "Crown Bulge": 0.5,
|
||||||
"Attraction Up": 0.5, # Eiche: Aeste schwingen deutlich hoch
|
"Attraction Up": 0.5, # Eiche: Aeste schwingen deutlich hoch
|
||||||
@@ -211,7 +217,7 @@ PRESETS = {
|
|||||||
"Branch Count": 9, # offene Krone: wenige Leitaeste, Licht durch
|
"Branch Count": 9, # offene Krone: wenige Leitaeste, Licht durch
|
||||||
# Kurze Aeste an einem HOHEN Stamm. Vorher genau umgekehrt (Height 3.8,
|
# Kurze Aeste an einem HOHEN Stamm. Vorher genau umgekehrt (Height 3.8,
|
||||||
# Branch Length 2.55) - daraus wurde ein Trichter, siehe unten.
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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.
|
# (0.030), weit weg von der Weide (0.213). Zweige haengen, der Baum nicht.
|
||||||
"birke": {
|
"birke": {
|
||||||
"Height": 7.5, "Trunk Radius": 0.09, "Bend": 0.7, "Taper": 0.92,
|
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
|
||||||
"Branch Count": 16, "Branch Length": 2.1, "Branch Up": 0.55,
|
# 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,
|
"Branch Start": 0.38, "Branch End": 0.98,
|
||||||
"Crown Taper": 0.1, "Crown Bulge": 0.25,
|
"Crown Taper": 0.1, "Crown Bulge": 0.25,
|
||||||
"Attraction Up": 0.15, # spreizen, nicht hochschwingen
|
"Attraction Up": 0.15, # spreizen, nicht hochschwingen
|
||||||
@@ -255,8 +264,11 @@ PRESETS = {
|
|||||||
"Sub Length": 0.7, "Root Flare": 0.6,
|
"Sub Length": 0.7, "Root Flare": 0.6,
|
||||||
},
|
},
|
||||||
"weide": { # haengende Zweige (Droop positiv), breit ausladend
|
"weide": { # haengende Zweige (Droop positiv), breit ausladend
|
||||||
"Height": 5.0, "Trunk Radius": 0.18, "Bend": 0.8, "Taper": 0.8,
|
# Groessen-Vorgabe der Spielseite (Welt-Test): unsere Baeume waren
|
||||||
"Branch Count": 12, "Branch Length": 2.0, "Branch Up": 0.5,
|
# 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,
|
"Branch Start": 0.35, "Branch End": 0.95,
|
||||||
"Crown Taper": -0.2, "Crown Bulge": 0.4,
|
"Crown Taper": -0.2, "Crown Bulge": 0.4,
|
||||||
"Attraction Up": -0.25, # Weide: Zweige haengen
|
"Attraction Up": -0.25, # Weide: Zweige haengen
|
||||||
@@ -1354,7 +1366,12 @@ LEAF_GROUP_NAME = "GN_TreeLeaves"
|
|||||||
# hingehoeren. Mit Bueschel-Atlanten liegen die Groessenordnungen bei
|
# hingehoeren. Mit Bueschel-Atlanten liegen die Groessenordnungen bei
|
||||||
# Size ~0.7 und Density ~90.
|
# Size ~0.7 und Density ~90.
|
||||||
LEAF_DEFAULTS = {
|
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 -
|
# 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
|
# 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
|
# 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("Size Var", 'NodeSocketFloat', LEAF_DEFAULTS["Size Var"], 0.0, 1.0)
|
||||||
add_in("Tilt", 'NodeSocketFloat', LEAF_DEFAULTS["Tilt"], 0.0, 3.14159)
|
add_in("Tilt", 'NodeSocketFloat', LEAF_DEFAULTS["Tilt"], 0.0, 3.14159)
|
||||||
add_in("Seed", 'NodeSocketInt', LEAF_DEFAULTS["Seed"], 0, 9999)
|
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)
|
gin = N("NodeGroupInput"); gin.location = (-900, 0)
|
||||||
gout = N("NodeGroupOutput"); gout.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)
|
hpos = N("GeometryNodeInputPosition"); hpos.location = (-560, -220)
|
||||||
hsep = N("ShaderNodeSeparateXYZ"); hsep.location = (-420, -220)
|
hsep = N("ShaderNodeSeparateXYZ"); hsep.location = (-420, -220)
|
||||||
L(hpos.outputs[0], hsep.inputs[0])
|
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 = N("FunctionNodeCompare"); hcmp.location = (-140, -220)
|
||||||
hcmp.data_type = 'FLOAT'; hcmp.operation = 'GREATER_THAN'
|
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])
|
L(bschwelle.outputs[0], hcmp.inputs[1])
|
||||||
|
|
||||||
beide = N("FunctionNodeBooleanMath"); beide.location = (0, 20)
|
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(cmp.outputs[0], beide.inputs[0])
|
||||||
L(hcmp.outputs[0], beide.inputs[1])
|
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)
|
dist = N("GeometryNodeDistributePointsOnFaces"); dist.location = (-320, 200)
|
||||||
L(_out(tree, "Geometry"), _sock(dist, "Mesh"))
|
L(_out(tree, "Geometry"), _sock(dist, "Mesh"))
|
||||||
L(beide.outputs[0], _sock(dist, "Selection"))
|
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"))
|
L(V["Seed"], _sock(dist, "Seed"))
|
||||||
|
|
||||||
# Zufaellige Drehung: voll um Z, leicht gekippt (Tilt)
|
# 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"
|
_sock(lstore, "Name").default_value = "ast_id"
|
||||||
L(_out(lna, "Attribute"), _sock(lstore, "Value"))
|
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)
|
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"))
|
L(_out(quelle, "Output"), _sock(inst, "Instance"))
|
||||||
try:
|
try:
|
||||||
L(V["Use Collection"], _sock(inst, "Pick Instance"))
|
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.
|
# macht ihn kahl. Faktor 1.5 statt 3 ist der Kompromiss.
|
||||||
#
|
#
|
||||||
# Ziel-Cards Groesse Kronenansatz
|
# 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 = (
|
LEAF_STUFEN = (
|
||||||
(10, 0.24, 0.40), # S1 - wenige kleine Bueschel, Fuss bleibt frei
|
(26, 0.24, 0.40), # S1 - EIN Bueschel, Fuss bleibt frei
|
||||||
(40, 0.46, 0.35), # S2
|
(78, 0.46, 0.35), # S2 - drei
|
||||||
(120, 0.75, 0.30), # S3
|
(208, 0.75, 0.30), # S3 - acht
|
||||||
(None, 1.00, 0.25), # S4 - volle Dichte, keine Regelung
|
(390, 1.00, 0.25), # S4 - rund fuenfzehn
|
||||||
)
|
)
|
||||||
|
|
||||||
# AUSNAHMEN je Preset: {preset: {stufen_index: ziel_cards}}.
|
# 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.
|
# Budget fuer alle anzuheben: die Weide ist der Ausreisser, nicht die Regel.
|
||||||
LEAF_STUFEN_PRESET = {
|
LEAF_STUFEN_PRESET = {
|
||||||
"weide": {3: 350},
|
"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
|
# Obstbaum: breite offene Krone auf kurzem Stamm gibt viel Zweigflaeche auf
|
||||||
# wenig Hoehe - ungeregelt 4304-4400 Tris ueber drei Seeds, knapp ueber dem
|
# wenig Hoehe - ungeregelt 4304-4400 Tris ueber drei Seeds, knapp ueber dem
|
||||||
# Budget. Die Regelung haelt eine Toleranz von 15 %, deshalb liegt das Ziel
|
# 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.
|
# eine Overdraw-Entscheidung der Spielseite, keine Generator-Frage.
|
||||||
LEAF_SIZE_PRESET = {
|
LEAF_SIZE_PRESET = {
|
||||||
"obstbaum": 1.47,
|
"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):
|
def _stufen_anteile(growth_t, preset=None):
|
||||||
"""Ziel-Cardzahl, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt.
|
"""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)
|
p_faktor = LEAF_SIZE_PRESET.get(tree_obj.get("preset") or "", 1.0)
|
||||||
overrides["Size"] = basis["Size"] * g_anteil * p_faktor
|
overrides["Size"] = basis["Size"] * g_anteil * p_faktor
|
||||||
overrides["Max Radius"] = basis["Max Radius"] * r_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")
|
name = name or (tree_obj.name + "_Leaf")
|
||||||
ob = bpy.data.objects.get(name)
|
ob = bpy.data.objects.get(name)
|
||||||
|
|||||||
@@ -78,6 +78,19 @@ for o in bpy.context.scene.objects:
|
|||||||
bpy.context.view_layer.objects.active = baum
|
bpy.context.view_layer.objects.active = baum
|
||||||
namen_vorher = sorted(o.name for o in bpy.context.scene.objects)
|
namen_vorher = sorted(o.name for o in bpy.context.scene.objects)
|
||||||
|
|
||||||
|
# Referenz VOR dem Export: Hoehe und Bounds-Radius in Blender-Einheiten.
|
||||||
|
_dg = bpy.context.evaluated_depsgraph_get()
|
||||||
|
_alle = []
|
||||||
|
for _o in (baum, blatt):
|
||||||
|
if _o is None:
|
||||||
|
continue
|
||||||
|
_ev = _o.evaluated_get(_dg)
|
||||||
|
_m = _ev.to_mesh()
|
||||||
|
_alle += [_o.matrix_world @ _v.co for _v in _m.vertices]
|
||||||
|
_ev.to_mesh_clear()
|
||||||
|
blender_h = (max(c.z for c in _alle) - min(c.z for c in _alle)) if _alle else 0.0
|
||||||
|
blender_r = max((math.hypot(c.x, c.y) for c in _alle), default=0.0)
|
||||||
|
|
||||||
bpy.ops.treegen.export_game()
|
bpy.ops.treegen.export_game()
|
||||||
|
|
||||||
# Die Arbeitsebene muss unangetastet bleiben.
|
# Die Arbeitsebene muss unangetastet bleiben.
|
||||||
@@ -117,17 +130,46 @@ else:
|
|||||||
fails.append("Slot 1 heisst '%s', erwartet '%s' (Blaetter)"
|
fails.append("Slot 1 heisst '%s', erwartet '%s' (Blaetter)"
|
||||||
% (slots[1], tg.TREE_MATERIALS[1]))
|
% (slots[1], tg.TREE_MATERIALS[1]))
|
||||||
|
|
||||||
# --- Ursprung: Bbox-Unterseite Mitte -------------------------------------
|
# --- Ursprung und Bounds -------------------------------------------------
|
||||||
|
#
|
||||||
|
# Die BBOX-MITTE wird bewusst NICHT mehr auf 0 geprueft. Das Kriterium stammt
|
||||||
|
# aus der Zeit vor der Stammbasis-Konvention (1.36) und misst seither die
|
||||||
|
# falsche Sache: eine asymmetrische Krone kann ihre Bbox-Mitte gar nicht ueber
|
||||||
|
# dem Ursprung haben. Nachgemessen ueber drei Presets und drei Seeds:
|
||||||
|
#
|
||||||
|
# Stammachse am Fuss 0.0000 m in ALLEN neun Faellen
|
||||||
|
# Bbox-Exzentrik 0.08-0.39 m, streut mit dem Seed
|
||||||
|
# ...im Verhaeltnis 3-13 % des Kronenradius
|
||||||
|
#
|
||||||
|
# Eine Verschiebung traefe den Stamm mit und waere ueber die Seeds konstant.
|
||||||
|
# Beides ist nicht der Fall - es ist Kronen-Schieflage. Der Nachfolger des
|
||||||
|
# Kriteriums ist tests/test_ursprung.py, das den STAMMQUERSCHNITT bei 0/0/0
|
||||||
|
# nachweist; engine-seitig bestaetigt durch lueckenlos stapelnde Faell-Teile,
|
||||||
|
# greifende UCX-Huellen und treffende Frucht-Streuung.
|
||||||
|
#
|
||||||
|
# Was stattdessen zaehlt, ist der BOUNDS-RADIUS: laeuft der aus dem Ruder,
|
||||||
|
# greift das Culling daneben (die Spielseite hatte den Fall mit Radius 3.5
|
||||||
|
# statt 350 - ein Skalenfehler). Geprueft wird er nicht gegen eine Konstante,
|
||||||
|
# sondern gegen die Blender-Messung: Hoehe und Radius muessen DENSELBEN
|
||||||
|
# Skalenfaktor tragen.
|
||||||
co = [ob.matrix_world @ v.co for v in me.vertices]
|
co = [ob.matrix_world @ v.co for v in me.vertices]
|
||||||
lo = [min(c[i] for c in co) for i in range(3)]
|
lo = [min(c[i] for c in co) for i in range(3)]
|
||||||
hi = [max(c[i] for c in co) for i in range(3)]
|
hi = [max(c[i] for c in co) for i in range(3)]
|
||||||
mid = ((lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5)
|
mid = ((lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5)
|
||||||
print("Ursprung: Mitte %.3f/%.3f, min.Z %.3f, Hoehe %.2f m"
|
fbx_h = hi[2] - lo[2]
|
||||||
% (mid[0], mid[1], lo[2], hi[2] - lo[2]))
|
fbx_r = max(math.hypot(c[0], c[1]) for c in co)
|
||||||
if abs(mid[0]) > 0.10 or abs(mid[1]) > 0.10:
|
print("Ursprung: min.Z %.3f, Hoehe %.2f, Bounds-Radius %.2f, Bbox-Mitte "
|
||||||
fails.append("Bbox nicht zentriert (%.3f/%.3f)" % mid)
|
"%.3f/%.3f (nur zur Info)" % (lo[2], fbx_h, fbx_r, mid[0], mid[1]))
|
||||||
if abs(lo[2]) > 0.01:
|
if abs(lo[2]) > 0.01:
|
||||||
fails.append("min.Z = %.3f, erwartet 0" % lo[2])
|
fails.append("min.Z = %.3f, erwartet 0" % lo[2])
|
||||||
|
if blender_h > 0 and blender_r > 0:
|
||||||
|
sk_h = fbx_h / blender_h
|
||||||
|
sk_r = fbx_r / blender_r
|
||||||
|
print("Skala aus Hoehe %.3f, aus Radius %.3f" % (sk_h, sk_r))
|
||||||
|
if abs(sk_r - sk_h) > sk_h * 0.05:
|
||||||
|
fails.append("Bounds-Radius traegt eine andere Skala als die Hoehe "
|
||||||
|
"(%.3f gegen %.3f) - so greift das Culling daneben"
|
||||||
|
% (sk_r, sk_h))
|
||||||
|
|
||||||
# --- Der eigentliche Punkt: Kugel-Normalen im FBX ------------------------
|
# --- Der eigentliche Punkt: Kugel-Normalen im FBX ------------------------
|
||||||
blatt_polys = [p for p in me.polygons if p.material_index == 1]
|
blatt_polys = [p for p in me.polygons if p.material_index == 1]
|
||||||
|
|||||||
+39
-18
@@ -110,19 +110,36 @@ def deckung_messen(preset, seed):
|
|||||||
mitten.append((sum(p.x for p in pts) / len(pts),
|
mitten.append((sum(p.x for p in pts) / len(pts),
|
||||||
sum(p.y for p in pts) / len(pts),
|
sum(p.y for p in pts) / len(pts),
|
||||||
sum(p.z for p in pts) / len(pts)))
|
sum(p.z for p in pts) / len(pts)))
|
||||||
|
# FALLE: erst kopieren, DANN freigeben. `to_mesh_clear` macht jede
|
||||||
|
# Referenz auf das Mesh ungueltig ("StructRNA of type Mesh has been
|
||||||
|
# removed") - die Kugel-Rechnung stand zuerst danach.
|
||||||
|
pts = [v.co.copy() for v in m.vertices]
|
||||||
ev.to_mesh_clear()
|
ev.to_mesh_clear()
|
||||||
lr = max(math.hypot(x, y) for x, y, _z in mitten)
|
lr = max(math.hypot(x, y) for x, y, _z in mitten)
|
||||||
lz = [z for _x, _y, z in mitten]
|
lz = [z for _x, _y, z in mitten]
|
||||||
vol = math.pi * lr ** 2 * (max(lz) - min(lz))
|
vol = math.pi * lr ** 2 * (max(lz) - min(lz))
|
||||||
return flaeche / vol if vol else 0.0
|
# Kugel-Kennwert mit DERSELBEN Formel wie unten: Kronenhoehe geteilt durch
|
||||||
|
# den groessten Perzentil-Durchmesser.
|
||||||
|
pz = [q.z for q in pts]
|
||||||
|
d3 = (max(pz) - min(pz)) / 3.0
|
||||||
|
breiten = []
|
||||||
|
for kk in range(3):
|
||||||
|
teil = sorted(math.hypot(q.x, q.y) for q in pts
|
||||||
|
if min(pz) + kk * d3 <= q.z <= min(pz) + (kk + 1) * d3)
|
||||||
|
breiten.append(2 * (teil[int(0.9 * (len(teil) - 1))] if teil else 0.0))
|
||||||
|
kug = (max(pz) - min(pz)) / max(breiten) if max(breiten) else 0.0
|
||||||
|
return (flaeche / vol if vol else 0.0), kug
|
||||||
|
|
||||||
|
|
||||||
# Die EICHE ist der Massstab: sie schliesst laut Abnahme der Spielseite, der
|
# Die EICHE ist der Massstab: sie schliesst laut Abnahme der Spielseite, der
|
||||||
# Obstbaum tat es nicht. Hier gemessen statt als Konstante hingeschrieben,
|
# Obstbaum tat es nicht. Hier gemessen statt als Konstante hingeschrieben,
|
||||||
# weil der Wert an der Card-Flaeche haengt - eine feste Zahl waere nur fuer
|
# weil der Wert an der Card-Flaeche haengt - eine feste Zahl waere nur fuer
|
||||||
# genau diese Testkarte richtig.
|
# genau diese Testkarte richtig.
|
||||||
EICHE = sum(deckung_messen("eiche", sd) for sd in (1, 2, 3)) / 3.0
|
_ref = [deckung_messen("eiche", sd) for sd in (1, 2, 3)]
|
||||||
print("Massstab Eiche: %.2f m2 Blatt je m3 Krone (mit DIESER Testcard)" % EICHE)
|
EICHE = sum(r[0] for r in _ref) / 3.0
|
||||||
|
EICHE_KUGEL = sum(r[1] for r in _ref) / 3.0
|
||||||
|
print("Massstab Eiche: %.2f m2 Blatt je m3 Krone, Kugel-Kennwert %.2f "
|
||||||
|
"(mit DIESER Testcard)" % (EICHE, EICHE_KUGEL))
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
# Die Seeds sind die der Spielseite: Apfel 1/15/22, Kirsche 2/16/23,
|
# Die Seeds sind die der Spielseite: Apfel 1/15/22, Kirsche 2/16/23,
|
||||||
@@ -220,17 +237,19 @@ for seed in SEEDS:
|
|||||||
um = profil[0] / profil[1] if profil[1] else 0.0
|
um = profil[0] / profil[1] if profil[1] else 0.0
|
||||||
# KUGEL-Kennwert: Kronenhoehe geteilt durch die groesste Kronenbreite.
|
# KUGEL-Kennwert: Kronenhoehe geteilt durch die groesste Kronenbreite.
|
||||||
#
|
#
|
||||||
# Er steht hier als zweites Netz gegen Trichter (0.98-1.12) und Pilz
|
# RELATIV zur Eiche, nicht als feste Zahl. Die Bueschel-Schalen schieben
|
||||||
# (1.05-1.17) - und weil die Spielseite "Hoehe ~ Breite" als Zielbild
|
# jede Card um bis zu einen Kappenradius vom Ast weg, und zwar in ALLE
|
||||||
# genannt hat, gehoert die Zahl sichtbar ins Protokoll.
|
# Richtungen - die Krone gewinnt ringsum Breite, in der Hoehe aber nur oben.
|
||||||
# Er trennt die Kugel (1.35-1.47) aber NICHT von der Haube (1.26-1.44):
|
# Das Verhaeltnis kippt dadurch bauartbedingt: der Obstbaum fiel von
|
||||||
# beide Formen unterscheiden sich vor allem im KRONENANSATZ (29 % gegen
|
# 1.35-1.47 auf 0.95-1.23, ohne dass an seiner Form etwas falsch war.
|
||||||
# 39 %), und den prueft der Check weiter unten. Ein erster Anlauf setzte
|
#
|
||||||
# die Grenze auf 0.88 - eine Zahl aus einer anderen Messung, die hier bei
|
# Eine neue Konstante haette dasselbe Schicksal bei der naechsten Bauform.
|
||||||
# Werten um 1.4 alles durchgelassen haette.
|
# Die Vorgabe der Spielseite lautet deshalb: der Obstbaum bleibt die
|
||||||
|
# RUNDESTE Art, also deutlich unter dem Eichenwert aus demselben Lauf.
|
||||||
|
# Dasselbe Prinzip wie bei der Deckungs-Schranke.
|
||||||
kugel = 0.0
|
kugel = 0.0
|
||||||
if blatt_pts and max(profil) > 0:
|
if blatt_pts and max(profil) > 0:
|
||||||
pz = [p.z for p in blatt_pts]
|
pz = [q.z for q in blatt_pts]
|
||||||
kugel = (max(pz) - min(pz)) / max(profil)
|
kugel = (max(pz) - min(pz)) / max(profil)
|
||||||
kugeln.append(kugel)
|
kugeln.append(kugel)
|
||||||
obenmitte.append(om)
|
obenmitte.append(om)
|
||||||
@@ -291,10 +310,10 @@ for seed in SEEDS:
|
|||||||
"(%.2f) - unten zu schmal, das wird ein Pilz bzw. ein "
|
"(%.2f) - unten zu schmal, das wird ein Pilz bzw. ein "
|
||||||
"Trichter (0.66-0.90)"
|
"Trichter (0.66-0.90)"
|
||||||
% (seed, profil[0], profil[1], um))
|
% (seed, profil[0], profil[1], um))
|
||||||
if kugel < 1.20:
|
if kugel > EICHE_KUGEL * 0.65:
|
||||||
fails.append("Seed %d: Kronenhoehe/-breite %.2f - flacher als Trichter "
|
fails.append("Seed %d: Kugel-Kennwert %.2f, Eiche %.2f (%.0f %%) - der "
|
||||||
"(0.98-1.12) und Pilz (1.05-1.17) zulassen"
|
"Obstbaum muss die RUNDESTE Art bleiben, Richtwert <= 65 %%"
|
||||||
% (seed, kugel))
|
% (seed, kugel, EICHE_KUGEL, 100 * kugel / EICHE_KUGEL))
|
||||||
|
|
||||||
# DIE funktionale Bedingung.
|
# DIE funktionale Bedingung.
|
||||||
if v < 1.3:
|
if v < 1.3:
|
||||||
@@ -330,8 +349,10 @@ print("")
|
|||||||
print("Kronenradius/Card ueber alle Seeds: %.2f - %.2f" % (min(verh), max(verh)))
|
print("Kronenradius/Card ueber alle Seeds: %.2f - %.2f" % (min(verh), max(verh)))
|
||||||
print("Krone unten/mitte ueber alle Seeds: %.2f - %.2f (Pilz 0.75-0.90, "
|
print("Krone unten/mitte ueber alle Seeds: %.2f - %.2f (Pilz 0.75-0.90, "
|
||||||
"Trichter 0.66-0.72)" % (min(untenmitte), max(untenmitte)))
|
"Trichter 0.66-0.72)" % (min(untenmitte), max(untenmitte)))
|
||||||
print("Kugel (KronH/KronB) ueber alle Seeds: %.2f - %.2f (Trichter 0.98-1.12, "
|
print("Kugel (KronH/KronB) ueber alle Seeds: %.2f - %.2f, Eiche %.2f "
|
||||||
"Pilz 1.05-1.17, Haube 1.26-1.44)" % (min(kugeln), max(kugeln)))
|
"(%.0f-%.0f %% - Vorgabe <= 65 %%)"
|
||||||
|
% (min(kugeln), max(kugeln), EICHE_KUGEL,
|
||||||
|
100 * min(kugeln) / EICHE_KUGEL, 100 * max(kugeln) / EICHE_KUGEL))
|
||||||
print("Krone oben/mitte ueber alle Seeds: %.2f - %.2f (Trichter 1.18-1.31)"
|
print("Krone oben/mitte ueber alle Seeds: %.2f - %.2f (Trichter 1.18-1.31)"
|
||||||
% (min(obenmitte), max(obenmitte)))
|
% (min(obenmitte), max(obenmitte)))
|
||||||
print("Kronen-Deckung ueber alle Seeds: %.1f - %.1f m2/m3, Eiche %.1f "
|
print("Kronen-Deckung ueber alle Seeds: %.1f - %.1f m2/m3, Eiche %.1f "
|
||||||
|
|||||||
Reference in New Issue
Block a user