Tree Gen 1.30.0: Bueschel-Defaults, Wachstums-Kopplung, Stufen-Budget

Blatt-Defaults auf die Bueschel-Atlas-Aera umgestellt. Eine Card ist ein ganzer
Zweigbueschel, kein einzelnes Blatt - die alten Werte streuten hunderte winziger
Cards, wo ein paar Dutzend grosse hingehoeren. Werte an einer S4-Eiche in UE
gemessen und abgenommen:

    Groesse      0.22 -> 0.85
    Dichte        500 -> 170
    Max Radius   0.02 -> 0.04     <- der Volumen-Hebel
    Kippung       0.5 -> 0.9

Max Radius ist der unscheinbare Held: bei 0.02 trugen nur die aeussersten
Zweigspitzen Laub, die Krone hatte nur Kontur und war innen nackt.

Bueschelgroesse haengt jetzt am Wachstum: make_object merkt growth_t am Objekt
(aus dem Namen zu lesen waere bruechig), der Blattwerk-Operator skaliert die
Cards damit. Gemessen ueber vier Stufen: Card-Kante 0.19/0.28/0.37/0.45.

Falle dabei: "Dichte" ist Punkte pro m2 Zweigflaeche, und ein Setzling hat davon
fast keine - bei Stufe 1 kamen exakt NULL Cards heraus, der Setzling stand kahl
da. Da die Bueschel um denselben Faktor kleiner sind, deckt jeder nur faktor^2
der Flaeche; die Dichte gegenlaeufig anzuheben haelt die Blattdeckung gleich.

Tri-Budget PRO STUFE (ASSETS.md Teil 6): S1 <= 800, S2 <= 1200, S3/S4 <= 4000.
Bewusst Stufen und keine Interpolation; die Schwellen greifen auch bei drei
Stufen richtig (t = 0 / 0.5 / 1.0). Gemessen 494 / 878 / 1032 / 1396 - alle im
Rahmen.

