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:
@@ -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")
|
||||
@@ -77,7 +77,10 @@ def streuen(collection, einzelcard, seed):
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
ev = bl.evaluated_get(dg)
|
||||
me = ev.to_mesh()
|
||||
flaechen = sorted({round(p.area, 5) for p in me.polygons})
|
||||
# Auf 4 Stellen runden, nicht auf 5: die Cards werden gedreht, und seit die
|
||||
# Kippung bei 0.9 steht streuen die Flaechen in der sechsten Stelle. Mit
|
||||
# 5 Stellen zerfiel eine einzige Card-Sorte in vier Eimer.
|
||||
flaechen = sorted({round(p.area, 4) for p in me.polygons})
|
||||
n = len(me.polygons)
|
||||
ev.to_mesh_clear()
|
||||
return n, flaechen
|
||||
|
||||
Reference in New Issue
Block a user