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:
+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
|
||||
|
||||
Reference in New Issue
Block a user