WICHTIGSTE FALLE, von der UE-Seite gemeldet und hier nachgestellt:
md[socket] = wert loest KEINE Neuauswertung des GN-Modifiers aus. Die
Blattebene blieb bei 118 Faces stehen, obwohl die Dichte von 90 auf 400
geschrieben war - auch view_layer.update() half nicht, erst ein
show_viewport-Toggle. Der Knopf-Weg funktionierte bisher nur ZUFAELLIG, weil
make_leaves jedes Mal auch die Objekt-Zeiger neu schreibt und das Aendern eines
ID-Zeigers die Depsgraph-Beziehungen umbaut. Wer das spaeter optimiert ("nur
geaenderte Werte schreiben"), haette den Fehler still zurueck.
_modifier_anstossen() macht es jetzt explizit - in make_leaves UND make_object.

Nebenbei: der Variantentest rundete Card-Flaechen auf 5 Stellen; seit die
Kippung bei 0.9 steht streuen sie in der sechsten und eine einzige Card-Sorte
zerfiel in vier Eimer.

Neu: tests/test_blatt_bueschel.py. 23 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 08:23:27 +02:00
co-authored by Claude Opus 4.8
parent 914a1af967
commit b1fdad8106
9 changed files with 387 additions and 26 deletions
+219
View File
@@ -0,0 +1,219 @@
"""Blatt-Defaults fuer BUESCHEL-Atlanten (eine Card = ein ganzer Zweigbueschel).
Die alten Werte (Size 0.22, Density 150-500) stammen aus der Blaettchen-Aera:
damit streute der Generator hunderte winziger Cards, wo ein paar Dutzend grosse
hingehoeren. Diese Datei nagelt die neuen Groessenordnungen fest und prueft die
Kopplung an die Wachstums-Stufen.
Aufruf: blender --background --factory-startup --python tests/test_blatt_bueschel.py
"""
import os
import sys
import math
import importlib.util
import bpy
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
spec = importlib.util.spec_from_file_location(
"tg", os.path.join(ROOT, "stylized_tree_generator.py"))
tg = importlib.util.module_from_spec(spec)
sys.modules["tg"] = tg
spec.loader.exec_module(tg)
tg.register()
fails = []
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
me = bpy.data.meshes.new("Leaf_Card")
me.from_pydata([(-0.5, 0, 0), (0, 0, 0.7), (0.5, 0, 0), (0, 0, -0.1)],
[], [(0, 1, 2), (0, 2, 3)])
me.update()
card = bpy.data.objects.new("Leaf_Card", me)
bpy.context.scene.collection.objects.link(card)
s = bpy.context.scene.tree_gen_settings
# --- 1) Auslieferungszustand ---------------------------------------------
print("Defaults: Dichte %.0f, Groesse %.2f, Setzlings-Anteil %.2f"
% (s.leaf_density, s.leaf_size, s.leaf_size_growth))
if not (0.6 <= s.leaf_size <= 1.1):
fails.append("leaf_size ist %.2f - fuer Bueschel-Atlanten werden ~0.85 "
"gebraucht, 0.2 waere ein einzelnes Blaettchen" % s.leaf_size)
if s.leaf_budget_s1 > 800 or s.leaf_budget_s2 > 1200 or s.leaf_budget > 4000:
fails.append("Stufen-Budgets (%d/%d/%d) ueber ASSETS.md Teil 6 (800/1200/4000)"
% (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget))
if not (130.0 <= s.leaf_density <= 200.0):
fails.append("leaf_density ist %.0f - der Sweetspot liegt bei 130-170"
% s.leaf_density)
# Der Volumen-Hebel: bei 0.02 trugen nur die aeussersten Zweigspitzen Laub und
# die Krone war innen nackt.
if s.leaf_max_radius < 0.035:
fails.append("leaf_max_radius ist %.3f - unter 0.035 tragen nur die "
"Zweigspitzen Laub und die Krone bleibt innen nackt"
% s.leaf_max_radius)
if s.leaf_tilt < 0.7:
fails.append("leaf_tilt ist %.2f - unter 0.7 fehlt das Rotations-Chaos, "
"das die Sichtluecken schliesst" % s.leaf_tilt)
if abs(tg.LEAF_DEFAULTS["Size"] - s.leaf_size) > 0.2:
fails.append("LEAF_DEFAULTS['Size'] (%.2f) und die UI (%.2f) laufen "
"auseinander" % (tg.LEAF_DEFAULTS["Size"], s.leaf_size))
def bauen(preset, wachstum=False, stufen=4):
for o in list(bpy.context.scene.objects):
if o is not card:
bpy.data.objects.remove(o, do_unlink=True)
s.preset = preset
s.art_name = preset.capitalize()
s.variant = 1
s.apply_modifier = True
s.use_growth = wachstum
s.stages = stufen
s.leaf_card = card
bpy.ops.object.treegen_create()
staemme = sorted((o for o in bpy.context.scene.objects
if o.type == 'MESH' and o is not card
and not o.name.endswith(("_Leaf", "_Frucht"))),
key=lambda o: o.name)
for o in bpy.context.scene.objects:
o.select_set(o in staemme)
bpy.context.view_layer.objects.active = staemme[0]
bpy.ops.object.treegen_leaves()
return staemme
def messen(stamm):
bl = bpy.data.objects.get(stamm.name + "_Leaf")
if bl is None:
return 0, 0.0, 0
dg = bpy.context.evaluated_depsgraph_get()
ev = bl.evaluated_get(dg)
m = ev.to_mesh()
cards = len(m.polygons) // 2
kante = math.sqrt(sum(p.area for p in m.polygons) / cards) if cards else 0.0
ev.to_mesh_clear()
ev2 = stamm.evaluated_get(dg)
m2 = ev2.to_mesh()
tris = sum(len(p.vertices) - 2 for p in m2.polygons) + cards * 2
ev2.to_mesh_clear()
return cards, kante, tris
# --- 2) Alle Presets: Card-Zahl plausibel, Budget gehalten ---------------
print("")
# BUDGET: ASSETS.md nennt 2500 Tris fuer Baum (Stamm + Krone). Mit
# Bueschel-Atlanten liegt LOD0 darueber - das ist eine bewusste Entscheidung der
# Spielseite: der dichtere LOD0 traegt den Look aus der Naehe, und die LOD-Kette
# dampft ihn auf 16 % ein. Die harte Grenze liegt deshalb hier bei 3000; alles
# zwischen 2500 und 3000 wird als Hinweis ausgegeben, damit es nicht unbemerkt
# weiterwaechst.
# Die urspruenglichen 2500 waren eine konservative Setzung. Bei ISM-Instanzen
# mit LOD-Kette sind Roh-Dreiecke nicht der Engpass, sondern der OVERDRAW der
# Cards (die GPU zahlt pro Bildschirmflaeche, nicht pro Dreieck) und die
# Schattenpaesse. Genau deshalb sind 170 grosse Bueschel guenstiger als 500
# kleine - bei gleichem Tri-Zaehler. Die Zahl hier ist die Hartgrenze fuer die
# ausgewachsene Stufe; die endgueltige Freigabe kommt aus einer Messung im
# Level (Testwald, stat unit / stat gpu), nicht aus diesem Test.
LOD0_HART = s.leaf_budget
LOD0_HINWEIS = int(s.leaf_budget * 0.8)
ueber = []
print("%-12s %6s %11s %9s" % ("Preset", "Cards", "Card-Kante", "Tris ges"))
for p in tg.PRESETS:
staemme = bauen(p)
cards, kante, tris = messen(staemme[0])
marke = ""
if tris > LOD0_HART:
marke = "UEBER HARTGRENZE"
elif tris > LOD0_HINWEIS:
marke = "ueber 2500 (LOD0 ok)"
ueber.append((p, tris))
print("%-12s %6d %11.2f %9d %s" % (p, cards, kante, tris, marke))
if tris > LOD0_HART:
fails.append("%s: %d Tris ueber der LOD0-Hartgrenze von %d"
% (p, tris, LOD0_HART))
if p == "kaktus":
continue # kein Ast duenner als Max Radius - richtig so
if cards == 0:
fails.append("%s: keine Cards gestreut" % p)
# Ob es Bueschel oder Blaettchen sind, entscheidet die GROESSE, nicht die
# Anzahl: ein grosser Baum darf 600 Bueschel tragen. Die Menge deckelt das
# Tri-Budget oben ohnehin.
elif kante < 0.30:
fails.append("%s: Card-Kante nur %.2f m - das sind Blaettchen, keine "
"Bueschel" % (p, kante))
if ueber:
print("Hinweis: %s liegen ueber dem Stufenbudget - fuer LOD0 abgesegnet, bei "
"Bedarf ist die Dichte der Feinregler (130-170)."
% ", ".join("%s (%d)" % (p, t) for p, t in ueber))
# --- 3) Wachstum: Bueschel wachsen mit, Setzling ist NICHT kahl ----------
staemme = bauen("baum", wachstum=True, stufen=4)
print("")
print("%-18s %6s %6s %11s" % ("Stufe", "t", "Cards", "Card-Kante"))
kanten, zahlen = [], []
for st in staemme:
cards, kante, _ = messen(st)
gt = float(st.get("growth_t", 1.0))
print("%-18s %6.2f %6d %11.2f" % (st.name, gt, cards, kante))
kanten.append(kante)
zahlen.append(cards)
if cards == 0:
# Genau das war der Fehler: "Dichte" ist Punkte pro m2 Zweigflaeche,
# und ein Setzling hat davon fast keine.
fails.append("%s: keine Cards - ein Setzling darf nicht kahl sein"
% st.name)
for i in range(1, len(kanten)):
if kanten[i] <= kanten[i - 1]:
fails.append("Stufe %d hat Card-Kante %.2f, Stufe %d hatte %.2f - die "
"Bueschel wachsen nicht mit"
% (i + 1, kanten[i], i, kanten[i - 1]))
if kanten and kanten[0] > 0:
anteil = kanten[0] / kanten[-1]
print("Setzlings-Bueschel: %.0f%% der Endgroesse (Regler steht auf %.0f%%)"
% (100 * anteil, 100 * s.leaf_size_growth))
# Der gemessene Anteil darf nicht ueber dem Regler liegen - dann haette die
# Skalierung nicht gegriffen.
if anteil > s.leaf_size_growth + 0.15:
fails.append("Setzlings-Bueschel sind %.0f%% der Endgroesse, der Regler "
"steht auf %.0f%% - die Wachstums-Kopplung greift nicht"
% (100 * anteil, 100 * s.leaf_size_growth))
# --- 3b) Budget je Stufe statt global -----------------------------------
print("")
print("%-18s %6s %8s %8s" % ("Stufe", "t", "Tris", "Budget"))
for st in staemme:
cards, _, tris = messen(st)
gt = float(st.get("growth_t", 1.0))
budget = tg._stufen_budget(s, gt)
print("%-18s %6.2f %8d %8d %s"
% (st.name, gt, tris, budget, "UEBER" if tris > budget else ""))
if tris > budget:
fails.append("%s: %d Tris ueber dem Stufenbudget %d - ein Setzling mit "
"Altbaum-Dichte ist Verschwendung" % (st.name, tris, budget))
# Die Budgets muessen nach oben gehen, sonst ist die Staffelung sinnlos.
if not (s.leaf_budget_s1 < s.leaf_budget_s2 <= s.leaf_budget):
fails.append("Stufen-Budgets steigen nicht: %d / %d / %d"
% (s.leaf_budget_s1, s.leaf_budget_s2, s.leaf_budget))
# --- 4) Ohne Wachstum bleibt alles unveraendert -------------------------
staemme = bauen("baum", wachstum=False)
cards_ohne, kante_ohne, _ = messen(staemme[0])
print("")
print("Ohne Wachstum: %d Cards, Kante %.2f (growth_t=%.2f)"
% (cards_ohne, kante_ohne, float(staemme[0].get("growth_t", 1.0))))
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]))
print("")
if fails:
print("ERGEBNIS: %d FEHLER" % len(fails))
for x in fails:
print(" - " + x)
sys.exit(1)
print("ERGEBNIS: ALLE CHECKS OK")