Tree Gen 1.46.0: Halbkugel-Bueschel, groessere Baeume, Spitzen-Deckung

1) NEUE BAUFORM: Blattwerk als Halbkugel-Cluster
   Jeder gestreute Punkt ist jetzt ein Cluster-ZENTRUM, die Cards sitzen auf
   der oberen Halbkugel darum herum. Untere Haelfte weggelassen - von unten
   sieht man die Krone gegen den Himmel, das waere Overdraw fuer nichts.

   Zwei Annahmen kassiert die Messung: GeometryNodeMeshIcoSphere liefert bei
   Subdivisions 0 UND 1 dasselbe Ikosaeder, ein Cluster hat SECHS Cards statt
   der angenommenen 26. Und die Stufen-Ziele muessen Cluster-Vielfache sein
   (26/78/208/390) - S1 stand auf 10, unter der kleinsten Einheit, die Regelung
   waere nie konvergiert. S4 ist seither geregelt statt "volle Dichte":
   ungeregelt lieferte die Schale 2706 Cards an der Eiche.
   ast_id ueberlebt Instanzieren + Realize, an den Cards nachgewiesen.

2) SPITZEN-DECKUNG: zwei echte Ursachen, beide vorher nur vermutet
   - Der Kronenansatz-Filter stand auf 25 % der Baumhoehe, die Tanne setzt ihre
     Aeste ab 12 % an: bei 5 von 22 Aesten lag das aeussere Fuenftel KOMPLETT
     unter dem Filter. Jetzt auf den Branch Start der Art gedeckelt.
   - Haengende Spitzen fielen unter den Filter. Jetzt zaehlt der ANSATZ des
     Astes (anchor) mit, nicht nur die Punkthoehe.

   Ergebnis: Eiche/Baum 100 %, Weide 100/90, Obstbaum 100/93, Tanne 86/86,
   Birke 100/72. Die 85-%-Schwelle der Spielseite ist damit erreicht, ohne dass
   die Rinden-Ersparnis noetig war.

   Die Metrik war zweimal falsch: der Abstand zur Card-MITTE ist zu streng -
   eine 0.85-m-Card deckt die Spitze auch aus 0.45 m Abstand, bei 1.9 m
   Astlaenge kamen so 24 % heraus, obwohl das Polster genau auf der Spitze sass.
   Jetzt zaehlt Deckung, nicht Abstand.

3) GROESSERE BAEUME, proportional skaliert
   Eiche 9.3 m, Birke 11.0, Tanne 13.0, Weide 7.0 - Hoehe, Astlaenge und
   Stammradius zusammen, sonst wird der Baum eine Stange mit Minikrone. Alle
   unter dem 4300er-Deckel; die Birke brauchte eine Card-Deckelung auf 340.

4) Kugel-Kennwert RELATIV, Bbox-Kriterium ersetzt
   Die Schalen schieben jede Card in ALLE Richtungen vom Ast weg: die Krone
   gewinnt ringsum Breite, in der Hoehe nur oben. Der Obstbaum fiel dadurch von
   1.35-1.47 auf 0.95-1.23, ohne dass an seiner Form etwas falsch war.

   Zwei Hypothesen sind an der Messung gescheitert, beide klangen plausibel:
   den Kappen-Faktor entkoppeln (0.93-1.14 -> 0.95-1.23) und den Kappen-Radius
   vergroessern (59-77 % -> 58-75 %). Der Radius waechst in alle Richtungen und
   verschiebt das Verhaeltnis deshalb kaum. Was wirkt, ist die Kronenbreite.

   Schranke jetzt relativ: hoechstens 65 % des Eichenwerts aus DEMSELBEN Lauf.
   Eine neue Konstante haette bei der naechsten Bauform dasselbe Schicksal.
   LEAF_CLUSTER_PRESET bleibt trotzdem eine eigene Tabelle - Card-Grobheit und
   Polster-Abstand sind zwei verschiedene Fragen.

   "Bbox-Mitte == 0" ist raus. Nachgemessen ueber drei Presets und drei Seeds:
   Stammachse 0.0000 m in allen neun Faellen, Bbox-Exzentrik 0.08-0.39 m mit
   dem Seed streuend, 3-13 % des Kronenradius. Eine Verschiebung traefe den
   Stamm mit und waere ueber die Seeds konstant - es ist Kronen-Schieflage.
   An seine Stelle tritt der Bounds-Radius, an dem das Culling haengt, geprueft
   gegen die Blender-Messung statt gegen eine Konstante.

