Tree Gen 1.36.0: Ursprung an der Stammbasis, Weide gedeckelt, Birke ueberarbeitet
1) Ursprung: Stammbasis statt tiefstem Slot-0-Punkt Zweimal war das schon "behoben". Erst kam der Ursprung aus der Gesamt-Bbox (eine Blatt-Card hob den Baum an), dann aus Material-Slot 0 - aber die AESTE gehoeren ebenfalls zum Rinden-Slot. Gemessen an Tanne_01_S1: tiefster Slot-0-Punkt -5.35 cm, tiefster STAMM-Punkt -0.02 cm. Die Stammbasis schwebte 5.3 cm ueber dem Boden. Die exakte Trennung liefert ast_id (0 = Stamm) - dasselbe Attribut, das der Baum fuer die Faell-Teile ohnehin traegt. Ein Schwellwert auf den Achsabstand faellt beim Kaktus um: dessen Stamm ist dicker als jeder sinnvolle Schwellwert. X/Y kommt jetzt aus dem untersten RING statt aus der Stamm-Bbox; bei Bend 1.5 liegt die Bbox-Mitte in der Kruemmung. Neu: tests/test_ursprung.py, gemessen am reimportierten FBX. Die erste Fassung des Tests suchte den "tiefsten Punkt nahe der Achse" - die Gegenprobe hat sie umgeworfen, weil bei der Tanne eine Astspitze ins Achsband faellt. Ein Schwellwert, der die Bugfassung durchlaesst, prueft nichts. Jetzt wird ein Stammquerschnitt gesucht: Vertices, die die Achse in allen Himmelsrichtungen umschliessen. Gegenprobe bestanden. Nebenbefund: ein Baum ohne Aeste traegt gar kein ast_id (Branch Count 0 schaltet den Ast-Zweig des Graphen ab). Dann ist alles Stamm und der Fallback exakt. 2) Weide S4 auf 350 Cards gedeckelt - Entscheidung des Users Gemessen 5456-5594 Tris ueber drei Seeds gegen 4300 Budget; nach der Deckelung 3890-3962. LEAF_STUFEN_PRESET haelt solche Ausnahmen je Preset. 3) Birke war eine Saeule Gemessen B/H 0.15 - schmaler als der Standardbaum (0.22), nicht die Haelfte der Eiche (0.36). Ursache: Branch Up 1.1, die Aeste klebten am Stamm. Zielband 0.35-0.55 aus Van den Berk (8-12 m bei 15-20 m) und Woodland Trust (6-10 m bei 15-25 m); jetzt 0.51. Haengende Zweigenden ueber Branch Droop 0.8 + Droop Curve 3.5, nicht ueber negatives Sub Up: das wurde von +2.2 bis -2.0 durchgemessen und bewegt nichts (1-6 %), weil die Sub-Aeste zu kurz sind. Mit Droop 26 % bei einem Fall von 0.033 x Hoehe - wie die Tanne (0.030), weit weg von der Weide (0.213). 4) run_tests.ps1 benennt Blender-Abstuerze test_export_robust starb rund in jedem sechsten Lauf mit EXCEPTION_ACCESS_VIOLATION und der Runner meldete "<keine Ergebniszeile>". Der Stack enthaelt keinen einzigen Addon-Frame (RNA_property_int_get -> rna_property_rna_or_id_get -> IDP_GetPropertyFromGroup, Null-Deref), und mit eingebauten Markern verschwindet der Absturz: Heisenbug in Blender 5.1.2. Der Runner wiederholt jetzt einmal und benennt den Absturz. 24 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+119
-28
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "Stylized Tree Generator",
|
||||
"author": "D4rkst3r",
|
||||
"version": (1, 35, 0),
|
||||
"version": (1, 36, 0),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "View3D > Sidebar > Tree Gen",
|
||||
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
||||
@@ -159,13 +159,35 @@ PRESETS = {
|
||||
"Branch Bend": 0.9, "Branch Thickness": 0.24, "Sub Count": 3,
|
||||
"Sub Length": 0.8, "Root Flare": 1.5, "Root Lobe Depth": 0.4,
|
||||
},
|
||||
"birke": { # schlank, hoher Ansatz, feine Zweige
|
||||
# Birke: schlanker Stamm, aber KEINE Saeule. Der vorherige Satz war eine -
|
||||
# gemessen Breite/Hoehe 0.15, weniger als der Standardbaum (0.22) und nicht
|
||||
# einmal die Haelfte der Eiche (0.36). Ursache war "Branch Up" 1.1: die
|
||||
# Aeste zeigten fast senkrecht nach oben und klebten am Stamm.
|
||||
#
|
||||
# Zielband aus zwei Baumschul-/Fachquellen, die sich ueberlappen:
|
||||
# Van den Berk nennt fuer Betula pendula 15-20 m Hoehe bei 8-12 m
|
||||
# Kronenbreite (B/H 0.40-0.80), der Woodland Trust 15-25 m bei 6-10 m
|
||||
# (0.24-0.67). Schnittmenge also rund 0.35-0.55; gemessen jetzt 0.45.
|
||||
#
|
||||
# Der Habitus steht bei Van den Berk woertlich: "spreading branches have
|
||||
# clearly pendulous and thin twigs", Krone "irregularly oval to oblong,
|
||||
# half-open". Das ist der Palmen-Bogen im Kleinen - der Ast steigt erst und
|
||||
# kippt dann ab -, deshalb Branch Droop mit spaeter Droop Curve statt
|
||||
# negativem "Sub Up". Sub Up wurde durchgemessen und bewegt hier nichts:
|
||||
# von +2.2 bis -2.0 blieb der Anteil haengender Vertices bei 1-6 %, weil
|
||||
# die Sub-Aeste zu kurz sind, um die Silhouette zu tragen. Mit Droop 0.8
|
||||
# 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.
|
||||
"birke": {
|
||||
"Height": 7.5, "Trunk Radius": 0.09, "Bend": 0.7, "Taper": 0.92,
|
||||
"Branch Count": 12, "Branch Length": 1.4, "Branch Up": 1.1,
|
||||
"Branch Start": 0.45, "Branch End": 0.98,
|
||||
"Branch Count": 16, "Branch Length": 2.1, "Branch Up": 0.55,
|
||||
"Branch Start": 0.38, "Branch End": 0.98,
|
||||
"Crown Taper": 0.1, "Crown Bulge": 0.25,
|
||||
"Attraction Up": 0.15, # spreizen, nicht hochschwingen
|
||||
"Branch Droop": 0.8, # Zweigenden kippen ab ...
|
||||
"Droop Curve": 3.5, # ... erst im aeusseren Drittel
|
||||
"Branch Bend": 0.8, "Branch Thickness": 0.11, "Sub Count": 3,
|
||||
"Sub Length": 0.5, "Root Flare": 0.6,
|
||||
"Sub Length": 0.7, "Root Flare": 0.6,
|
||||
},
|
||||
"weide": { # haengende Zweige (Droop positiv), breit ausladend
|
||||
"Height": 5.0, "Trunk Radius": 0.18, "Bend": 0.8, "Taper": 0.8,
|
||||
@@ -1245,6 +1267,10 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None):
|
||||
# spaeter und getrennt - er kann sonst nicht wissen, ob er einen Setzling
|
||||
# oder einen Altbaum vor sich hat. Aus dem Namen zu lesen waere bruechig.
|
||||
ob["growth_t"] = 1.0 if growth is None else max(0.0, min(1.0, float(growth)))
|
||||
# Das Preset ebenso: einzelne Arten brauchen eine eigene Ziel-Cardzahl
|
||||
# (siehe LEAF_STUFEN_PRESET), und der Blattwerk-Operator sieht spaeter nur
|
||||
# noch das fertige Objekt.
|
||||
ob["preset"] = preset
|
||||
|
||||
bpy.context.view_layer.objects.active = ob
|
||||
return ob
|
||||
@@ -1523,20 +1549,42 @@ LEAF_STUFEN = (
|
||||
(None, 1.00, 0.25), # S4 - volle Dichte, keine Regelung
|
||||
)
|
||||
|
||||
# AUSNAHMEN je Preset: {preset: {stufen_index: ziel_cards}}.
|
||||
#
|
||||
# "Volle Dichte" auf S4 heisst: das Ergebnis haengt an der Zweigflaeche, und die
|
||||
# faellt je Art verschieden aus. Die WEIDE traegt die meiste - haengende Zweige
|
||||
# mit Sub Length 0.9 und Sub Count 3 - und landete deshalb gemessen bei
|
||||
# 5456/5558/5594 Tris ueber drei Seeds, gegen ein Stufenbudget von 4300.
|
||||
# Deckelung auf 350 Cards: gemessen 361 Cards / 3818 Tris, damit im Budget.
|
||||
#
|
||||
# Das macht die Tabelle preset-abhaengig - bewusst so entschieden, statt das
|
||||
# Budget fuer alle anzuheben: die Weide ist der Ausreisser, nicht die Regel.
|
||||
LEAF_STUFEN_PRESET = {
|
||||
"weide": {3: 350},
|
||||
}
|
||||
|
||||
def _stufen_anteile(growth_t):
|
||||
|
||||
def _stufen_anteile(growth_t, preset=None):
|
||||
"""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
|
||||
sinnvolle Werte bekommen.
|
||||
|
||||
`preset` zieht die Ausnahmen aus LEAF_STUFEN_PRESET; ohne Angabe gilt die
|
||||
allgemeine Tabelle.
|
||||
"""
|
||||
aus = LEAF_STUFEN_PRESET.get(preset or "", {})
|
||||
|
||||
def _ziel(i):
|
||||
return aus.get(i, LEAF_STUFEN[i][0])
|
||||
|
||||
n = len(LEAF_STUFEN)
|
||||
x = max(0.0, min(1.0, growth_t)) * (n - 1)
|
||||
i = min(int(x), n - 2)
|
||||
f = x - i
|
||||
z0, g0, h0 = LEAF_STUFEN[i]
|
||||
z1, g1, h1 = LEAF_STUFEN[i + 1]
|
||||
z0, g0, h0 = _ziel(i), LEAF_STUFEN[i][1], LEAF_STUFEN[i][2]
|
||||
z1, g1, h1 = _ziel(i + 1), LEAF_STUFEN[i + 1][1], LEAF_STUFEN[i + 1][2]
|
||||
# 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
|
||||
@@ -2369,6 +2417,36 @@ def _deselect(context):
|
||||
pass
|
||||
|
||||
|
||||
def _stamm_koordinaten(me):
|
||||
"""Koordinaten der reinen STAMM-Vertices, mit absteigender Genauigkeit.
|
||||
|
||||
1. "ast_id" == 0 - exakt, kommt aus dem Generator selbst.
|
||||
2. Material-Slot 0 - Rinde inklusive Aeste, immerhin ohne Blatt-Cards.
|
||||
3. alles - wenn das Mesh weder Attribut noch zweiten Slot hat.
|
||||
|
||||
Stufe 2 und 3 sind Notnaegel fuer fremde oder vereinfachte Meshes; jede
|
||||
Stufe weiter unten ist ungenauer, aber keine faellt aus.
|
||||
"""
|
||||
slot0 = 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:
|
||||
slot0 = idx
|
||||
|
||||
attr = me.attributes.get("ast_id")
|
||||
if attr is not None and attr.domain == 'POINT' and len(attr.data) == len(me.vertices):
|
||||
idx = {i for i in range(len(me.vertices)) if attr.data[i].value == 0}
|
||||
if slot0 is not None:
|
||||
idx &= slot0
|
||||
if idx:
|
||||
return [me.vertices[i].co for i in idx]
|
||||
|
||||
if slot0:
|
||||
return [me.vertices[i].co for i in slot0]
|
||||
return [v.co for v in me.vertices]
|
||||
|
||||
|
||||
def _origin_bottom_center(ob):
|
||||
"""Ursprung auf Bbox-Unterseite Mitte - identisch zur Fels-Pipeline.
|
||||
|
||||
@@ -2383,27 +2461,39 @@ def _origin_bottom_center(ob):
|
||||
if not me.vertices:
|
||||
return Matrix.Identity(4)
|
||||
|
||||
# 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]
|
||||
# Der Ursprung kommt aus der STAMMBASIS, nicht aus einer Bbox.
|
||||
#
|
||||
# Zwei Stufen dieses Fehlers gab es schon. Zuerst die Gesamt-Bbox: eine
|
||||
# einzige tief haengende Blatt-Card hob den ganzen Baum an. Dann Slot 0 -
|
||||
# aber zum Rinden-Slot gehoeren auch die AESTE. Gemessen an Tanne_01_S1
|
||||
# (haengende Aeste ab 6 % Stammhoehe): der tiefste Slot-0-Punkt lag bei
|
||||
# -5.35 cm, der tiefste Stammpunkt bei -0.02 cm. Die Stammbasis schwebte
|
||||
# also 5.3 cm ueber dem Boden, weil eine Astspitze den Nullpunkt bestimmte.
|
||||
#
|
||||
# Die exakte Trennung liefert das Attribut "ast_id" (0 = Stamm), das der
|
||||
# Baum ohnehin fuer die Faell-Teile traegt. Ein Schwellwert auf den
|
||||
# Achsabstand waere die naheliegende Alternative und faellt beim Kaktus um:
|
||||
# dessen Stamm ist dicker als jeder sinnvolle Schwellwert.
|
||||
stamm_verts = _stamm_koordinaten(me)
|
||||
|
||||
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,
|
||||
-min(zs),
|
||||
))
|
||||
z_fuss = min(zs)
|
||||
# X/Y aus dem UNTERSTEN RING, nicht aus der Bbox des ganzen Stammes: ein
|
||||
# gekruemmter Stamm (Bend 1.5 beim toten Baum) hat seine Bbox-Mitte
|
||||
# irgendwo in der Kruemmung, nicht am Fuss. Der Schwerpunkt des Ringes ist
|
||||
# die Stammachse.
|
||||
hoehe = max(zs) - z_fuss
|
||||
band = [c for c in stamm_verts if c.z <= z_fuss + max(1e-4, hoehe * 0.02)]
|
||||
if band:
|
||||
mx = sum(c.x for c in band) / len(band)
|
||||
my = sum(c.y for c in band) / len(band)
|
||||
else:
|
||||
xs = [c.x for c in stamm_verts]
|
||||
ys = [c.y for c in stamm_verts]
|
||||
mx = (min(xs) + max(xs)) * 0.5
|
||||
my = (min(ys) + max(ys)) * 0.5
|
||||
|
||||
versatz = Matrix.Translation((-mx, -my, -z_fuss))
|
||||
me.transform(versatz)
|
||||
return versatz
|
||||
|
||||
@@ -2773,7 +2863,8 @@ 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))
|
||||
ziel_cards, g_anteil, h_anteil = _stufen_anteile(gt)
|
||||
ziel_cards, g_anteil, h_anteil = _stufen_anteile(
|
||||
gt, t.get("preset"))
|
||||
|
||||
# Der RADIUS-FILTER muss mit dem Baum schrumpfen. Er trennt
|
||||
# duenne Zweige (Laub) von dickem Geaest (kein Laub) - aber ein
|
||||
|
||||
Reference in New Issue
Block a user