Drei Befunde aus der Turnhalle abgearbeitet. 1) Deckung PRO STUFE statt globaler Formel Die alte Rechnung (Dichte durch faktor^2) hielt den Setzling vor der Kahlheit, gab ihm aber die Deckung eines Altbaums. Jetzt Tabelle LEAF_STUFEN. S4 traegt die volle Dichte 170, S2 liegt 15 % darunter, S1 deutlich. Der Dichte-Anteil bei S1/S2 sieht hoch aus - "Dichte" ist Punkte pro m2 ZWEIGFLAECHE, und die waechst ueber die Stufen dramatisch. Was zaehlt ist die Anzahl: 17 / 43 / 67 / 130 Cards, Kante 0.19 / 0.29 / 0.37 / 0.45. Budget S3/S4 auf 4300 (die Spielseite hebt ASSETS.md nach dem 170er-Export entsprechend an). 2) Radius-Filter skaliert mit dem Wachstum (der S3-Befund) Der Filter trennt duenne Zweige von dickem Geaest - aber mit starren 0.04 m greift er bei jungen Baeumen nicht: gemessen sassen bei S2 81 % der Cards am Stamm, bei S3 58 %. Jetzt 48 bzw. 52 %. Dabei eine eigene Fehldeutung korrigiert: bei S1 bleiben ~88 % am Stamm, und das ist KEIN Defekt. Nachgemessen ist die zulaessige Flaeche dort fuer jeden Schwellwert identisch (0.013 m2) - der Saemling hat kein dickes Geaest, an dem sich etwas trennen liesse. Blaetter am Stiel sind bei einem Saemling der Normalfall. Ein erster Sweep hatte mich in die Irre gefuehrt, weil ich growth_t=0 auf einen AUSGEWACHSENEN Baum gesetzt hatte - der hat die volle Zweigflaeche und verhaelt sich voellig anders als ein echter Setzling. 3) LOD-Reduktion als TEILSET der LOD0-Cards Bisher wurde je Stufe neu gestreut - die Cards sassen an anderen Stellen und die Silhouette sprang beim LOD-Wechsel um. Jetzt wird ausgeduennt: jede verbleibende Card liegt genau dort, wo sie in LOD0 schon war, die Ueberlebenden werden groesser. Gearbeitet wird auf zusammenhaengenden INSELN, nicht auf Faces - eine Card muss ganz bleiben oder ganz verschwinden. Auswahl ist seed-stabil. Gemessen am reimportierten FBX: 260 / 156 / 78 / 32 Blatt-Faces, und 100 % der Cards auf LOD1-3 sitzen auf einer LOD0-Position. Der Versatz zu LOD0 ist dabei kleiner geworden als beim Neustreuen. 23 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
230 lines
9.7 KiB
Python
230 lines
9.7 KiB
Python
"""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)
|
|
# S3/S4 stehen auf 4300, nicht mehr auf 4000: die Spielseite hebt das Budget
|
|
# in ASSETS.md an, nachdem der 170er-Export abgenommen wurde. Die Doku folgt dem
|
|
# Auge, aber erst nach dem Export - deshalb steht die Zahl hier schon.
|
|
if s.leaf_budget_s1 > 800 or s.leaf_budget_s2 > 1200 or s.leaf_budget > 4300:
|
|
fails.append("Stufen-Budgets (%d/%d/%d) ueber der Vorgabe (800/1200/4300)"
|
|
% (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)
|
|
# Die Deckung steht als Tabelle pro Stufe, nicht als Formel. Card-ANZAHL muss
|
|
# ueber die Stufen steigen, Card-Groesse auch.
|
|
_d0, _g0 = tg._stufen_anteile(0.0)
|
|
_d1, _g1 = tg._stufen_anteile(1.0)
|
|
if _g0 >= _g1:
|
|
fails.append("Groessen-Anteil steigt nicht ueber die Stufen (%.2f -> %.2f)"
|
|
% (_g0, _g1))
|
|
# 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")
|