Tree Gen 1.34.0: Ziel-Cardzahl statt Dichte-Konstanten + Ursprung aus dem Stamm

Zwei Befunde von der Spielseite.

1) Dichte-Kompensation ueberschoss preset-abhaengig
   Die Eichung am Standardbaum (11 Cards / 486 Tris) kam auf der Eiche als
   1008 Tris an. Ursache ist strukturell: "Dichte" ist Punkte pro m2
   Zweigflaeche, und die verteilt sich je Preset voellig anders. Jeder
   Konstanten-Satz haette fuer den naechsten Baum neu geeicht werden muessen.

   Strukturwechsel wie vorgeschlagen: in LEAF_STUFEN steht jetzt die
   ZIEL-STUECKZAHL (S1 10, S2 40, S3 120, S4 = volle Dichte), und der Generator
   regelt die Dichte per Bisektion dagegen ein. Der Seed wird dabei nicht
   angefasst - gleicher Baum und Seed ergeben dieselbe Verteilung.

   Gemessen ueber vier Presets (Ziel 10 / 40 / 120):
     baum    9   41  104     eiche  10   43  138
     birke   9   36  136     tanne   9   38  125
   Die Eiche liegt bei S1 jetzt bei 576 Tris statt 1008.

   Der Test prueft die Anzahl DIREKT, statt ueber Dichte x Anteil x Flaeche zu
   rechnen - genau der Punkt des Vorschlags.

2) Baum stand "etwas in der Luft"
   Der Export-Ursprung kam aus der Gesamt-Bbox inklusive Blatt-Cards. Eine
   einzige tief haengende Card hebt damit den ganzen Baum an. Jetzt kommt er aus
   der STAMM-Geometrie (Slot 0). Gemessen an der Eiche: Stamm-min.Z 0.000 auf
   allen vier Stufen, waehrend die Gesamtbox bei S1 auf -0.106 liegt - das waren
   die 10 cm, um die er zu hoch stand.

Beim Einbauen selbst verursacht und behoben: eine abgebrochene Befehlskette
liess die Operator-Verdrahtung aus, wodurch die Dichte mit der ZIEL-ANZAHL
multipliziert wurde (170 x 40) - 732 Cards statt 40. Faellt sofort auf, wenn man
die Zahlen anschaut, statt den Durchlauf fuer erfolgreich zu halten.

