Tree Gen 1.40.0: Obstbaum-Krone schliesst sich oben (Trichter behoben)

Der Baum erfuellte Kronenradius, Deckung, Kronenansatz und Tri-Budget - und sah
trotzdem falsch aus. Sichtbar wurde es erst am BREITENPROFIL der Krone in
Dritteln (unten/mitte/oben, Messung der Spielseite nachgestellt):
  Eiche      3.0 / 2.8 / 2.6   oben/mitte 0.90  (liest sich als Baum)
  Obstbaum   3.3 / 4.5 / 4.7   oben/mitte 1.07  (Trichter)

Kein einzelner Regler hat das geloest: Crown Taper (-0.10 bis +0.50), Branch
Droop (0.6-1.2), Crown Bulge (1.0-1.8) und Attraction Up einzeln durchgefahren
liessen oben/mitte bei rund 1.0 stehen. Der Grund ist strukturell: die
Kronenform folgt nicht der Astlaenge je Hoehe, sondern dem WEG DER ASTSPITZEN.
Solange die Aeste aufwaerts schwingen, landet das aeusserste Laub oben.

Loesung ist eine andere Bauform, kein Feintuning - kurze Aeste an einem hohen
Stamm statt langer Aeste an einem kurzen:
  Height 3.8 -> 4.9, Branch Length 2.55 -> 1.25, Branch Start 0.52 -> 0.40,
  Attraction Up +0.40 -> -0.05, Crown Taper -0.30 -> +0.15,
  Crown Bulge 0.60 -> 1.10, Branch Droop 0.35 -> 0.40.

Ueber alle neun Produktions-Seeds: oben/mitte 0.63-0.84, Kronenradius
1.88-2.01 m, Deckung 100-134 % der Eiche, 3752-4166 Tris, Ansatz 39 %.

Beim Einregeln kippte die Form zwischendurch in eine PILZform (unten am
breitesten) - deshalb steht das Profil jetzt als Test da. test_obstbaum prueft
oben/mitte <= 0.85. Gegenprobe mit den 1.39er Werten: neun Fehler bei 0.98-1.11.

B/H liegt jetzt bei 0.77-0.82 (Band der Spielseite 0.55-0.75). Bleibt bewusst
ungeprueft, wie beim Kronenradius entschieden: die Vorgaben Kronenradius >= 1.80
und B/H <= 0.75 sind bei 4-5 m Hoehe nur im Mittel gleichzeitig erfuellbar.