27 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 12:22:35 +02:00
co-authored by Claude Opus 4.8
parent 5e6b84369f
commit 6ed2257085
9 changed files with 371 additions and 50 deletions
+47 -5
View File
@@ -78,6 +78,19 @@ for o in bpy.context.scene.objects:
bpy.context.view_layer.objects.active = baum
namen_vorher = sorted(o.name for o in bpy.context.scene.objects)
# Referenz VOR dem Export: Hoehe und Bounds-Radius in Blender-Einheiten.
_dg = bpy.context.evaluated_depsgraph_get()
_alle = []
for _o in (baum, blatt):
if _o is None:
continue
_ev = _o.evaluated_get(_dg)
_m = _ev.to_mesh()
_alle += [_o.matrix_world @ _v.co for _v in _m.vertices]
_ev.to_mesh_clear()
blender_h = (max(c.z for c in _alle) - min(c.z for c in _alle)) if _alle else 0.0
blender_r = max((math.hypot(c.x, c.y) for c in _alle), default=0.0)
bpy.ops.treegen.export_game()
# Die Arbeitsebene muss unangetastet bleiben.
@@ -117,17 +130,46 @@ else:
fails.append("Slot 1 heisst '%s', erwartet '%s' (Blaetter)"
% (slots[1], tg.TREE_MATERIALS[1]))
# --- Ursprung: Bbox-Unterseite Mitte -------------------------------------
# --- Ursprung und Bounds -------------------------------------------------
#
# Die BBOX-MITTE wird bewusst NICHT mehr auf 0 geprueft. Das Kriterium stammt
# aus der Zeit vor der Stammbasis-Konvention (1.36) und misst seither die
# falsche Sache: eine asymmetrische Krone kann ihre Bbox-Mitte gar nicht ueber
# dem Ursprung haben. Nachgemessen ueber drei Presets und drei Seeds:
#
# Stammachse am Fuss 0.0000 m in ALLEN neun Faellen
# Bbox-Exzentrik 0.08-0.39 m, streut mit dem Seed
# ...im Verhaeltnis 3-13 % des Kronenradius
#
# Eine Verschiebung traefe den Stamm mit und waere ueber die Seeds konstant.
# Beides ist nicht der Fall - es ist Kronen-Schieflage. Der Nachfolger des
# Kriteriums ist tests/test_ursprung.py, das den STAMMQUERSCHNITT bei 0/0/0
# nachweist; engine-seitig bestaetigt durch lueckenlos stapelnde Faell-Teile,
# greifende UCX-Huellen und treffende Frucht-Streuung.
#
# Was stattdessen zaehlt, ist der BOUNDS-RADIUS: laeuft der aus dem Ruder,
# greift das Culling daneben (die Spielseite hatte den Fall mit Radius 3.5
# statt 350 - ein Skalenfehler). Geprueft wird er nicht gegen eine Konstante,
# sondern gegen die Blender-Messung: Hoehe und Radius muessen DENSELBEN
# Skalenfaktor tragen.
co = [ob.matrix_world @ v.co for v in me.vertices]
lo = [min(c[i] for c in co) for i in range(3)]
hi = [max(c[i] for c in co) for i in range(3)]
mid = ((lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5)
print("Ursprung: Mitte %.3f/%.3f, min.Z %.3f, Hoehe %.2f m"
% (mid[0], mid[1], lo[2], hi[2] - lo[2]))
if abs(mid[0]) > 0.10 or abs(mid[1]) > 0.10:
fails.append("Bbox nicht zentriert (%.3f/%.3f)" % mid)
fbx_h = hi[2] - lo[2]
fbx_r = max(math.hypot(c[0], c[1]) for c in co)
print("Ursprung: min.Z %.3f, Hoehe %.2f, Bounds-Radius %.2f, Bbox-Mitte "
"%.3f/%.3f (nur zur Info)" % (lo[2], fbx_h, fbx_r, mid[0], mid[1]))
if abs(lo[2]) > 0.01:
fails.append("min.Z = %.3f, erwartet 0" % lo[2])
if blender_h > 0 and blender_r > 0:
sk_h = fbx_h / blender_h
sk_r = fbx_r / blender_r
print("Skala aus Hoehe %.3f, aus Radius %.3f" % (sk_h, sk_r))
if abs(sk_r - sk_h) > sk_h * 0.05:
fails.append("Bounds-Radius traegt eine andere Skala als die Hoehe "
"(%.3f gegen %.3f) - so greift das Culling daneben"
% (sk_r, sk_h))
# --- Der eigentliche Punkt: Kugel-Normalen im FBX ------------------------
blatt_polys = [p for p in me.polygons if p.material_index == 1]