23 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 10:32:15 +02:00
co-authored by Claude Opus 4.8
parent d02562fb53
commit 1b833bf44a
8 changed files with 197 additions and 28 deletions
+18
View File
@@ -403,6 +403,24 @@ gemessen und abgenommen:
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.
**Ziel-Cardzahl statt Dichte-Konstanten.** Dichte-Faktoren mussten je Preset neu
geeicht werden: „Dichte" ist Punkte pro m² *Zweigflaeche*, und die verteilt sich
je Baum voellig anders — ein Faktor, der am Standardbaum 11 Cards ergab,
ueberschoss auf der Eiche um mehr als das Doppelte. In `LEAF_STUFEN` steht
deshalb die gewuenschte **Stueckzahl**, und der Generator regelt die Dichte per
Bisektion dagegen ein (seed-stabil, der Seed wird nicht angefasst). Damit ist die
Kalibrierung preset-unabhaengig und der Test prueft die Anzahl **direkt**:
| Preset | S1 (Ziel 10) | S2 (Ziel 40) | S3 (Ziel 120) | S4 (voll) |
|---|---|---|---|---|
| baum | 9 · 518 Tris | 41 · 914 | 104 · 1436 | 130 · 1916 |
| eiche | 10 · 576 | 43 · 998 | 138 · 1872 | 463 · 4206 |
| birke | 9 · 570 | 36 · 1028 | 136 · 1952 | 207 · 2894 |
| tanne | 9 · 778 | 38 · 1160 | 125 · 2282 | 235 · 3302 |
S4 wird **nicht** geregelt — der Altbaum ist die Referenz, an der die Dichte
ueberhaupt eingestellt wurde.
**Deckung pro Stufe, nicht per Formel.** Card-Anteil und Groesse stehen als
Tabelle (`LEAF_STUFEN`). Der Dichte-Anteil bei S1/S2 sieht hoch aus — das
taeuscht: „Dichte" ist Punkte pro m² *Zweigflaeche*, und die waechst ueber die
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_tree_generator"
version = "1.33.0"
version = "1.34.0"
name = "Stylized Tree Generator"
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
maintainer = "D4rkst3r"
+3 -3
View File
@@ -50,17 +50,17 @@
<td>34.1KB</td>
</tr>
<tr>
<td><tt><a href="./stylized_tree_generator-1.33.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.33.0</a></tt></td>
<td><tt><a href="./stylized_tree_generator-1.34.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.34.0</a></tt></td>
<td>Stylized Tree Generator</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>4.2.0 - ~</td>
<td>all</td>
<td>all</td>
<td>38.5KB</td>
<td>40.0KB</td>
</tr>
</table>
<center><p>Built 2026-08-06, 08:08</p></center>
<center><p>Built 2026-08-06, 08:32</p></center>
</body>
</html>
+4 -4
View File
@@ -82,7 +82,7 @@
"id": "stylized_tree_generator",
"name": "Stylized Tree Generator",
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
"version": "1.33.0",
"version": "1.34.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -98,9 +98,9 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_tree_generator-1.33.0.zip",
"archive_size": 39431,
"archive_hash": "sha256:757cecfccbac8f1a50eadd62d47b9439d98550ad2f0677604ed6832610fd74f8"
"archive_url": "./stylized_tree_generator-1.34.0.zip",
"archive_size": 40964,
"archive_hash": "sha256:1cb548b5fef9bb79342ef92f7bc40f7ecb66e21f6356c8e00623ada5c842b0d6"
}
]
}
Binary file not shown.
Binary file not shown.
+141 -18
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 33, 0),
"version": (1, 34, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -1434,20 +1434,30 @@ def leaf_socket_ids(ng):
LEAF_RADIUS_YOUNG = 0.20
# Dichte-Anteil Groessen-Anteil
# Dichte Groesse Kronenansatz
# ZIEL-ANZAHL statt Dichte-Anteil.
#
# Dichte-Faktoren waren preset-abhaengig und mussten fuer jeden Baum neu
# geeicht werden: "Dichte" ist Punkte pro m2 Zweigflaeche, und die verteilt sich
# je Preset voellig anders. Ein Faktor, der am Standardbaum 11 Cards ergab,
# lieferte auf der Eiche mehr als das Doppelte - die Kompensation ueberschoss.
#
# Jetzt steht die gewuenschte Card-ANZAHL da, und der Generator regelt die
# Dichte per Bisektion dagegen ein. Das ist preset-unabhaengig, und der Test
# kann die Anzahl DIREKT pruefen statt ueber Dichte x Anteil x Flaeche zu
# rechnen. None = nicht regeln, volle Dichte (der Altbaum ist die Referenz,
# an der die Dichte ueberhaupt eingestellt wurde).
#
# Ziel-Cards Groesse Kronenansatz
LEAF_STUFEN = (
# S1/S2 Dichte nachgezogen, nachdem der Hoehen-Filter dazukam: er nimmt dem
# Setzling die Haelfte der Cards (9 -> 4), weil dessen Laub vorher ueber die
# ganze Stammhoehe verteilt war.
(13.0, 0.45, 0.40), # S1 - wenige kleine Bueschel, Fuss bleibt frei
(2.10, 0.63, 0.35), # S2
(1.00, 0.82, 0.30), # S3
(1.00, 1.00, 0.25), # S4 - Sicherheitsnetz, hier sitzt ohnehin nichts tief
(10, 0.45, 0.40), # S1 - wenige kleine Bueschel, Fuss bleibt frei
(40, 0.63, 0.35), # S2
(120, 0.82, 0.30), # S3
(None, 1.00, 0.25), # S4 - volle Dichte, keine Regelung
)
def _stufen_anteile(growth_t):
"""Dichte-, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt.
"""Ziel-Cardzahl, Groessen- und Kronenansatz-Wert fuer einen Wachstums-Fortschritt.
Die Stufen liegen bei t = 0 / 1/3 / 2/3 / 1 (vier Stufen). Zwischen den
Stuetzstellen wird linear gemischt, damit auch drei oder fuenf Stufen
@@ -1457,9 +1467,12 @@ def _stufen_anteile(growth_t):
x = max(0.0, min(1.0, growth_t)) * (n - 1)
i = min(int(x), n - 2)
f = x - i
d0, g0, h0 = LEAF_STUFEN[i]
d1, g1, h1 = LEAF_STUFEN[i + 1]
return (d0 + (d1 - d0) * f,
z0, g0, h0 = LEAF_STUFEN[i]
z1, g1, h1 = LEAF_STUFEN[i + 1]
# Groesse und Kronenansatz werden gemischt, die Ziel-ANZAHL nicht: sie ist
# eine Stueckzahl, kein Verlauf. Es gilt die naeher liegende Stuetzstelle.
ziel = z0 if f < 0.5 else z1
return (ziel,
g0 + (g1 - g0) * f,
h0 + (h1 - h0) * f)
@@ -1492,6 +1505,88 @@ def _modifier_anstossen(md):
md.show_viewport = sichtbar
def _cards_pro_stueck(card_obj, card_coll):
"""Wie viele Faces hat EINE Card? Fuer die Umrechnung Faces -> Cardzahl."""
quellen = []
if card_coll is not None:
quellen = [o for o in card_coll.objects if o.type == 'MESH']
elif card_obj is not None and card_obj.type == 'MESH':
quellen = [card_obj]
zahlen = [len(o.data.polygons) for o in quellen if o.data.polygons]
if not zahlen:
return 1
return max(1, int(round(sum(zahlen) / float(len(zahlen)))))
def _card_zahl(ob, pro_stueck, dg=None):
"""Anzahl gestreuter Cards auf einer ausgewerteten Blatt-Ebene."""
dg = dg or bpy.context.evaluated_depsgraph_get()
ev = ob.evaluated_get(dg)
me = ev.to_mesh()
n = len(me.polygons)
ev.to_mesh_clear()
return int(round(n / float(max(pro_stueck, 1))))
def _dichte_einregeln(context, ob, md, ids, ziel, start_dichte, pro_stueck,
schritte=14, toleranz=0.15):
"""Regelt die Dichte per Bisektion auf eine Ziel-Cardzahl ein.
Warum ueberhaupt regeln: "Dichte" ist Punkte pro m2 Zweigflaeche, und die
faellt je Preset voellig anders aus. Ein Faktor, der am Standardbaum passte,
ueberschoss auf der Eiche um mehr als das Doppelte. Eine Stueckzahl ist
dagegen preset-unabhaengig - und direkt pruefbar.
Seed-stabil: der Seed wird nicht angefasst, nur die Dichte. Gleicher Baum
und gleicher Seed ergeben deshalb dieselbe Verteilung.
"""
if not ziel or ziel <= 0:
return start_dichte, _card_zahl(ob, pro_stueck)
def _messen(dichte):
md[ids["Density"]] = float(dichte)
_modifier_anstossen(md)
context.view_layer.update()
return _card_zahl(ob, pro_stueck)
lo, hi = start_dichte, start_dichte
n = _messen(start_dichte)
if n == ziel:
return start_dichte, n
# Klammer suchen: verdoppeln bzw. halbieren, bis das Ziel eingeschlossen ist.
if n < ziel:
for _ in range(12):
hi = hi * 2.0
if _messen(hi) >= ziel:
break
lo = start_dichte
else:
for _ in range(12):
lo = lo * 0.5
if lo < 1e-3 or _messen(lo) <= ziel:
break
hi = start_dichte
bestes = (abs(n - ziel), start_dichte, n)
for _ in range(schritte):
mitte = (lo + hi) * 0.5
n = _messen(mitte)
if abs(n - ziel) < bestes[0]:
bestes = (abs(n - ziel), mitte, n)
if abs(n - ziel) <= max(1, ziel * toleranz):
return mitte, n
if n < ziel:
lo = mitte
else:
hi = mitte
# Beste gefundene Naeherung wiederherstellen.
_messen(bestes[1])
return bestes[1], bestes[2]
def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overrides):
"""Erzeugt/aktualisiert ein <Baum>_Leaf-Objekt mit dem Blattwerk-Modifier."""
name = name or (tree_obj.name + "_Leaf")
@@ -2219,9 +2314,23 @@ def _origin_bottom_center(ob):
ob.matrix_parent_inverse = Matrix.Identity(4)
if not me.vertices:
return Matrix.Identity(4)
xs = [v.co.x for v in me.vertices]
ys = [v.co.y for v in me.vertices]
zs = [v.co.z for v in me.vertices]
# Der Ursprung kommt aus der STAMM-Geometrie (Slot 0), nicht aus der
# Gesamt-Bbox. Sonst hebt eine einzige tief haengende Blatt-Card den ganzen
# Baum an und er steht in UE in der Luft - gemeldet am S1-Setzling, wo die
# Krone relativ zum kurzen Stamm weit ausladet.
stamm_verts = None
if len(me.materials) > 1:
idx = {vi for p in me.polygons if p.material_index == 0
for vi in p.vertices}
if idx:
stamm_verts = [me.vertices[i].co for i in idx]
if stamm_verts is None:
stamm_verts = [v.co for v in me.vertices]
xs = [c.x for c in stamm_verts]
ys = [c.y for c in stamm_verts]
zs = [c.z for c in stamm_verts]
versatz = Matrix.Translation((
-(min(xs) + max(xs)) * 0.5,
-(min(ys) + max(ys)) * 0.5,
@@ -2596,7 +2705,7 @@ class TREEGEN_OT_leaves(Operator):
# traegt kleinere Bueschel. Baeume ohne Wachstums-Stufe haben
# growth_t = 1.0 und bleiben unveraendert.
gt = float(t.get("growth_t", 1.0))
d_anteil, g_anteil, h_anteil = _stufen_anteile(gt)
ziel_cards, g_anteil, h_anteil = _stufen_anteile(gt)
# Der RADIUS-FILTER muss mit dem Baum schrumpfen. Er trennt
# duenne Zweige (Laub) von dickem Geaest (kein Laub) - aber ein
@@ -2613,12 +2722,26 @@ class TREEGEN_OT_leaves(Operator):
lf = make_leaves(context, t, s.leaf_card,
card_coll=s.leaf_cards,
Density=s.leaf_density * d_anteil,
Density=s.leaf_density,
Size=s.leaf_size * g_anteil,
**{"Size Var": s.leaf_size_var,
"Max Radius": s.leaf_max_radius * r_faktor,
"Min Height": h_anteil,
"Tilt": s.leaf_tilt, "Seed": s.seed})
# Dichte auf die ZIEL-CARDZAHL einregeln. Muss nach dem Anlegen
# passieren, weil dafuer gemessen werden muss, was tatsaechlich
# herauskommt - die Zweigflaeche faellt je Preset voellig
# anders aus.
if ziel_cards:
md = lf.modifiers.get("GN_Leaves")
ng = md.node_group if md is not None else None
if ng is not None:
pro = _cards_pro_stueck(s.leaf_card, s.leaf_cards)
dichte, ist = _dichte_einregeln(
context, lf, md, leaf_socket_ids(ng),
ziel_cards, s.leaf_density, pro)
print("[TreeGen] %s: %d Cards (Ziel %d) bei Dichte %.0f"
% (t.name, ist, ziel_cards, dichte))
except Exception as exc: # noqa: BLE001
self.report({'WARNING'}, "%s: %s" % (t.name, exc))
continue
+30 -2
View File
@@ -52,8 +52,8 @@ if not (130.0 <= s.leaf_density <= 200.0):
% s.leaf_density)
# Die Deckung steht als Tabelle pro Stufe, nicht als Formel. Card-ANZAHL muss
# ueber die Stufen steigen, Card-Groesse auch.
_d0, _g0, _h0 = tg._stufen_anteile(0.0)
_d1, _g1, _h1 = tg._stufen_anteile(1.0)
_z0, _g0, _h0 = tg._stufen_anteile(0.0)
_z1, _g1, _h1 = tg._stufen_anteile(1.0)
if _g0 >= _g1:
fails.append("Groessen-Anteil steigt nicht ueber die Stufen (%.2f -> %.2f)"
% (_g0, _g1))
@@ -275,6 +275,34 @@ 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]))
# --- 3c) Ziel-Cardzahl wird preset-UNABHAENGIG eingeregelt ---------------
# Frueher standen hier Dichte-Faktoren. Die mussten je Preset neu geeicht
# werden: "Dichte" ist Punkte pro m2 Zweigflaeche, und die verteilt sich je
# Baum voellig anders - ein Faktor, der am Standardbaum 11 Cards ergab,
# ueberschoss auf der Eiche um mehr als das Doppelte. Jetzt steht die
# Stueckzahl da und wird eingeregelt; der Test prueft sie DIREKT.
print("")
print("%-10s %8s %8s %8s %8s" % ("Preset", "S1", "S2", "S3", "S4"))
for pname in ("baum", "eiche", "birke"):
reihe = bauen(pname, wachstum=True, stufen=4)
pro = tg._cards_pro_stueck(card, None)
dg = bpy.context.evaluated_depsgraph_get()
zellen = []
for st in reihe:
bl = bpy.data.objects.get(st.name + "_Leaf")
n = tg._card_zahl(bl, pro, dg) if bl else 0
gt = float(st.get("growth_t", 1.0))
ziel, _, _ = tg._stufen_anteile(gt)
zellen.append((n, ziel))
if ziel:
# 25 % Toleranz: die Bisektion arbeitet auf einer diskreten
# Punktverteilung, exakt treffen kann sie nicht.
if abs(n - ziel) > max(2, ziel * 0.25):
fails.append("%s/%s: %d Cards, Ziel %d - die Regelung trifft "
"nicht" % (pname, st.name, n, ziel))
print("%-10s %8s %8s %8s %8s"
% (pname, *["%d/%s" % (n, z if z else "voll") for n, z in zellen]))
print("")
if fails:
print("ERGEBNIS: %d FEHLER" % len(fails))