26 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 14:32:57 +02:00
co-authored by Claude Opus 4.8
parent 5a43ead8d1
commit 0b6f0deff9
8 changed files with 91 additions and 26 deletions
+35
View File
@@ -563,6 +563,41 @@ Der Kaktus erzeugt regulaer **0 Cards** — kein Ast ist duenner als `Max Radius
und das ist botanisch richtig. Vorher lief das still durch und man stand vor und das ist botanisch richtig. Vorher lief das still durch und man stand vor
einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler. einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler.
### Obstbaum-Silhouette: Trichter statt Krone
Der Baum erfuellte Kronenradius, Deckung, Ansatz und Tri-Budget - und sah
trotzdem falsch aus. Gemessen wurde das erst am BREITENPROFIL der Krone in
Dritteln (unten/mitte/oben):
| | unten | mitte | oben | oben/mitte |
|---|---|---|---|---|
| Eiche (liest sich als Baum) | 3.0 | 2.8 | 2.6 | **0.90** |
| Obstbaum vorher | 3.3 | 4.5 | 4.7 | **1.07** |
Nach oben BREITER: die Laubmasse sass an den top-aussen endenden Astspitzen,
unten war die Krone hohl. Ein Trichter, keine Krone.
Kein einzelner Regler hat das geloest - `Crown Taper`, `Branch Droop`,
`Crown Bulge` und `Attraction Up` einzeln durchgefahren liessen das Verhaeltnis
bei rund 1.0 stehen. Der Grund: die Kronenform folgt nicht der Astlaenge je
Hoehe, sondern dem WEG DER ASTSPITZEN. Solange die Aeste aufwaerts schwingen
(`Attraction Up` positiv), landet das aeusserste Laub oben - egal wie die
Laengen verteilt sind.
Die Loesung ist eine andere Bauform, kein Feintuning: **kurze Aeste an einem
hohen Stamm** (Height 3.8 -> 4.9, Branch Length 2.55 -> 1.25) statt langer
Aeste an einem kurzen. Dazu `Attraction Up` +0.40 -> -0.05 (kein Aufschwung),
`Crown Taper` -0.30 -> +0.15 (oben kuerzere Aeste) und `Crown Bulge` 0.60 ->
1.10 (Bauch in der Mitte).
Ueber alle neun Produktions-Seeds: oben/mitte **0.63-0.84**, Kronenradius
1.88-2.01 m, Deckung 100-134 % der Eiche, 3752-4166 Tris, Kronenansatz 39 %.
Beim Einregeln kippte die Form zwischendurch in eine PILZform (unten am
breitesten, u/m 1.17) - deshalb steht das Profil jetzt als Test da und nicht
als Kommentar. `tests/test_obstbaum.py` prueft oben/mitte <= 0.85; Gegenprobe
mit den Werten aus 1.39 meldet neun Fehler bei 0.98-1.11.
### Kronenfuellung: es zaehlt die Flaeche, nicht die Card-Zahl ### Kronenfuellung: es zaehlt die Flaeche, nicht die Card-Zahl
Der Obstbaum las sich in der Engine als einzelne belaubte Ast-Schlaeuche mit Der Obstbaum las sich in der Engine als einzelne belaubte Ast-Schlaeuche mit
+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.39.0" version = "1.40.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.39.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.39.0</a></tt></td> <td><tt><a href="./stylized_tree_generator-1.40.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.40.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>46.0KB</td> <td>45.9KB</td>
</tr> </tr>
</table> </table>
<center><p>Built 2026-08-06, 12:09</p></center> <center><p>Built 2026-08-06, 12:32</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.39.0", "version": "1.40.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.39.0.zip", "archive_url": "./stylized_tree_generator-1.40.0.zip",
"archive_size": 47076, "archive_size": 46997,
"archive_hash": "sha256:44a7601ffb6cfb8004d213416cdc0fdd5592e0c2596a89ed6a32b2c06f12bb4d" "archive_hash": "sha256:530a636ee2a8a6750699c482ac718cd1c16e5ee9c2da8d9c78c6efa07927db05"
} }
] ]
} }
Binary file not shown.
Binary file not shown.
+10 -12
View File
@@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "Stylized Tree Generator", "name": "Stylized Tree Generator",
"author": "D4rkst3r", "author": "D4rkst3r",
"version": (1, 39, 0), "version": (1, 40, 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",
@@ -195,18 +195,16 @@ PRESETS = {
# Und der Kronenansatz zaehlt gegen die GESAMThoehe: Branch Start 0.37 ergab # Und der Kronenansatz zaehlt gegen die GESAMThoehe: Branch Start 0.37 ergab
# gemessen 29 %, nicht 37 %. # gemessen 29 %, nicht 37 %.
"obstbaum": { "obstbaum": {
"Height": 3.8, "Trunk Radius": 0.13, "Bend": 0.3, "Taper": 0.7, "Height": 4.9, "Trunk Radius": 0.13, "Bend": 0.3, "Taper": 0.7,
"Branch Count": 9, # offene Krone: wenige Leitaeste, Licht durch "Branch Count": 9, # offene Krone: wenige Leitaeste, Licht durch
# Bleibt bei 2.55. Die Krone zu verkleinern waere der zweite Hebel gegen # Kurze Aeste an einem HOHEN Stamm. Vorher genau umgekehrt (Height 3.8,
# den niedrigen Fuellgrad gewesen - bei 2.35 fielen aber SECHS der neun # Branch Length 2.55) - daraus wurde ein Trichter, siehe unten.
# Seeds unter den Kronenradius von 1.80 m, den die Frucht-Streuung "Branch Length": 1.25, "Branch Up": 0.7,
# braucht (1.73-1.79). Ueber drei Seeds gemittelt sah es mit 1.83 noch "Branch Start": 0.40, "Branch End": 0.90,
# gut aus; erst der Neun-Seed-Test hat es aufgedeckt. "Crown Taper": 0.15, # oben kuerzere Aeste (war -0.30 = besenfoermig)
"Branch Length": 2.55, "Branch Up": 0.7, "Crown Bulge": 1.1, # Bauch in der Mitte
"Branch Start": 0.52, "Branch End": 0.90, "Attraction Up": -0.05, # KEIN Aufwaertsschwung mehr ...
"Crown Taper": -0.30, "Crown Bulge": 0.60, # rund, Bauch in der Mitte "Branch Droop": 0.4, "Droop Curve": 3.0, # ... Spitzen kippen ab
"Attraction Up": 0.40, # Leitaeste steigen schraeg auf ...
"Branch Droop": 0.35, "Droop Curve": 3.0, # ... und bogen aussen ab
"Branch Bend": 0.9, "Branch Thickness": 0.22, "Branch Bend": 0.9, "Branch Thickness": 0.22,
"Sub Count": 3, "Sub Length": 0.7, "Root Flare": 1.0, "Sub Count": 3, "Sub Length": 0.7, "Root Flare": 1.0,
}, },
+38 -6
View File
@@ -127,12 +127,13 @@ print("")
# Birne 3/17/24. Ein Preset, das nur bei einem Seed passt, ist keines. # Birne 3/17/24. Ein Preset, das nur bei einem Seed passt, ist keines.
SEEDS = (1, 15, 22, 2, 16, 23, 3, 17, 24) SEEDS = (1, 15, 22, 2, 16, 23, 3, 17, 24)
print("%-6s %7s %6s %9s %9s %9s %7s %9s" print("%-6s %7s %6s %9s %9s %7s %8s %-15s %s"
% ("Seed", "Hoehe", "B/H", "KronR", "Card-Max", "KronR/Card", "Ansatz", % ("Seed", "Hoehe", "B/H", "KronR", "KronR/Cd", "Ansatz", "m2/m3",
"m2/m3")) "Profil u/m/o", "o/m"))
verh = [] verh = []
deckungen = [] deckungen = []
obenmitte = []
for seed in SEEDS: for seed in SEEDS:
for o in list(bpy.context.scene.objects): for o in list(bpy.context.scene.objects):
if o is not card: if o is not card:
@@ -172,6 +173,7 @@ for seed in SEEDS:
ev = bl.evaluated_get(dg) ev = bl.evaluated_get(dg)
m2 = ev.to_mesh() m2 = ev.to_mesh()
blatt_flaeche = sum(p.area for p in m2.polygons) blatt_flaeche = sum(p.area for p in m2.polygons)
blatt_pts = [v.co.copy() for v in m2.vertices]
mitten = [] mitten = []
for grp in inseln(m2): for grp in inseln(m2):
pts = [m2.vertices[i].co for i in grp] pts = [m2.vertices[i].co for i in grp]
@@ -186,13 +188,33 @@ for seed in SEEDS:
lz = [z for _x, _y, z in mitten] lz = [z for _x, _y, z in mitten]
kron_zyl = math.pi * lr ** 2 * (max(lz) - min(lz)) kron_zyl = math.pi * lr ** 2 * (max(lz) - min(lz))
# BREITENPROFIL der Krone in Dritteln.
#
# Ohne diesen Check kann die Krone alle bisherigen Zahlen erfuellen und
# trotzdem falsch aussehen: der erste Obstbaum war ein TRICHTER - nach oben
# breiter, unten hohl (gemessen oben/mitte 1.07 gegen 0.90 der Eiche). Die
# Laubmasse sass an den top-aussen endenden Astspitzen. Kronenradius,
# Deckung und Tri-Zahl waren dabei alle in Ordnung.
profil = [0.0, 0.0, 0.0]
if blatt_pts:
pz = [p.z for p in blatt_pts]
pz0, pz1 = min(pz), max(pz)
drittel = (pz1 - pz0) / 3.0
for kk in range(3):
teil = [p for p in blatt_pts
if pz0 + kk * drittel <= p.z <= pz0 + (kk + 1) * drittel]
profil[kk] = 2 * max((math.hypot(p.x, p.y) for p in teil),
default=0.0)
om = profil[2] / profil[1] if profil[1] else 0.0
obenmitte.append(om)
v = kron_r / card_max if card_max else 0.0 v = kron_r / card_max if card_max else 0.0
verh.append(v) verh.append(v)
deckung = blatt_flaeche / kron_zyl if kron_zyl else 0.0 deckung = blatt_flaeche / kron_zyl if kron_zyl else 0.0
deckungen.append(deckung) deckungen.append(deckung)
print("%-6d %7.2f %6.2f %9.2f %9.2f %9.2f %6.0f%% %9.1f" print("%-6d %7.2f %6.2f %9.2f %9.2f %6.0f%% %8.1f %.1f/%.1f/%.1f %.2f"
% (seed, hoehe, 2 * kron_r / hoehe, kron_r, card_max, v, ansatz, % (seed, hoehe, 2 * kron_r / hoehe, kron_r, v, ansatz, deckung,
deckung)) profil[0], profil[1], profil[2], om))
# Kronen-DECKUNG: Blattflaeche je m3 Kronenvolumen. # Kronen-DECKUNG: Blattflaeche je m3 Kronenvolumen.
# #
@@ -218,6 +240,14 @@ for seed in SEEDS:
"Ast-Schlaeuche" % (seed, deckung, EICHE, "Ast-Schlaeuche" % (seed, deckung, EICHE,
100 * deckung / EICHE)) 100 * deckung / EICHE))
# Die Krone muss sich nach OBEN schliessen. Die Eiche, die laut Abnahme als
# Baum liest, kommt auf 0.90; der Trichter lag bei 1.07. 0.85 als Grenze
# laesst Spielraum nach oben, schliesst den Trichter aber sicher aus.
if om > 0.85:
fails.append("Seed %d: Krone oben %.1f m breit, in der Mitte %.1f "
"(%.2f) - sie wird nach oben breiter statt sich zu "
"schliessen" % (seed, profil[2], profil[1], om))
# DIE funktionale Bedingung. # DIE funktionale Bedingung.
if v < 1.3: if v < 1.3:
fails.append("Seed %d: Kronenradius %.2f m bei %.2f m Card - die Krone " fails.append("Seed %d: Kronenradius %.2f m bei %.2f m Card - die Krone "
@@ -241,6 +271,8 @@ for seed in SEEDS:
# funktionale (Kronenradius), weil an ihr die Frucht-Streuung haengt. # funktionale (Kronenradius), weil an ihr die Frucht-Streuung haengt.
print("") 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 oben/mitte ueber alle Seeds: %.2f - %.2f (Eiche 0.90, "
"Trichter war 1.07)" % (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 "
"(%.0f-%.0f %%)" "(%.0f-%.0f %%)"
% (min(deckungen), max(deckungen), EICHE, % (min(deckungen), max(deckungen), EICHE,