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