Drei gemeldete Probleme - alle bestaetigt, Ursachen gemessen statt geraten.
1) Palme sah "extrem komisch" aus
Gemessen: Kronenradius/Hoehe 0.11 - die Wedel klebten als Mop-Kopf am
Stamm. Zielwert aus zwei unabhaengigen Quellen, die sich decken:
tree-gen palm.py (Weber&Penn) setzt Wedellaenge = 0.3 x Stamm, die
TreeIt-Referenzpalme kommt auf 3.3 m bei 4.33 m = 0.38.
Jetzt 0.30. Dazu der Winkel: tree-gen gibt down_angle 60 + curve 40 an,
also schraeg raus und ueberbiegen. Mit Branch Up 0 allein standen die
Wedel als gerade Antennenspeichen ab; mit Up 0.35 / Droop 1.2 steigen sie
gemessen 1.1 m ueber den Ansatz und kippen dann ab.
2) Busch sah aus wie ein Baum
Gemessen: 1.81 m hoch bei 0.84 m Breite (B/H 0.46), durchgehender Stamm,
Aeste erst ab 15 %, Wurzelanlauf wie beim Baum. Das IST ein Baeumchen.
Botanisch ist der Unterschied nicht die Groesse, sondern die Architektur:
mehrere Stiele ab Boden, kein Leittrieb, gemessen nach Hoehe UND Breite.
Jetzt kurzer Stumpf + Aeste ab Boden: 1.20 m hoch, 2.0 m breit (B/H 1.6).
3) Kleinste Wachstumsstufe sah komisch aus / "nur runterskaliert"
Gemessen: der Setzling war relativ zu seiner Hoehe 2.4x DICKER als der
Altbaum (r/H 0.025 gegen 0.0104), weil Trunk Radius mit 0.30 skalierte,
die Hoehe aber mit 0.16. Botanisch gilt die elastische Aehnlichkeit nach
McMahon: Hoehe ~ Durchmesser^(2/3), also Faktor 0.16^1.5 = 0.064.
Dazu behielt jeder Setzling den vollen Wurzelanlauf -> Trichterfuss.
Neu ausserdem: Aeste schrumpfen mit der Hoehe statt langsamer, sitzen
tiefer (Aufasten kommt erst mit dem Alter) und haengen weniger.
Nicht jede Pflanze waechst wie ein Laubbaum -> GROWTH_OVERRIDES:
Palme kein Dickenwachstum, erst Durchmesser dann Hoehe (UF/IFAS ENH1212:
"its diameter is fixed for life") -> Setzling ist ein kurzer Stumpf
Kaktus Sukkulent, gleiche Logik, und OHNE Arme: Saguaros bilden die erst
mit 50-70 Jahren bei rund 2 m Hoehe (NPS)
Weide haengt erst mit langen Zweigen; sonst reichten sie 0.27 m unter
den Stammfuss und das Asset waere in UE geschwebt
Zwei Bugs fielen dabei auf:
- Branch Count = 0 schaltete die Aeste NICHT ab - dieselbe Falle wie damals
bei Sub Count (Curve to Points klemmt Count auf min. 1). Fuer die Sub-Ebene
gab es laengst einen Geometry-Switch, fuer die Hauptaeste nicht.
- Im Busch-Preset stand "Branch Count" zweimal im selben Dict; der zweite
Eintrag (10) hat den ersten (14) still ueberschrieben.
Die sieben uebrigen Presets sind unveraendert (gleiche Maße, gleiche Tris).
Neu: tests/test_proportionen.py nagelt die Zielwerte fest. 14 Tests bestehen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
147 lines
5.6 KiB
Python
147 lines
5.6 KiB
Python
"""Haelt die Proportionen der Baum-Presets fest.
|
|
|
|
Diese Werte sind nicht nach Geschmack gewaehlt, sondern nachgemessen bzw.
|
|
belegt - deshalb lohnt es sich, sie festzunageln:
|
|
|
|
* Palmenkrone: tree-gen `palm.py` (Weber&Penn) setzt die Wedellaenge auf
|
|
0.3 x Stammlaenge; die TreeIt-Referenzpalme kommt auf 3.3 m Spannweite bei
|
|
4.33 m Stamm = 0.38. Zielband also 0.25-0.45.
|
|
* Strauch: botanisch ist der Unterschied zum Baum die Architektur (mehrere
|
|
Stiele ab Boden, kein Leittrieb) - gemessen wird er nach Hoehe UND Breite.
|
|
Ein Strauch, der schmaler ist als hoch, ist ein Baeumchen.
|
|
* Setzling-Schlankheit: elastische Aehnlichkeit nach McMahon, Hoehe ~
|
|
Durchmesser^(2/3). Der Setzling darf relativ zu seiner Hoehe nicht dicker
|
|
sein als der Altbaum. Vorher war er es um den Faktor 2.4.
|
|
* Palme und Kaktus sind davon ausgenommen: Palmen haben kein Dickenwachstum
|
|
("its diameter is fixed for life", UF/IFAS ENH1212), Kakteen sind Sukkulenten.
|
|
Beide bauen zuerst den Durchmesser auf - deren Setzling ist absichtlich
|
|
gedrungen.
|
|
|
|
Aufruf: blender --background --factory-startup --python tests/test_proportionen.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()
|
|
NG = tg.build_group()
|
|
IDS = tg.socket_ids(NG)
|
|
|
|
fails = []
|
|
|
|
|
|
def geom(preset, t=None, over=None):
|
|
for o in list(bpy.data.objects):
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
ob = tg.make_object(NG, preset=preset, growth=t, seed=7)
|
|
md = ob.modifiers["GN_Tree"]
|
|
for k, v in (over or {}).items():
|
|
md[IDS[k]] = v
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
ev = ob.evaluated_get(dg)
|
|
me = ev.to_mesh()
|
|
vs = [v.co.copy() for v in me.vertices]
|
|
n = len(me.vertices)
|
|
ev.to_mesh_clear()
|
|
return vs, n
|
|
|
|
|
|
def hw(vs):
|
|
z0 = min(v.z for v in vs)
|
|
h = max(v.z for v in vs) - z0
|
|
b = 2 * max(math.hypot(v.x, v.y) for v in vs)
|
|
return z0, h, b
|
|
|
|
|
|
def trunk_radius(preset, t=None):
|
|
"""Reiner Stammradius im Band 30-40 % - nur moeglich, weil Branch Count 0
|
|
die Aeste inzwischen wirklich abschaltet (siehe Test weiter unten)."""
|
|
vs, _ = geom(preset, t, {"Branch Count": 0, "Sub Count": 0})
|
|
z0, h, _ = hw(vs)
|
|
band = [math.hypot(v.x, v.y) for v in vs
|
|
if z0 + h * 0.30 <= v.z <= z0 + h * 0.40]
|
|
return (sum(band) / len(band) if band else 0.0), h
|
|
|
|
|
|
# --- 1) Branch Count 0 schaltet die Aeste wirklich ab ----------------------
|
|
# "Curve to Points" klemmt Count auf mindestens 1. Ohne den Geometry-Switch
|
|
# lieferten 0 und 1 identische Geometrie.
|
|
_, n0 = geom("baum", over={"Branch Count": 0})
|
|
_, n1 = geom("baum", over={"Branch Count": 1})
|
|
if n0 >= n1:
|
|
fails.append("Branch Count 0 (%d Verts) schaltet die Aeste nicht ab (1 -> %d)"
|
|
% (n0, n1))
|
|
|
|
# --- 2) Palmenkrone ragt aus --------------------------------------------
|
|
vs, _ = geom("palme")
|
|
z0, h, _ = hw(vs)
|
|
krone = max(math.hypot(v.x, v.y) for v in vs if v.z > z0 + h * 0.8)
|
|
if not (0.25 <= krone / h <= 0.45):
|
|
fails.append("Palme: Kronenradius/Hoehe %.2f, erwartet 0.25-0.45 "
|
|
"(tree-gen 0.30, TreeIt 0.38)" % (krone / h))
|
|
|
|
# --- 3) Busch ist breiter als hoch ---------------------------------------
|
|
vs, _ = geom("busch")
|
|
_, h, b = hw(vs)
|
|
if b < h:
|
|
fails.append("Busch: %.2f m breit bei %.2f m hoch - ein Strauch ist nicht "
|
|
"schmaler als hoch, sonst ist es ein Baeumchen" % (b, h))
|
|
|
|
# --- 4) Setzlinge sind nicht gedrungener als der Altbaum ------------------
|
|
# Ausnahmen: Palme (kein Dickenwachstum) und Kaktus (Sukkulent).
|
|
for p in tg.PRESETS:
|
|
if p in ("palme", "kaktus"):
|
|
continue
|
|
r_alt, h_alt = trunk_radius(p, 1.0)
|
|
r_neu, h_neu = trunk_radius(p, 0.0)
|
|
if not (h_alt and h_neu and r_alt):
|
|
continue
|
|
schlank_alt = r_alt / h_alt
|
|
schlank_neu = r_neu / h_neu
|
|
if schlank_neu > schlank_alt * 1.5:
|
|
fails.append("%s: Setzling ist relativ %.1fx dicker als der Altbaum "
|
|
"(r/H %.4f gegen %.4f) - McMahon verlangt schlanker"
|
|
% (p, schlank_neu / schlank_alt, schlank_neu, schlank_alt))
|
|
|
|
# --- 5) Kein Setzling haengt nennenswert unter den Wurzelansatz -----------
|
|
# Wedel und Aeste, die unter den Ursprung reichen, lassen das Asset in UE
|
|
# schweben. Grenze grosszuegig: 10 % der Hoehe.
|
|
for p in tg.PRESETS:
|
|
vs, _ = geom(p, 0.0)
|
|
zs = [v.z for v in vs]
|
|
z0, h = min(zs), max(zs) - min(zs)
|
|
# Der Stammfuss wird EXAKT bestimmt, indem der Baum ohne Aeste gebaut wird.
|
|
# Ein "tiefster Punkt nahe der Achse" reicht nicht: beim Kaktus ist der
|
|
# Stamm dicker als jeder sinnvolle Achsen-Schwellwert, die Suche fand dann
|
|
# nur Vertices weiter oben und meldete 92 % Ueberhang, wo keiner war.
|
|
tvs, _ = geom(p, 0.0, {"Branch Count": 0, "Sub Count": 0})
|
|
fuss = min(v.z for v in tvs)
|
|
if fuss - z0 > h * 0.10:
|
|
fails.append("%s S1: Geometrie reicht %.2f m unter den Stammfuss (%.0f%% "
|
|
"der Hoehe)" % (p, fuss - z0, 100 * (fuss - z0) / h))
|
|
|
|
# --- 6) Kaktus-Setzling hat noch keine Arme ------------------------------
|
|
# NPS: "branches normally begin to appear when a saguaro reaches 50 to 70
|
|
# years of age" - da ist er rund 2 m hoch.
|
|
vals = tg.growth_values("kaktus", 0.0)
|
|
if vals.get("Branch Count", 1) != 0:
|
|
fails.append("Kaktus-Setzling hat %d Arme, erwartet 0"
|
|
% vals.get("Branch Count"))
|
|
|
|
print("")
|
|
if fails:
|
|
print("ERGEBNIS: %d FEHLER" % len(fails))
|
|
for f in fails:
|
|
print(" - " + f)
|
|
sys.exit(1)
|
|
print("ERGEBNIS: ALLE CHECKS OK")
|