Files
stylized-rock-generator/README.md
T
D4rkst3randClaude Opus 4.8 a718476592 Tree Gen 1.48.0: Kronen-Huelle, weniger Blaetter, Formband neu geeicht
Vom User im Wald abgenommen: "sehr schoen etwas weniger blaetter und wir
koennen das fuer alle baeume uebernehmen".

1) KRONEN-HUELLE statt Einzelkappen
   Kugeln auf die Aeste, Mesh to Volume, Volume to Mesh, dann Streuung auf der
   verschmolzenen Oberflaeche - der Standard-Ablauf aus den
   Produktions-Breakdowns, den wir uebersprungen hatten. Die Aeste verschwinden
   im Inneren der Krone; die Polster sind nicht mehr als getrennte Koerper
   sichtbar.

   Drei Befunde, alle gemessen:
   - ast_id kam nur an 3 von 10 Aesten an. Nicht das Sampling, sondern die
     Eingabe: nur VIER Cluster-Zentren auf drei Aesten. Sichtbar erst mit
     Points to Vertices - to_mesh() auf eine Punktwolke liefert eine leere Mesh
     und misst nichts.
   - S1 stand kahl. Reihe ueber die Stufen 0/20/41/109 Zentren: allmaehlicher
     Abfall, keine Klippe, also die Cluster-Dichte. "Dichte" ist Kugeln je m2
     Zweigflaeche, die schrumpft quadratisch - sie laeuft jetzt mit 1/g^2
     gegen die Groesse, damit die ANZAHL konstant bleibt. Skalenfrei, kein
     Sonderweg fuer Setzlinge.
   - 31 % der Cards lagen unter dem Kronenansatz: die Verschmelzung weitet die
     Form nach unten, der Filter griff nur auf den Zentren. Jetzt auch auf der
     Huelle.

2) WENIGER BLAETTER (-15 % auf die Stufen-Ziele)
   Gemessen an der Eiche: 381 -> 319 Cards, Deckung 10.4 -> 8.6, 372 Tris
   gespart. Der Gegenversuch - Cards um 15 % verkleinern - kostet MEHR Deckung
   (7.9, die Flaeche geht quadratisch) und spart kein Dreieck.

3) FORMBAND um das Abgenommene gelegt, nicht umgekehrt
   Alle Referenzformen unter der neuen Bauform neu gemessen:
     Eiche      u/m 1.04-1.16   o/m 0.79-0.91
     abgenommen u/m 0.78-1.02   o/m 0.68-0.79
     Trichter   u/m 0.68-0.78   o/m 1.04-1.14
     Pilz       u/m 0.59-0.90   o/m 0.82-0.95
     Haube      u/m 0.94-1.03   o/m 0.71-0.78

   u/m TRENNT NICHTS MEHR - der abgenommene Bereich ueberlappt den Pilz und
   stoesst an den Trichter. Die alte 0.82-Schranke stammt aus der
   Einzelkappen-Aera; dagegen zu kalibrieren haette die Krone verbogen, um eine
   Zahl zu retten, die ihre Aussagekraft verloren hat. u/m wird nur noch
   mitgedruckt. o/m trennt sauber und traegt die Formpruefung allein.

27 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 15:40:49 +02:00

1541 lines
80 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Stylized Asset Generators (Blender Addons)
> Dieses Repo liefert **vier** Extensions ueber **eine** URL:
>
> | Addon | Panel | Zweck |
> |---|---|---|
> | **Stylized Rock Generator** | *Rock Gen* | Batch-Rocks + Textur-Bake + FBX/LOD-Export |
> | **Stylized Tree Generator** | *Tree Gen* | Baeume/Palmen/Bueschen/Kakteen (Geometry Nodes) mit Wachstums-Stufen |
> | **Stylized Grass Generator** | *Grass Gen* | Gras/Blumen/Farne mit Wind-UV fuer `SimpleGrassWind` |
> | **Stylized Asset Utils** | *Asset Utils* | **Handgebaute** Assets auf die Konvention bringen |
>
> Ein Blender-Extension-Repo kann mehrere Extensions ausliefern — `server-generate`
> listet jedes Zip in `dist/`. Ein „Check for Updates" aktualisiert also alle vier.
---
## Panel-Aufbau (alle vier Addons)
Jedes Addon hat oben im N-Panel nur das, was man **bei jedem Asset** anfasst -
Vorlage, Name, den grossen Erzeugen-Knopf, Zufall. Alles andere sitzt in
nummerierten, eingeklappten Unter-Panels in der Reihenfolge des Arbeitsablaufs:
| Addon | Unter-Panels |
|---|---|
| Rock Gen | 1 Benennung & Groesse · 2 Form · 3 Detail & Cleanup · 4 Shading · 5 Vertex-Farben · 6 UV/LOD/Material · 7 Batch & Transform · 8 FBX-Export · 9 Textur-Bake |
| Tree Gen | 1 Wachstum · 2 Blattwerk · 3 Zusatz-Ebenen · 4 Export · 5 Faell-Teile |
| Grass Gen | 1 Form · 2 Haltung · 3 Batch & Budget · 4 Kanal-Belegung |
| Asset Utils | 1 Vorbereiten · 2 Vertex-Farben · 3 Benennung · 4 Pruefen · 5 Export |
Schalter, die ein ganzes Unter-Panel an- und ausknipsen (Vertex-Farben backen,
Wachstums-Stufen), sitzen als Haekchen **im Panel-Kopf** - so sieht man den
Zustand, ohne aufzuklappen.
In den Generatoren sind alle Unter-Panels zugeklappt, weil es Einstellungen sind,
die man selten aendert. In Asset Utils ist jedes Unter-Panel ein Arbeitsschritt;
1, 2 und 4 macht man bei jedem Asset und bleiben deshalb offen. `tests/test_panels.py`
haelt diese Absicht fest, damit sie nicht versehentlich verrutscht.
**Falle beim Bauen:** Unter-Panels brauchen dieselbe `bl_category` wie das
Eltern-Panel - sonst landen sie in einem anderen Tab und sind praktisch
unsichtbar. Und das Eltern-Panel muss **vor** seinen Kindern registriert werden,
sonst findet Blender die `bl_parent_id` nicht.
---
## EcoGame-Konventionen (alle Addons)
Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
**Benennung**
- Felsen: `Rock_<Form>_<Klasse>` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`.
Das Praefix ist **`Rock`** (Standard) — die eingefrorene EcoGame-Konvention.
Die Klasse setzt eine **Basis-Groesse** (S=0.45, M=1.0, L=1.9, XL=3.2) und ist
damit ein bewusst anderes Asset — gemessen: S 0.85 m, M 1.9 m, L 3.5 m, XL 6.0 m.
Davon strikt getrennt ist die **Zufalls-Streuung** (`scale_min/max`): die variiert
nur die FORM und wird von „Gesamtgroesse konstant halten" auf konstantes Volumen
normiert (1.11× statt 1.92×), damit sie sich nicht mit den ScaleMin/Max der Engine
multipliziert.
- Baeume: `<Art>_<Variante>_S<Stufe>`, z. B. `Birke_01_S1``Birke_01_S4`.
Zusatz-Ebenen haengen hinten an: `_Leaf`, `_Frucht`.
- Texturen: `<name>_albedo` und `<name>_normal` — mehr nicht.
**Textur-Bake**
- Nur **Albedo + Normal**. Roughness ist im Master-Material eine **Zahl**,
keine Textur.
- **AO/Cavity wird ins Albedo multipliziert** (Option „AO ins Albedo einrechnen",
Staerke regelbar). Dunkle Ritzen und helle Platten stecken im Farbbild — das
traegt den Stylized-Look. Separate Rough-/AO-Paesse sind Default **aus**.
- **Plattenstil (Genshin-Referenz):** Die Textur besteht aus **wenigen grossen,
fast leeren Platten** mit **schmalen dunklen Fugen** — das Detail steckt in der
Fuge, nicht in der Flaeche. Regler: `Plattengroesse` (kleiner = groessere
Platten), `Fugenbreite` (klein = harte Fuge, flache Platte), `Platten flach`
(1 = Flaeche nahezu einfarbig). Ein dichtes Zellmuster ueber die ganze
Oberflaeche erschlaegt die Form und passt NICHT zu diesem Look.
- **Vertex-Farb-Layout v2 (kanal-getrennt), Attribut `Cavity`:**
| Kanal | Inhalt |
|---|---|
| **R** | Cavity pur — 1 = offene Flaeche, 0 = tiefe Fuge (**ohne** Hoehen-Verlauf) |
| **G** | Moos-/Schmutzmaske — 1 = hier waechst was (Oberseite + Fugennaehe + Noise) |
| **B** | Hoehen-Verlauf — 0 = Unterseite, 1 = oben |
| **A** | Erz-Maske, **invertiert**: 1.0 = kein Erz, 0.0 = Erz. Ohne Erz-Bake bleibt der Kanal durchgehend auf 1.0 |
Getrennt statt eingebacken: der erdige **Boden-Saum** am Stein-Fuss und die
**Moosfarbe** werden pro Material-Instanz geregelt, nicht im Mesh festgelegt.
Regler: `Cavity-Staerke`, `Weichzeichnen`, `Moos-Menge`, `Moos-Aufbruch`,
`Moos-Seed`.
**G alternativ als Teil-Maske:** Haken `G = Teil-Maske (Shells)` markiert
getrennte Mesh-Teile statt Moos — der oberste/breiteste Teil bekommt 1, der
Rest 0. Assets, die aus mehreren Objekten mit **Strg+J** verbunden wurden
(Pilz = Hut + Stiel), lassen sich damit EXAKT trennen statt ueber Normalen
geschaetzt. Ein Material toent dann beide Teile unterschiedlich — **kein
zweiter Material-Slot noetig**. Einteilige Meshes bleiben unmarkiert (G=0).
**Der Button `Vertex-Cavity auf Auswahl` backt das Layout auf JEDES markierte
Mesh** — also auch auf handgebaute Assets, nicht nur auf generierte Steine.
Verifiziert: R ohne Hoehenabhaengigkeit (unten 0.62 / oben 0.59), B steigt
sauber (0.08 -> 0.92), G oben staerker (0.20 -> 0.57), A konstant 1,
Seed aendert die Verteilung, und alle Kanaele ueberleben den FBX-Roundtrip.
- **Texturfreie Alternative — Vertex-Cavity:** Statt zu baken laesst sich die
Cavity in die **Vertex-Farben** schreiben (Attribut `Cavity`, ueberlebt den
FBX-Roundtrip verlustfrei — gemessen 0.250..0.947 vor und nach dem Import).
Das Material braucht dann **keine Textur**:
`Base Color = Farbton x VertexColor`, dazu Kantenlicht (Fresnel) und
Schattentint. Vorteil gegenueber Triplanar: die dunklen Fugen sitzen auf den
ECHTEN Kanten des Meshes statt darueberzuschweben. Grenze: die Aufloesung
haengt an der Vertex-Dichte — fuer flaechige Platten reicht sie, fuer feine
Struktur nicht.
- 1024 px reicht fuer stylized Formen.
- Tileable (4D-Torus) passt zum triplanar mappenden Master-Material.
**Material-Slots (Teil des Vertrags, keine Kosmetik)**
- Die UE-Skripte mappen ueber den **Slot-Namen**. Ohne Slot legt Unreal nur
einen Default an und die Konvention greift nicht mehr — deshalb ist
`Material zuweisen` **Standard an**.
- Normale Steine rotieren durch `M_Rock_Granite/Basalt/Sandstone/Mossy`.
- Das Erzfels-Set bekommt **einen neutralen Slot `M_Rock_Ore`** — ein Set fuer
alle Erzsorten, welche Sorte es ist macht die Material-Instanz.
**Mesh**
- Echte Meter, Ursprung unten-mittig, FBX pro Asset bzw. LOD-Gruppe.
- **UV1 Lightmap ist Default aus** — EcoGame beleuchtet dynamisch.
- Meshes brauchen **keine eigenen Materialien**; in UE bekommt alles die
Material-Instanz vom Master. Ein Platzhalter im FBX ist okay.
- Scale-Randomisierung nur fuer die **Form** (nicht-uniform) nutzen, nicht fuer
die Gesamtgroesse — sonst multipliziert sie sich mit ScaleMin/Max der Engine.
---
## Schnellstart-Sorten
Ganz oben im Panel: Sorte waehlen, Haken druecken, erzeugen. Deckt den Alltag
ab — die neun Unter-Panels bleiben fuer den Feinschliff.
| Sorte | Form | Dissolve | gemessen |
|---|---|---|---|
| **Findling** | rund, weiche Kanten | 1725° | ~1160 V / ~2320 Tris |
| **Platte** | flach, liegt satt auf | 1725° | ~670 V / ~1340 Tris |
| **Kantig** | Bevel + Weighted Normal, Flat | 814° | ~350 V / ~690 Tris |
| **Erzbrocken** | dicht genug fuer die Nugget-Maske, Slot `M_Rock_Ore` | 1725° | ~1280 V / ~2560 Tris |
**Der Dissolve-Winkel ist ein Bereich, kein Wert.** Ein fester Winkel passt nie
fuer alle Steine einer Sorte — man stellt ihn sonst bei jedem Stein nach. Je
Stein wird einer aus dem Bereich gezogen; gemessen an sechs Findlingen bei
1725°: 19362928 Tris. Beide Regler gleich setzen = fester Winkel wie frueher.
> **Warum die Sorten unterschiedlich dicht sind.** Die Erz-Maske haengt an der
> Vertex-Dichte, ein normaler Stein braucht das nicht. Deshalb hat der
> Erzbrocken ein hoeheres Decimate als der Findling — sonst waere entweder das
> Erz verschmiert oder der normale Stein ueber dem Tri-Fenster. Der Satz hinter
> dem **S+M-Set** ist noch etwas dichter (0.70/0.92), weil dort auch kleine
> S-Brocken vorkommen: mit dem Findling-Satz fielen die im schlechtesten Fall
> auf 708 Verts und damit unter die Schwelle fuer eine saubere Maske.
## Erzfels-Set (A-Kanal)
**EIN neutrales Set fuer ALLE Erzsorten.** Die Maske im A-Kanal sagt nur, *wo*
Erz sitzt. *Welches* Erz es ist (Kupfer/Eisen/Gold = Tint + Gluehen), entscheidet
die Material-Instanz in UE. Knopf: **Erzfels-Set erzeugen (S + M)** erzeugt
`Rock_Ore_S_NN` und `Rock_Ore_M_NN` und stellt danach alle Einstellungen zurueck.
Regler: `Erz-Anteil`, `Nugget-Groesse` (Radius in m), `Erz-Seed`.
**Richtung des A-Kanals — wichtig.** Die Nuggets laufen auf **A = 0** zu, nicht
auf 1. Der Master rechnet:
```
Erz = (1 - A) * ErzMenge
```
`ErzMenge` ist ein MI-Regler mit **Standard 0**. Damit ignorieren normale
Stein-MIs den A-Kanal vollstaendig, und nur die Erz-MIs drehen ihn auf. Ein
Stein ohne Erz-Bake behaelt seine durchgehende 1.0 und liefert `(1-A) = 0`
deshalb musste das bestehende 25er-Set **nicht** neu exportiert werden.
Andersherum gebacken waere das Erz unsichtbar und der ganze *restliche* Stein
wuerde als Erz gelesen. `tests/test_erz.py` prueft deshalb direkt gegen diese
Formel und nicht gegen den Rohwert.
Die **Nugget-Groesse bleibt absolut** (Radius in Metern): ein kleiner Brocken
bekommt weniger, dafuer proportional groessere Einschluesse. Das ist physikalisch
richtig, und das Spiel skaliert die Meshes per ScaleMin/Max ohnehin zufaellig -
eine groessenrelative Optik waere dadurch sowieso wieder zerlegt.
Nuggets statt Rauschen: gestreute Zentren mit weichem Abfall. Ein Noise-Feld
gaebe wolkige Schlieren ueber den ganzen Stein, Erz sitzt aber in abgegrenzten
Einschluessen. Die Zentren bevorzugen Fugen — Adern folgen Rissen, und das
Gluehen liest sich in einer Vertiefung besser als auf einer flachen Platte.
> **Die Maskenaufloesung haengt an der Vertex-Dichte.** Bei den bisherigen
> ~150-Vert-Steinen war jedes dritte Vertex ein Nugget-Zentrum: die Nuggets
> liefen zu grossen Schlieren zusammen. Deshalb erzeugt das Erz-Set **dicht**
> (siehe unten) — dort liest es sich als einzelne Einschluesse. Die Nuggetzahl
> ist zusaetzlich auf ein Zentrum je ~10 Verts gedeckelt.
Gemessen (dicht, ~2700 Tris): Erz-Anteil 0.08 → 4 %, 0.18 → 9 %, 0.35 → 15 %
der Oberflaeche. A faellt auf 0.00 und erreicht 1.00, und die Maske ueberlebt
den FBX-Roundtrip.
## Bruchstuecke (Steine zerschlagen)
Kein Laufzeit-Fracturing im Spiel: die Teile werden beim Anschlagen als
Physik-Actors gespawnt. Sie entstehen deshalb hier als vollwertige Assets.
Knopf **In Bruchstuecke zerlegen** arbeitet auf der Auswahl und liefert
`<Name>_Piece_01..NN`. Regler: `Bruchstuecke` (2-4), `Bruch-Seed`,
`Ein Stueck mit Erz`, `Mutterstein ausblenden`.
Jedes Teil bekommt: Vertex-Layout v2, Ursprung unten-mittig, die **gleichen
Material-Slots** wie der Mutterstein (die UE-Skripte mappen darueber) und beim
Export eine eigene `UCX_`-Huelle.
**Punkt 4a:** mit `Ein Stueck mit Erz` heisst ein zufaelliges Teil
`<Name>_Piece_Erz` und traegt Erz-Adern im A-Kanal (Richtung 0). Alle anderen
Teile behalten durchgehend A = 1.0 und sind damit fuer den Master erzfrei. Ob das Erz-Stueck gespawnt wird, entscheidet die Loot-Logik im Spiel.
**Wie die Bruchkanten zusammenpassen:** Voronoi ueber Halbraeume. Fuer jedes
andere Zentrum wird an der Mittelsenkrechten geschnitten und die abgewandte
Seite verworfen. Benachbarte Zellen schneiden dadurch an *genau derselben*
Ebene - die Bruchflaechen passen zusammen, ohne dass man sie abstimmen muesste.
Bewusst ohne das Cell-Fracture-Addon: das muesste beim User aktiviert sein und
haengt an Operator-Kontext, bmesh laeuft immer.
> **Die entscheidende Falle.** Jeder Schnitt muss **sofort** geschlossen werden,
> und zwar ueber genau die Kanten, die `bisect_plane` als `geom_cut` zurueckgibt.
> Der naheliegende Weg - am Ende alle Randkanten suchen und fuellen - erzeugte
> Kanten mit vier Flaechen; raeumte man die ab, blieben Loecher. Aufgefallen ist
> das am **Volumen**: die Teile kamen zusammen auf 72.7 % des Muttersteins,
> weil `calc_volume` bei offenen Meshes Unsinn liefert. Jetzt sind es **100.2 %**
> bei 2, 3 und 4 Teilen, mit 0 offenen und 0 non-manifold Kanten.
`tests/test_bruchstuecke.py` prueft genau diesen Volumen-Vergleich - er ist
zugleich das Warnsignal fuer kaputte Topologie.
## Smooth & dicht
Knopf im Form-Panel: setzt Unterteilung 5, Decimate 0.40/0.80 und Auto-Smooth.
Gemessen ueber drei Seeds: **2708/2658/2716 Tris** (Schnitt 2694) — im
Zielfenster 15003000. Kein Low-Poly-Look, und alle Vertex-Masken (Cavity, Moos,
Erz) werden feiner.
---
## Export-Haertung (Rock Gen)
Drei Dinge passieren beim FBX-Export jetzt automatisch. Alles laeuft auf
**temporaeren Kopien** - die Szene bleibt unveraendert.
**1. Ursprung wird erzwungen** (keine Option). Jedes Objekt wandert an den
Welt-Ursprung, der Ursprung sitzt unten-mittig der Bounding-Box. Anlass: das
25er-Set trug seine Szenen-Rasterpositionen (bis 20 m) im Mesh, die Steine
schwebten im Spiel neben ihrem Platzierungspunkt. Nachgemessen am
**reimportierten** FBX: Bbox-Mitte 0.000/0.000, `min.Z` 0.000.
**2. Flache Steine werden hingelegt** (`Flache Steine hinlegen`, Default an).
Die kuerzeste Bbox-Achse wird nach oben gedreht. Gedreht wird nur, wenn es etwas
bringt - bei einem 2.53/1.40/1.54-Stein wuerde Kippen die Form nur willkuerlich
drehen. Im Spiel gibt es nur zufaelligen Yaw, die gebackene Ausrichtung ist also
endgueltig.
**3. UCX-Kollision** (`UCX-Kollision mitexportieren`, Default an). Konvexe Huelle
als `UCX_<Name>` im selben FBX, Unreal nimmt sie als Simple Collision. Budget
ueber `UCX max. Verts` (Default 32).
Drei Fallen, die dabei aufgefallen sind:
- Blender haengt an Kopien `.001` an. Unreal ordnet die Kollision aber ueber den
**exakten** Mesh-Namen zu - das Original bekommt fuer die Dauer des Exports
einen Hilfsnamen, die Kopie den echten.
- `bmesh.from_mesh` bringt die **Originalflaechen** mit, `convex_hull` legt seine
Huelle dann obendrauf: gemessen 16-21 offene Kanten. Es duerfen nur die
**Punkte** eingelesen werden.
- Jede Vereinfachung der Huelle schneidet Ecken ab, sie wird dadurch **kleiner**
als der Stein - eine Kollision kleiner als das Mesh laesst den Spieler
einsinken. Deshalb wird sie danach exakt so weit aufgeblasen, dass wieder alles
hineinpasst.
### LOD-Gruppen + Vertex-Farben (war offen, jetzt geklaert)
Ging **nicht**: der LOD-Zweig sprang komplett am Cavity-Bake vorbei, LOD-Gruppen
kamen ohne Farbattribut aus dem Export - auf keiner der vier Stufen. Jetzt wird
auf LOD0 gebacken, **bevor** dupliziert wird; das Decimate interpoliert die
Farben mit. Wuerde je Stufe neu gebacken, haette jede Stufe ihre eigene Cavity
und der LOD-Wechsel wuerde poppen. Nach dem FBX-Roundtrip gemessen:
| Stufe | Verts | R-Bereich |
|---|---|---|
| LOD0 | 40 | 0.30..1.00 |
| LOD1 | 21 | 0.41..0.92 |
| LOD2 | 11 | 0.45..0.92 |
| LOD3 | 6 | 0.49..0.88 |
Dazu die zweite Falle: `ob.matrix_world = Identity` loescht die Transformation
**nicht**, wenn das Objekt ein Eltern-Objekt hat - Blender rechnet eine lokale
Gegen-Matrix aus. Bei einer LOD-Gruppe auf (13/-7.5/2.2) landete das Mesh dadurch
bei genau MINUS dieser Position. `matrix_basis` und `matrix_parent_inverse`
muessen direkt genullt werden.
Die UCX-Huelle haengt bewusst **neben** dem LodGroup-Empty, nicht darunter -
Unreal wuerde jedes Kind der Gruppe als weitere LOD-Stufe lesen.
---
## Stylized Asset Utils
Fuer alles, was **von Hand** gebaut wird (Pilze, Baumstumpf, Erzfels, Props) und
deshalb nicht durch einen Generator laeuft. Panel in fuenf einklappbaren Schritten:
1. **Vorbereiten** — Rotation/Scale anwenden, Ursprung unten-mittig, optional
Normalen neu berechnen. Damit steht das Asset in UE bei Scale 1 auf dem Boden.
2. **Vertex-Farben** — dasselbe Layout wie beim Rock Generator
(`R` = Cavity, `G` = Maske, `B` = Hoehe, `A` = 1).
`G` wahlweise als **Teil-Maske**: bei Assets, die mit **Strg+J** aus mehreren
Objekten verbunden wurden (Pilz = Hut + Stiel), bekommt der oberste Teil 1 und
der Rest 0 — exakt statt geschaetzt. **Ein** Material toent damit beide Teile,
ein zweiter Material-Slot entfaellt. Einteilige Meshes bleiben unmarkiert.
3. **Benennung** — Auswahl nach `<Basis>_<NN>` umbenennen, Mesh-Daten ziehen mit.
4. **Pruefen** — Asset-Check: nicht angewendete Transforms, Ursprung nicht unten,
Tri-Budget, fehlende UV/Vertex-Farben, lose Vertices, Custom Split Normals.
Details in der System-Konsole.
5. **Export** — FBX je Mesh mit `colors_type='SRGB'`, Triangulate und waehlbarem
Smoothing.
**Falle, die dabei auffiel:** `bpy.ops.paint.vertex_color_set` braucht den
Vertex-Paint-Modus und scheitert im Object-Mode mit „poll() failed". Steht es mit
`vertex_color_dirt` im selben `try`, wird der ganze Bake stillschweigend
uebersprungen. Die Initialisierung laeuft deshalb direkt ueber Python.
---
## Stylized Grass Generator
Gras-, Blumen- und Farn-Bueschel. Der Kern ist die **Wind-UV**:
- **UV0.V = 0 an der Wurzel, 1 an der Spitze** — genau die Maske, die UEs
`SimpleGrassWind` als *WindWeight* braucht. Ohne sie rutscht der Halm beim
Wehen vom Boden weg oder bewegt sich gar nicht.
Am Zielmaterial `M_Grass` geprueft: es liest `TextureCoordinate`, aber **keine**
`VertexColor` — deshalb liefert der Generator die Maske PRIMAER ueber die UV.
Die Assets funktionieren damit **ohne Materialaenderung**.
- **Vertex-Farben zusaetzlich** (nicht als einzige Quelle):
`R` = Kopie der Windmaske · `G` = Variation je HALM · `B` = AO (unten dunkel).
`G` ist die Ebene **unter** dem Biom-Tint, den die Engine ueber
`PerInstanceCustomData 0/1` pro Instanz setzt — im Material **multiplizieren**,
nicht ersetzen.
- Presets: `gras`, `gras_hoch`, `steppengras`, `farn`, `blume`.
- Tri-Budget-Warnung (Default 400 laut `ASSETS.md` fuer Gras/Blume).
Gemessen: ein Standard-Bueschel mit 14 Halmen = **112 Tris**.
- Ursprung unten-mittig (`min_z = 0.0000`), UV und Vertex-Farben ueberleben den
FBX-Roundtrip.
---
## Stylized Tree Generator
### Spielfertiger Stand-Baum (Faellsystem)
Knopf **Spielfertig exportieren** im Export-Panel: Stamm und `_Leaf`-Ebene
werden zu **einem** Mesh mit **zwei Material-Slots** verschmolzen
(Slot 0 = `M_Bark`, Slot 1 = `M_Foliage`). Die getrennten Objekte im Blend
bleiben die Arbeitsebene — verschmolzen wird nur die Export-Kopie. Ursprung
Bbox-Unterseite Mitte, FBX-Einstellungen wie in der Fels-Pipeline.
**Kugel-Normalen** (Haken `Kugel-Normalen auf Blaetter`, Standard an): alle
Blatt-Normalen zeigen radial vom Kronen-Schwerpunkt nach aussen. Ohne das wirkt
jede Card wie ein eigenes flaches Blech und die Krone flimmert im Licht. Der
Schwerpunkt kommt **nur aus den Blatt-Faces** — der Stamm wuerde die Mitte nach
unten ziehen und die Kugel verkippen.
> Bewusst direkt ueber `normals_split_custom_set` statt ueber den
> Normal-Edit-Modifier: der muesste vor dem Export angewendet werden, und
> angewendete Modifier plus Custom Normals ist genau die Kombination, bei der
> der FBX-Export gerne flachbuegelt. Am **reimportierten** FBX gemessen:
> mittlere Radialitaet **1.000**, 100 % der 510 Blatt-Loops ueber 0.8.
> Gegenprobe Rinde 0.043.
**Card-Varianten:** `Card-Sammlung` nimmt eine Collection mit 46 Diamond-Cards
unterschiedlicher Atlas-UVs; je Blattpunkt wird eine zufaellig gezogen
(seed-stabil). Eine einzelne Card geht weiter — die Sammlung hat Vorrang.
Zwei Fallen beim Bauen:
- Ueber `bmesh.from_mesh` zusammenzufuegen haengt die Geometrie zwar an, wirft
aber die **Material-Indizes** weg: danach lagen alle 748 Faces auf Index 0.
Blenders `join` fuehrt die Slot-Listen zusammen und rechnet die Indizes um.
- `join` loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach
tot. Die Aufraeumliste fuehrt deshalb **Namen** statt Objekte.
### Blatt-Defaults: Bueschel, nicht Blaettchen
Eine Card ist ein ganzer **Zweigbueschel**, kein einzelnes Blatt. Die alten Werte
stammten aus der Blaettchen-Aera und streuten hunderte winziger Cards, wo ein
paar Dutzend grosse hingehoeren. Die neuen Werte sind an einer S4-Eiche in UE
gemessen und abgenommen:
| | vorher | jetzt | Wirkung |
|---|---|---|---|
| Groesse | 0.22 | **0.85** | Bueschel statt Bonsai |
| Dichte | 500 | **170** | wenige grosse statt viele Fussel |
| **Max Radius** | 0.02 | **0.04** | der Volumen-Hebel |
| Kippung | 0.5 | **0.9** | Rotations-Chaos schliesst Sichtluecken |
`Max Radius` ist der unscheinbare Held: bei 0.02 trugen nur die aeussersten
Zweigspitzen Laub, die Krone hatte nur Kontur und war innen nackt. Bei 0.04
wachsen Bueschel auch am mittleren Geaest und die Krone bekommt Tiefe.
**Ziel-Cardzahl statt Dichte-Konstanten.** Dichte-Faktoren mussten je Preset neu
geeicht werden: „Dichte" ist Punkte pro m² *Zweigflaeche*, und die verteilt sich
je Baum voellig anders — ein Faktor, der am Standardbaum 11 Cards ergab,
ueberschoss auf der Eiche um mehr als das Doppelte. In `LEAF_STUFEN` steht
deshalb die gewuenschte **Stueckzahl**, und der Generator regelt die Dichte per
Bisektion dagegen ein (seed-stabil, der Seed wird nicht angefasst). Damit ist die
Kalibrierung preset-unabhaengig und der Test prueft die Anzahl **direkt**:
| Preset | S1 (Ziel 10) | S2 (Ziel 40) | S3 (Ziel 120) | S4 (voll) |
|---|---|---|---|---|
| baum | 9 · 518 Tris | 41 · 914 | 104 · 1436 | 130 · 1916 |
| eiche | 10 · 576 | 43 · 998 | 138 · 1872 | 463 · 4206 |
| birke | 9 · 570 | 36 · 1028 | 136 · 1952 | 207 · 2894 |
| tanne | 9 · 778 | 38 · 1160 | 125 · 2282 | 235 · 3302 |
S4 wird **nicht** geregelt — der Altbaum ist die Referenz, an der die Dichte
ueberhaupt eingestellt wurde.
**Deckung pro Stufe, nicht per Formel.** Card-Anteil und Groesse stehen als
Tabelle (`LEAF_STUFEN`). Der Dichte-Anteil bei S1/S2 sieht hoch aus — das
taeuscht: „Dichte" ist Punkte pro m² *Zweigflaeche*, und die waechst ueber die
Stufen dramatisch. Gemessen hat der S1-Setzling **0.013 m²** zulaessige Flaeche;
ohne Anhebung bekaeme er null Cards. Was zaehlt, ist die Anzahl:
| Stufe | Cards | Card-Kante | Tris | Budget |
|---|---|---|---|---|
| S1 | 17 | 0.19 | 498 | 800 |
| S2 | 43 | 0.29 | 754 | 1200 |
| S3 | 67 | 0.37 | 946 | 4300 |
| S4 | 130 | 0.45 | 1396 | 4300 |
**Der Hoehen-Filter haelt den Fuss frei.** Der Radius-Filter allein reicht
nicht: er trennt duenn von dick, aber ein junger Stamm ist *ueberall* duenn.
Gemessen sassen beim Setzling **44 %** der Cards unter 40 % der Baumhoehe, die
tiefste bei **6 %** — eine belaubte Saeule statt einer Krone. Der Kronenansatz
steht deshalb je Stufe in `LEAF_STUFEN` (Anteil der Baumhoehe, aus der Bounding
Box abgeleitet — ein fester Meterwert waere beim Setzling die halbe Pflanze):
| Stufe | Ansatz | unter Ansatz vorher | nachher | tiefste Card |
|---|---|---|---|---|
| S1 | 40 % | 44 % | **0 %** | 6 % → **46 %** |
| S2 | 35 % | 32 % | **0 %** | 24 % → 36 % |
| S3 | 30 % | 9 % | **0 %** | 31 % → 34 % |
| S4 | 25 % | 3 % | **0 %** | 34 % (unveraendert) |
Beim Altbaum ist der Filter praktisch ein Sicherheitsnetz — dort sitzt ohnehin
nichts tief. Die Dichte von S1/S2 wurde nachgezogen, weil der Filter dem
Setzling sonst die Haelfte der Cards nimmt.
**Der Radius-Filter skaliert mit.** Er trennt duenne Zweige (Laub) von dickem
Geaest — aber ein Setzling ist *komplett* duenner als eine feste Schwelle. Mit
starren 0.04 m sassen bei S2 **81 %** der Cards am Stamm, bei S3 58 %; jetzt
48 bzw. 52 %.
> Bei S1 bleiben rund 88 % am Stamm, und das ist **kein Defekt**: gemessen ist
> die zulaessige Flaeche dort fuer *jeden* Schwellwert identisch — der Saemling
> hat schlicht kein dickes Geaest, an dem sich etwas trennen liesse. Blaetter am
> Stiel sind bei einem Saemling der Normalfall.
**Bueschelgroesse haengt am Wachstum.** Der Generator merkt sich `growth_t` am
Objekt; der Blattwerk-Operator skaliert die Cards damit (`Setzlings-Anteil`,
Standard 45 %). Gemessen ueber vier Stufen: Card-Kante 0.19 → 0.28 → 0.37 → 0.45.
> **Falle dabei:** „Dichte" ist Punkte pro m² 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² der Flaeche; die Dichte entsprechend gegenlaeufig anzuheben haelt
> die Blattdeckung ueber alle Stufen gleich.
**Tri-Budget pro Stufe** (ASSETS.md Teil 6) statt global: 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). Die 2500 von frueher waren eine
konservative Setzung — bei ISM-Instanzen mit LOD-Kette sind Roh-Dreiecke nicht
der Engpass, sondern **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 wichtigste Code-Falle in diesem Addon.** `md[socket] = wert` loest
> **keine** Neuauswertung des Geometry-Nodes-Modifiers aus. Nachgestellt: 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 brachte das Ergebnis. 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.
> `_modifier_anstossen()` macht das jetzt explizit.
### LOD-Kette (Stand-Baum)
Haken `LOD-Kette erzeugen`: exportiert `<Name>_LODGroup.fbx` mit `_LOD0..3` unter
einem `fbx_type=LodGroup`-Empty — dasselbe Schema wie die Fels-Pipeline. **Nur
der Stand-Baum**; die Faell-Teile leben Sekunden als Physik-Actor, dort reicht LOD0.
Die Blatt-Reduktion ist ein **Teilset der LOD0-Cards**, keine Neustreuung. Das
ist der Unterschied zwischen „die Krone wird duenner" und „die Krone springt
um": beim Neustreuen laegen die Cards an anderen Stellen und die Silhouette
wechselt beim LOD-Uebergang sichtbar. Als Teilset bleibt jede verbleibende Card
genau dort, wo sie in LOD0 schon war; damit die Krone nicht loechrig wird,
werden die Ueberlebenden groesser. Gemessen: **100 % der Cards auf LOD13
sitzen auf einer LOD0-Position.** LOD3 ist der Impostor-Ersatz.
Gearbeitet wird auf zusammenhaengenden **Inseln**, nicht auf einzelnen Faces —
eine Card besteht aus mehreren Dreiecken und muss ganz bleiben oder ganz
verschwinden.
| Stufe | Dichte | Card-Groesse | Rinde | gemessen (Rinde/Blatt/Tris) |
|---|---|---|---|---|
| LOD0 | 100 % | 100 % | 100 % | 578 / 170 / 1306 |
| LOD1 | 60 % | 115 % | 70 % | 477 / 118 / 912 |
| LOD2 | 30 % | 145 % | 45 % | 364 / 66 / 575 |
| LOD3 | 12 % | 200 % | 25 % | 221 / 28 / 302 |
**Kugel-Normalen auf jeder Stufe** — LOD13 haben eine eigene Card-Verteilung,
die Normalen muessen dazu passen. Am reimportierten FBX gemessen: **100 %
radial auf allen vier Stufen**. LOD3 traegt 16 % der Cards von LOD0.
Der Ursprung kommt aus **LOD0** (Unreal platziert danach); alle Stufen werden um
denselben Betrag verschoben, damit der Baum beim LOD-Wechsel nicht springt.
> **Falle, aus UE gemeldet:** `_origin_bottom_center` backt zuerst die Weltmatrix
> ins Mesh und wendet **dann** die Zentrierung an — zurueck gibt es aber nur die
> Zentrierung. Wer den Stufen 13 nur diese aufdrueckt, laesst ihre Weltmatrix
> ungebacken: sie landen bei **minus der Standposition**. Gemessen an einem Baum
> bei x = 18 m: LOD13 bei x = 17.87. Bei einem Baum am Ursprung faellt das
> nicht auf, weil minus null eben null ist — genau deshalb steht der Baum im
> Test jetzt **bewusst nicht** am Ursprung, und die X/Y-Mitte wird je Stufe im
> reimportierten FBX gemessen.
Das Decimate der Rinde laeuft **vor** dem Verschmelzen — danach wuerde es die
Blatt-Cards mit einschmelzen, und eine halbe Card ist keine Card.
### Blatt-Streuung: nachgemessen (Punkt 6)
`Max Radius` filtert die dicken Aeste heraus, damit keine Blaetter am Stamm
kleben. Ueber alle Presets gemessen, wie viele Cards tatsaechlich am Stamm
landen (moeglich, weil `ast_id` mitlaeuft):
| Preset | Cards | am Stamm | Tris gesamt |
|---|---|---|---|
| baum | 85 | 19 % | 1306 |
| palme | 246 | 0 % | 1710 |
| busch | 118 | 2 % | 876 |
| tanne | 176 | 6 % | 2244 |
| eiche | 172 | 2 % | 1772 |
| birke | 133 | 9 % | 1918 |
| weide | 297 | 1 % | 2246 |
| toter_baum | 49 | 22 % | 838 |
| kaktus | **0** | | 740 |
Der Filter arbeitet also richtig, und **alle Presets bleiben unter dem
2500-Tri-Budget** (Maximum weide 2246).
> **Messfalle dabei:** Zuerst hatte ich den Anteil der Vertices unter der
> Radius-Schwelle gemessen — der lag beim Busch bei 95 % und sah nach einem
> Problem aus. Das misst aber die Vertex-Verteilung, nicht wo Blaetter landen:
> ein Busch besteht nun mal fast nur aus duennen Zweigen. Die richtige Frage
> beantwortet erst `ast_id`.
Der Kaktus erzeugt regulaer **0 Cards** — kein Ast ist duenner als `Max Radius`,
und das ist botanisch richtig. Vorher lief das still durch und man stand vor
einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler.
### UCX bei LOD-Gruppen: der Name muss auf den MESH-Node zeigen
Der Legacy-Importer von Unreal matcht `UCX_<Mesh-Node>` - **nicht** gegen das
LodGroup-Empty. Eine Huelle mit dem Gruppennamen liegt zwar in der Datei und ein
Reimport nach Blender zeigt sie brav an, aber Unreal verwirft sie STILL:
gemeldet von der Spielseite mit `convex = 0` an 108 Stand-Baeumen und 57
Fels-Meshes. Beweis von dort: dieselbe Datei, Huelle umbenannt auf den
LOD0-Node, liefert `convex = 1`.
`UCX_<Basisname>` ist nur OHNE LOD-Kette richtig, weil der Mesh-Node dann genau
so heisst. Deshalb funktionierten die Faell-Teile und `_Pieces_All.fbx` die
ganze Zeit - die haben keine LODs.
**Wie das durchrutschen konnte, ist die eigentliche Lehre.** Fuer 1.42 habe ich
die Konvention nicht geraten, sondern am Fels-Export nachgemessen: exportieren,
reimportieren, Namen und Hierarchie pruefen. Das war richtig - und hat trotzdem
nichts bewiesen, weil der Fels-Export denselben Fehler hatte. Gemessen wurde die
STRUKTUR ("liegt die Huelle in der Datei, heisst sie wie erwartet, haengt sie
richtig?"), nicht die WIRKUNG ("nimmt Unreal sie an?"). Eine abgeschaute
Konvention ist nur so gut wie die Quelle, von der sie stammt.
Die Tests pruefen deshalb jetzt die REGEL statt des Namens: hinter `UCX_` muss
ein Mesh-Node aus derselben Datei stehen, und zwar LOD0. Das haette den Fehler
in beiden Addons gefunden, ohne dass jemand die Antwort vorher kennt.
Gegenproben: Baum 9 Fehler, Fels 1 Fehler.
Was nur die Engine beantworten kann - ob `convex` nach dem Import wirklich
groesser 0 ist - bleibt bei der Spielseite. Der Test kann nur sicherstellen,
dass die Datei die Bedingung erfuellt, an der es lag.
### Die Gesamtdatei steht jetzt auch auf der Stammbasis
`_Pieces_All.fbx` trug den SZENEN-Nullpunkt mit: ein Baum bei x = 18 m
exportierte gemessen mit X 16.5..19.3, Y -8.7..-6.2. Die Einzel-Teile hatten
das Problem nie - sie sitzen ohnehin auf ihrem Massenmittelpunkt -, und der
Stand-Baum folgt der Stammbasis-Konvention seit 1.36. Nur die
Zusammenbau-Datei fiel heraus.
Der Versatz kommt aus **derselben Funktion** wie beim Stand-Baum
(`_stammbasis_versatz`, aus `_origin_bottom_center` herausgeloest). Zwei
Rechnungen fuer denselben Nullpunkt waeren genau die Fehlerklasse, die hier
schon dreimal zugeschlagen hat - und hier haette sie besonders weh getan: die
zusammengesetzten Truemmer muessen exakt auf dem Platz liegen, wo der Baum
stand.
Angewendet wird der Versatz nur fuer diesen Export und danach zurueckgenommen.
Mit "Teile behalten" staenden sie sonst verschoben neben dem Baum in der Szene.
Geprueft wird ueber den STAMMQUERSCHNITT auf Fusshoehe, nicht ueber die
Bbox-Mitte: die Krone haengt einseitig, eine Bbox-Mitte wuerde den Fehler
verwischen. Der Test stellt den Baum dafuer bewusst auf (18, -7.5) - bei
(0,0,0) faellt so etwas nicht auf. Gegenprobe ohne den Versatz: Stammfuss bei
18.000 / -7.500, genau der gemeldete Wert.
### UCX auch in der Gesamtdatei - und zwei Fehler, die dabei auffielen
`_Pieces_All.fbx` kam ohne Kollision an, waehrend die Einzeldateien welche
hatten. Jetzt bekommt jedes Teil auch dort seine Huelle, gleiche Konvention wie
beim Stand-Baum: `UCX_<Teilname>` als Geschwister, kein Elternteil. Sie
entstehen VOR dem Umsetzen der Pivots - in der Gesamtdatei stehen alle Teile
noch an ihrer Originalposition.
**Die Huellen umschlossen das Laub mit.** `_tree_ucx` las alle Vertices eines
Teils, also auch die Blatt-Cards. Gemessen am `baum`-Preset war die Huelle
dadurch **1.4- bis 2.1-mal** so gross wie das Holz - beim obersten Stammsegment,
das die Krone traegt, 1.67x. Jetzt nur noch Material-Slot 0 (Rinde). Die
Spielseite hatte das fuer ihre Interim-Boxen laengst so gemacht.
**Und das Aufweiten war fuer schlanke Teile kaputt.** Nach dem Vereinfachen
wurde die Huelle wieder aufgeblasen, damit alles hineinpasst - durch Skalieren
um die Huellenmitte. Bei langgestreckten Teilen entgleist das: liegt eine
Flaeche fast durch die Mitte, wird der Hebel winzig und der noetige Faktor
explodiert. Gemessen an einem Stammsegment (Laenge zu Dicke rund 13:1)
**Faktor 4.2**. Bei der dicken Eiche blieb es bei 1.0-1.4 - deshalb ist es nie
aufgefallen. Erst der Wechsel auf Rinde-only machte die Teile schlank genug,
dass es durchschlug.
Jetzt wird nicht mehr skaliert, sondern je Flaeche der ECHTE Fehlbetrag
gemessen (wie weit liegt der schlimmste Quellpunkt vor ihrer Ebene?) und die
Vertices um genau diesen Betrag nach aussen geschoben - eine Verschiebung in
Metern, kein Verhaeltnis, sie kann nicht explodieren. Das laeuft ITERATIV: ein
Vertex gehoert mehreren Flaechen an und wandert entlang seiner eigenen Normale,
die Ebene verschiebt sich also nur um den projizierten Anteil. Ein Durchgang
liess einen Rest stehen (der Test schlug an), nach wenigen ist er weg. Bleibt
doch etwas uebrig, faellt die Funktion auf die unvereinfachte Huelle zurueck -
ein paar Vertices mehr kosten nichts, eine vierfach zu grosse Kollision schon.
Ergebnis am 12-teiligen Baum: alle Huellen 14-31 Verts (Budget 32), keine
groesser als das 1.35-fache ihres Holzes. Gegenproben: ohne Rinde-Filter 10
Fehler (1.41x bis 2.06x), ohne Huellen in der Gesamtdatei 1 Fehler.
### Stamm-UCX fuer den Stand-Baum
Die Stand-Baeume bekommen eine konvexe Kollisionshuelle um den **Stamm**, vom
Fuss bis zum Kronenansatz. Bewusst keine Kronen-Huelle: gegen die liefe der
Spieler, bevor er den Baum ueberhaupt beruehrt.
Die Trennung kommt aus `ast_id` (0 = Stamm) - dasselbe Attribut wie beim
Ursprung und bei den Faell-Teilen. Kronenansatz ist der tiefste Punkt mit
ast_id > 0, plus ein Zehntel Stammhoehe Luft, weil der tiefste Astpunkt eine
herabgebogene Spitze sein kann. Ohne Aeste (Kaktus-Setzling) ist die ganze
Pflanze Stamm - und genau die soll dann die Kollision sein.
**Name und Hierarchie sind nicht geraten, sondern vom Fels-Export abgeschaut**,
der in der Spiel-Pipeline schon abgenommen ist: dort exportiert eine LOD-Gruppe
als `UCX_<LODGroup-Name>`, und zwar als GESCHWISTER des LodGroup-Empty, nicht
als Kind - Unreal liest jedes Kind der Gruppe als LOD-Stufe. Nachgemessen im
Fels-FBX, bevor eine Zeile Baum-Code entstand.
Ergebnis im reimportierten Baum-FBX:
```
Eiche_01_LOD0..3 MESH parent = Eiche_01_LODGroup
Eiche_01_LODGroup EMPTY parent = -
UCX_Eiche_01_LODGroup MESH parent = - 12 Verts
```
Zwei Dinge, die erst die Messung gezeigt hat:
* Die Huelle rutschte **4-6 cm unter den Boden**. Das Vereinfachen auf 12 Verts
schneidet Ecken ab, und das anschliessende Aufblasen (damit wieder alles
hineinpasst) skaliert um die Huellenmitte - dabei wandert die Unterseite nach
unten. Im Spiel waere das eine Kollision, die aus dem Terrain herausragt.
Jetzt wird der Boden auf Z = 0 geklemmt.
* Der erste Anlauf des Tests rechnete die FBX-Skala aus der **Gesamthoehe**
zurueck - die Blatt-Cards ragen aber ueber die Rinde hinaus, die Skala kam zu
gross heraus und die Huelle sah zu klein aus. Jetzt kommt der Massstab aus
Material-Slot 0. Ein Massstab, der etwas anderes misst als die Referenz,
verschiebt jede Aussage.
`tests/test_stamm_ucx.py` prueft an vier Faellen (gerader Stamm, krummer Stamm
mit Bend 1.5, Obstbaum, und einmal ohne LODs): genau eine Huelle, richtiger
Name, kein Elternteil, 8-24 Verts, Radius zwischen Stammradius und dem
2.5-fachen davon, Fuss auf Z = 0, Hoehe nicht ueber den Kronenansatz hinaus.
Gegenprobe ohne die Boden-Klemmung: vier Fehler.
Drei bestehende Tests zaehlten die Huelle als Render-Mesh mit und schlugen zu
Recht an - sie filtern jetzt `UCX_`.
### Die Kronen-Huelle: Kugeln verschmelzen, dann streuen
Der "behaarte Finger"-Look kam zurueck, weil wir einen Schritt der
Standard-Technik uebersprungen hatten. Die Produktions-Breakdowns (80.lv
"Meadows", "The Witness"-Baeume) machen es so: Kugeln auf die Aeste setzen,
sie zu EINER Kronenmasse verschmelzen, und die Cards ueber deren **Oberflaeche**
streuen. Wir hatten je Cluster eine eigene Halbkugel bestueckt - die Polster
blieben dadurch als getrennte Koerper sichtbar, ein Laubhandschuh je Ast.
Jetzt: Kugeln instanzieren -> `Mesh to Volume` -> `Volume to Mesh` (das ist die
metaball-artige Verschmelzung) -> `Distribute Points on Faces` auf der Huelle.
Die Aeste verschwinden im Inneren der Krone. Vom User im Wald abgenommen.
**Drei Befunde auf dem Weg, alle gemessen statt geraten:**
`ast_id` kam nur an 3 von 10 Aesten an. Ursache war NICHT das Sampling, sondern
die Eingabe: es existierten nur **vier Cluster-Zentren auf drei Aesten**. In der
alten Bauform regelte die Bisektion die Cluster-Dichte selbst hoch; seit sie
stattdessen die Cards auf der Huelle regelt, muss der Startwert stimmen.
Sichtbar wurde das erst mit `Points to Vertices` - `to_mesh()` auf eine reine
Punktwolke liefert eine LEERE Mesh und misst nichts. Ein Messwerkzeug, das nicht
messen kann, sieht aus wie ein Befund.
S1 stand komplett kahl. Die Reihe ueber die Stufen zeigte **0 / 20 / 41 / 109**
Zentren - allmaehlicher Abfall bis null, keine Klippe. Also die Cluster-Dichte,
nicht die Voxel-Groesse. "Dichte" heisst Kugeln je m2 ZWEIGflaeche, und die
schrumpft mit dem QUADRAT der Groesse; sie laeuft jetzt mit 1/g^2 gegen die
Baumgroesse, damit die ANZAHL konstant bleibt. Das ist die richtige Groesse:
eine Krone braucht eine bestimmte Zahl Ballen zum Verschmelzen, nicht eine
bestimmte Dichte. Damit ist die Bauform skalenfrei - kein Sonderweg fuer
Setzlinge.
31 % der Cards lagen unter dem Kronenansatz. Die Verschmelzung weitet die Form
um bis zu einen Kugelradius nach unten, der Filter griff aber nur auf den
Zentren. Jetzt auch auf der Huelle - sonst waere der Fehler, gegen den der
Filter gebaut wurde, durch die Hintertuer zurueckgekommen.
### Weniger Blaetter, und warum die ZAHL der richtige Hebel war
Nach der Abnahme wollte der User "etwas weniger Blaetter". Zwei Hebel standen
zur Wahl, beide gemessen (Eiche):
| | Cards | Tris | Blattflaeche je Huellenflaeche |
|---|---|---|---|
| vorher | 381 | 3714 | 10.36 |
| Ziel -15 % | 319 | 3342 | 8.64 |
| Cards -15 % kleiner | 381 | 3714 | 7.93 |
Die Zahl zu senken kostet weniger Deckung UND spart 372 Dreiecke; die Cards zu
verkleinern kostet mehr Deckung (die Flaeche geht quadratisch) und spart kein
einziges. Der woertliche Wunsch war ausserdem der messbar bessere.
### Das Formband gehoert um das Abgenommene, nicht umgekehrt
Nach dem Umbau lag `u/m` bei 0.78-1.02 gegen eine Schranke von 0.82. Die
naheliegende Reaktion waere gewesen, die Krone auf die Zahl zu kalibrieren.
Stattdessen: alle Referenzformen unter der NEUEN Bauform neu durchmessen.
| Form | u/m | o/m |
|---|---|---|
| Eiche | 1.04-1.16 | 0.79-0.91 |
| **abgenommen** | **0.78-1.02** | **0.68-0.79** |
| Trichter | 0.68-0.78 | 1.04-1.14 |
| Pilz | 0.59-0.90 | 0.82-0.95 |
| Haube | 0.94-1.03 | 0.71-0.78 |
**Daraus folgt etwas Unbequemes: `u/m` trennt nichts mehr.** Der abgenommene
Bereich ueberlappt den Pilz und stoesst an den Trichter. Die 0.82 stammten aus
der Einzelkappen-Aera - dagegen zu kalibrieren haette die Krone verbogen, um
eine Zahl zu retten, die ihre Aussagekraft verloren hat. Dieselbe Falle wie beim
Kugel-Kennwert eine Runde zuvor, nur eine Kennzahl weiter.
`o/m` trennt dagegen sauber (abgenommen bis 0.79, Pilz ab 0.82, Trichter ab
1.04) und traegt die Formpruefung jetzt allein. `u/m` wird nur noch
mitgedruckt - ein Kriterium, das die Fehlformen nicht mehr ausschliesst, ist
schlimmer als keines: es sieht aus wie ein Schutz und ist keiner.
### Der Hochstamm muss auf JEDER Stufe stehen
Bis 1.46 war am Obstbaum nur S4 kalibriert. Im Welt-Test ist der User auf einen
S3 gestossen, der sich als duenne Astgabel mit Laubklumpen las statt als
Obstbaeumchen. Gemessen ueber die Stufen lag der Kronenansatz bei
**10 / 16 / 22 / 29 %** - den freien Stamm gab es nur ganz am Ende.
Ursache ist der allgemeine Setzlings-Faktor 0.35 auf `Branch Start`. Der bildet
die Selbstbeschneidung eines WILDEN Baums ab: junge Baeume tragen ihre Aeste
tiefer und asten sich erst spaeter auf. Beim Obstbaum stimmt das nicht - ein
**Hochstamm wird gezogen, nicht gewachsen**: die Baumschule veredelt hoch und
nimmt die unteren Triebe weg, der freie Stamm ist von Anfang an da. Genau daran
haengt der Hochstamm-Charakter, und genau den verlor der Baum in den
Zwischenstufen.
`GROWTH_OVERRIDES["obstbaum"] = {"Branch Start": 1.0}` - jetzt 29 % auf allen
vier Stufen. `tests/test_obstbaum.py` prueft das ueber die ganze Wachstumsreihe;
Gegenprobe ohne die Ausnahme: drei Fehler.
Nebenbei ein Definitions-Hinweis, der beim Vergleich mit der Spielseite wichtig
war: dort wird "Ansatz" ueber den tiefsten BLATT-Vertex gemessen, hier ueber den
tiefsten AST-Vertex. Die Spielseite sah deshalb einen Einbruch in der Mitte
(33/17/13/21 %), wir eine monotone Reihe - dasselbe Preset, zwei Definitionen.
Der Befund stimmte trotzdem: die Zwischenstufen hatten keinen freien Stamm.
### Blattwerk als Halbkugel-Bueschel, und groessere Baeume
Drei Aenderungen, die zusammengehoeren.
**Neue Bauform.** Die Cards sassen einzeln auf der Astkurve - aus der Naehe sah
man deshalb nackte Ast-Enden mit einem Mini-Bueschel daran. Jetzt wird jeder
gestreute Punkt ein CLUSTER-ZENTRUM, und die Cards sitzen auf der oberen
Halbkugel darum herum. Die Astspitze verschwindet im Polster, weil das Zentrum
auf ihr liegt. Untere Halbkugel weggelassen: von unten sieht man die Krone
gegen den Himmel, das waere die Haelfte des Overdraws fuer nichts.
Zwei Annahmen kassiert die Messung: `GeometryNodeMeshIcoSphere` liefert bei
Subdivisions 0 UND 1 dasselbe Ikosaeder - ein Cluster hat **sechs** Cards, nicht
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
das Sechsfache (2706 Cards an der Eiche).
**Ast-Spitzen-Deckung: zwei echte Ursachen.** Der Kronenansatz-Filter stand auf
25 % der Baumhoehe, die Tanne setzt ihre Aeste aber ab 12 % an - bei 5 von 22
Aesten lag das aeussere Fuenftel KOMPLETT unter dem Filter. Er ist jetzt auf den
tatsaechlichen `Branch Start` der Art gedeckelt. Und haengende Spitzen fielen
unter den Filter: es zaehlt jetzt der ANSATZ des Astes (`anchor`) mit, nicht nur
die Punkthoehe.
**Groessere Baeume**, proportional skaliert (Hoehe, Astlaenge, Stammradius
zusammen - Hoehe allein macht Stangen mit Minikrone): Eiche 9.3 m, Birke 11.0,
Tanne 13.0, Weide 7.0. Alle unter dem 4300er-Deckel.
Gemessene Spitzen-Deckung (Anteil der Aeste mit Laub im aeusseren Fuenftel):
| | Aeste gedeckt | aeusseres Fuenftel |
|---|---|---|
| Eiche, Baum | 100 % | 100 % |
| Weide | 100 % | 90 % |
| Obstbaum | 100 % | 93 % |
| Tanne | 86 % | 86 % |
| Birke | 100 % | 72 % |
**Die Metrik selbst musste zweimal korrigiert werden.** Erst mass sie den
Abstand von der Astspitze zur naechsten Card-MITTE - viel zu streng: eine Card
ist 0.85 m gross, ihre Mitte darf einen halben Meter weg sein und die Spitze
trotzdem im Laub liegen. Bei 1.9 m Astlaenge kamen so 24 % heraus, obwohl das
Polster genau auf der Spitze sass. Jetzt zaehlt DECKUNG: das aeussere Fuenftel
gilt als gedeckt, wenn dort eine Card-Mitte naeher ist als eine halbe
Cardlaenge.
### Kronenradius: ueberall das Perzentil, nicht das Maximum
Von der Spielseite gemeldet, hier nachgemessen: seit den Bueschel-Schalen
streut der Kronenradius viel breiter als vorher.
| Preset (S4) | p50 | p90 | max |
|---|---|---|---|
| Obstbaum | 1.35 | 2.12 | 3.17 |
| Eiche | 1.12 | 1.69 | 2.37 |
| Tanne | 1.17 | 2.49 | 3.91 |
Das Maximum haengt an einer einzelnen weit aussen sitzenden Card. Die
Deckungs-Zahl faellt damit **2.3-mal pessimistischer** aus (Obstbaum 1.2 statt
2.8 m2/m3) - und weil das Verhaeltnis max/p90 je Art verschieden ist (Tanne
1.57, Eiche 1.40), verzerrt es auch den VERGLEICH gegen die Eiche, den der Test
fuehrt. Das Breitenprofil rechnete laengst mit p90; der Kronenzylinder war die
Stelle, die uebersehen wurde.
Der Kronenradius fuer die Frucht-Bedingung (>= 1.80 m) bleibt das Maximum ueber
die RINDE - Bark ist von den Schalen nicht betroffen, dort aendert sich nichts.
### Kugel-Kennwert relativ, Bbox-Kriterium ersetzt
Die Bueschel-Schalen schieben jede Card um bis zu einen Kappenradius vom Ast
weg, und zwar in ALLE Richtungen: die Krone gewinnt ringsum Breite, in der Hoehe
aber nur oben. Der Obstbaum fiel dadurch von Kugel 1.35-1.47 auf 0.95-1.23,
ohne dass an seiner Form etwas falsch war.
Zwei Hypothesen dazu sind an der Messung gescheitert - beide klangen plausibel:
der Kappen-Faktor 1.47 des Obstbaums (entkoppelt: 0.93-1.14 -> 0.95-1.23, fast
nichts) und ein groesserer Kappen-Radius (1.3 und 1.6: 59-77 % -> 58-75 %, fast
nichts). Der Radius waechst in alle Richtungen und verschiebt das Verhaeltnis
deshalb kaum. Was wirkt, ist die Kronenbreite selbst.
Die Schranke ist jetzt RELATIV: der Obstbaum muss die rundeste Art bleiben,
hoechstens 65 % des Eichenwerts aus demselben Lauf. Eine neue Konstante haette
bei der naechsten Bauform dasselbe Schicksal gehabt.
**Und das Bbox-Kriterium ist raus.** "Bbox-Mitte == 0" stammt aus der Zeit vor
der Stammbasis-Konvention 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. An seine Stelle tritt der BOUNDS-RADIUS, an dem das
Culling haengt - geprueft nicht gegen eine Konstante, sondern gegen die
Blender-Messung: Hoehe und Radius muessen denselben Skalenfaktor tragen.
### Vom Trichter ueber die Haube zur Kugel
Zielbild ist der **Lollipop** der stilisierten Vektor-Illustration: kurzer
kraeftiger Stamm, darauf eine Kugelkrone. Der Weg dorthin fuehrt NICHT ueber
mehr Baumhoehe - 4.9 m sind die Ernte-Reichweite -, sondern ueber einen
tieferen Kronenansatz: 39 % -> 29 %, Stamm 1.9 m -> 1.5 m. Die Krone wird
dadurch von selbst hoeher (3.8 m -> 4.3 m) und rundet sich, tiefere Fruechte
sind fuers Ernten ohnehin besser.
Dazu `Crown Taper` 0.15 -> 0.35, `Crown Bulge` 1.1 -> 1.5, `Branch Droop`
0.4 -> 0.25.
**Das Messen war hier schwieriger als das Einstellen.** Zwei Fehler, beide
gefunden, bevor sie in einer Abnahme gelandet sind:
1. Die Kronenbreite je Drittel als **Maximum** ueber die Card-Punkte zu
messen haengt an EINER Card: bei ~400 Cards von 1.25 m reicht eine einzige
weit aussen haengende, um ein Drittel zu bestimmen. Unten/mitte sprang je
Seed zwischen 0.89 und 1.00, waehrend der Mittelwert stabil bei 0.94 lag -
das war Rauschen, keine Form. Mit dem **90. Perzentil** ist es stabil, und
es zeigte prompt etwas anderes: die Krone war unten tatsaechlich am
breitesten (u/m 1.04), was das Maximum verdeckt hatte.
2. Der Kugel-Kennwert bekam zunaechst die Schranke 0.88 - eine Zahl aus einem
Messskript mit anderer Breitendefinition. Mit der Test-Formel liegen die
Werte um 1.4, die Schranke haette alles durchgelassen. **Zum zweiten Mal
dieselbe Falle** (nach der Deckungs-Schwelle): eine Schranke aus einem
anderen Kontext ist keine Schranke.
Deshalb sind jetzt ALLE Referenzformen mit demselben Mass gemessen und stehen
im Test:
| Form | unten/mitte | oben/mitte | Kugel (KronH/KronB) |
|---|---|---|---|
| Eiche | 1.04-1.15 | 0.77-0.88 | 1.71-1.91 |
| Trichter (1.39) | 0.66-0.72 | 1.18-1.31 | 0.98-1.12 |
| Haube (1.40) | 0.99-1.13 | 0.69-0.80 | 1.26-1.44 |
| Pilz (Verirrung) | 0.75-0.90 | 0.78-0.94 | 1.05-1.17 |
| **Kugel (1.41)** | **0.86-1.01** | **0.63-0.74** | **1.35-1.47** |
Die Tabelle zeigt auch, was NICHT trennt: der Kugel-Kennwert unterscheidet die
Kugel nicht von der Haube (1.35-1.47 gegen 1.26-1.44). Was sie unterscheidet,
ist der Kronenansatz - und genau der wird geprueft. Ein Test, der so tut, als
pruefe er die Form, waehrend in Wahrheit ein anderer Wert die Arbeit macht,
waere schlimmer als keiner.
Gegenproben: die Haube meldet 9 Fehler (Kronenansatz), der Trichter 37 (alle
vier Formkriterien).
### Obstbaum-Silhouette: Trichter statt Krone
Der Baum erfuellte Kronenradius, Deckung, Ansatz und Tri-Budget - und sah
trotzdem falsch aus. Gemessen wurde das erst am BREITENPROFIL der Krone in
Dritteln (unten/mitte/oben):
| | unten | mitte | oben | oben/mitte |
|---|---|---|---|---|
| Eiche (liest sich als Baum) | 3.0 | 2.8 | 2.6 | **0.90** |
| Obstbaum vorher | 3.3 | 4.5 | 4.7 | **1.07** |
Nach oben BREITER: die Laubmasse sass an den top-aussen endenden Astspitzen,
unten war die Krone hohl. Ein Trichter, keine Krone.
Kein einzelner Regler hat das geloest - `Crown Taper`, `Branch Droop`,
`Crown Bulge` und `Attraction Up` einzeln durchgefahren liessen das Verhaeltnis
bei rund 1.0 stehen. Der Grund: die Kronenform folgt nicht der Astlaenge je
Hoehe, sondern dem WEG DER ASTSPITZEN. Solange die Aeste aufwaerts schwingen
(`Attraction Up` positiv), landet das aeusserste Laub oben - egal wie die
Laengen verteilt sind.
Die Loesung ist eine andere Bauform, kein Feintuning: **kurze Aeste an einem
hohen Stamm** (Height 3.8 -> 4.9, Branch Length 2.55 -> 1.25) statt langer
Aeste an einem kurzen. Dazu `Attraction Up` +0.40 -> -0.05 (kein Aufschwung),
`Crown Taper` -0.30 -> +0.15 (oben kuerzere Aeste) und `Crown Bulge` 0.60 ->
1.10 (Bauch in der Mitte).
Ueber alle neun Produktions-Seeds: oben/mitte **0.63-0.84**, Kronenradius
1.88-2.01 m, Deckung 100-134 % der Eiche, 3752-4166 Tris, Kronenansatz 39 %.
Beim Einregeln kippte die Form zwischendurch in eine PILZform (unten am
breitesten, u/m 1.17) - deshalb steht das Profil jetzt als Test da und nicht
als Kommentar. `tests/test_obstbaum.py` prueft oben/mitte <= 0.85; Gegenprobe
mit den Werten aus 1.39 meldet neun Fehler bei 0.98-1.11.
### Kronenfuellung: es zaehlt die Flaeche, nicht die Card-Zahl
Der Obstbaum las sich in der Engine als einzelne belaubte Ast-Schlaeuche mit
Himmel dazwischen - "behaarte Finger". Die Card-Zahl erklaert das nicht: 400
Cards koennen eine geschlossene Haube sein oder fuenf Schlaeuche. Was es
erklaert, ist die Blattflaeche pro Kronenvolumen:
| | Cards | Krone | Flaeche/Volumen |
|---|---|---|---|
| Eiche (schliesst) | 444 | r 1.26 x 3.78 = 18.9 m3 | **24.6 m2/m3** |
| Obstbaum vorher | 399 | r 2.00 x 2.88 = 36.2 m3 | **11.4 m2/m3** |
Die Obstbaum-Krone hat fast das doppelte Volumen bei gleicher Card-Zahl.
**Durchgemessen und verworfen: mehr Zweigwerk.** Sub Count 5 und 7, Sub Length
1.0 und 1.3, Sub Start 0.20 und 0.30, Branch Count 12 und 14 - der Fuellgrad
bewegte sich um hoechstens 7 Punkte, die Dreiecke stiegen auf 4310-5162. Der
Grund ist strukturell: neue Zweige wachsen aus denselben wenigen Hauptaesten
und vergroessern dabei die Krone. Das Volumen waechst mit, die gedeckelte
Card-Zahl nicht.
**Ebenfalls verworfen: mehr Cards.** Ziel 740 (rechnerisch noetig fuer 25/m3)
ergab 5426 Tris - 26 % ueber Budget - und hob den Fuellgrad nur auf 75 %.
Cards zaehlen ist nicht dasselbe wie Flaeche decken.
**Ebenfalls verworfen: kleinere Krone.** Branch Length 2.35 sah ueber drei
Seeds mit Kronenradius 1.83 m gut aus - im Neun-Seed-Test fielen dann SECHS
Seeds unter die 1.80 m, die die Frucht-Streuung braucht (1.73-1.79).
Was bleibt, ist die Card-GROESSE (`LEAF_SIZE_PRESET`): sie kostet kein einziges
Dreieck, weil die Zahl gedeckelt ist. Sie kostet OVERDRAW, und der ist bei
Bueschel-Cards der eigentliche Engpass. Faktor 1.47 hebt die Deckung von 46 auf
87-114 % der Eiche und verdoppelt die Card-Flaeche (414 -> 893 m2), bei 3697
statt 3714 Tris.
Der Test vergleicht gegen die **im selben Lauf gemessene Eiche**, nicht gegen
eine feste Zahl. Die Deckung haengt an der Card-Flaeche - eine 6-Tri-Card
bringt das Dreifache einer flachen Raute -, und der erste Anlauf des Checks ist
genau daran gescheitert: die Schwelle stammte aus einem Messskript mit anderer
Card und meldete neun Fehler auf einem korrekten Baum.
### Zwei Knoepfe, ein Weg
Blattwerk entsteht an zwei Stellen: ueber das Haekchen "_Leaf anlegen" beim
Erzeugen und ueber den Knopf "Blattwerk streuen". Nur der zweite rechnete die
Wachstums-Staffel. Von der Spielseite am Modifier nachgemessen trug der
S1-Setzling aus dem Create-Pfad **Size 0.85, Max Radius 0.04, Min Height 0.25** -
exakt die Altbaum-Werte.
Das ist zum dritten Mal dieselbe Fehlerklasse (nach der Stufen-Deckung und
`build_group`): **zwei Quellen fuer dieselbe Frage**. Die Staffel liegt deshalb
jetzt in `make_leaves` selbst, die sie aus `growth_t` und `preset` am
Baum-Objekt zieht - ein Aufrufer KANN sie nicht mehr vergessen. Darueber sitzt
`blattwerk_streuen()` als der eine Einstieg, den beide Knoepfe benutzen; dort
haengt auch die Dichte-Regelung, die vorher ebenfalls nur der Operator hatte.
`tests/test_zwei_pfade.py` prueft nicht die absoluten Werte, sondern die
DIFFERENZ zwischen beiden Wegen - ein Test, der nur einen Weg anschaut, findet
so etwas grundsaetzlich nie. Zusaetzlich prueft er, dass sich S1 und S4
ueberhaupt unterscheiden: waeren beide Wege gleich falsch, waere der Vergleich
allein zufrieden.
### Der generische Baum war auch eine Saeule
Gemessen Breite/Hoehe **0.18** ueber fuenf Seeds - schmaler als jedes andere
Laubgehoelz im Satz. Ursache dieselbe wie bei der Birke: `Branch Up` 1.3 aus den
DEFAULTS. Zielband der Spielseite 0.45-0.60, gemessen jetzt **0.55**
(0.52-0.56).
Die DEFAULTS bleiben unangetastet - sie sind die Socket-Vorgaben der
Node-Gruppe, an denen jeder haendische Regler haengt. Nur `baum` bekommt eigene
Werte; alle anderen Presets ueberschreiben `Branch Up` ohnehin.
### Obstbaum: die Krone muss groesser sein als eine Card
Das Preset ist nicht nur eine Silhouette, es ist eine FUNKTIONALE Bedingung.
Die Frucht-Streuung setzt Fruechte auf die Aussenhaut der Krone und schiebt sie
radial nach aussen. Am generischen `baum` war der Kronenradius 0.78 m bei
1.28 m Card-Diagonale - die Cards waren groesser als die Krone, es gab keine
Haut, auf die sich etwas setzen laesst.
`obstbaum` ist ein eigenes Preset, kein Umbau von `baum`: der wird anderswo
verwendet, und ein Hochstamm ist keine Standard-Silhouette. Proportion belegt -
beim Hochstamm liegt der Kronenansatz bei 180-220 cm, die Krone selbst ist 3-4 m
hoch; 1.8 m Ansatz plus 3 m Krone sind 4.8 m Gesamthoehe und ein Ansatz bei
knapp 40 %.
Gemessen ueber alle neun Seeds der Spielseite (Apfel 1/15/22, Kirsche 2/16/23,
Birne 3/17/24): Hoehe 4.91-5.12 m, Kronenradius 1.83-2.05 m, Kronenansatz
38-40 %, Kronenradius/Card 2.15-2.42, 3560-3824 Tris.
Zwei Fallen beim Einregeln, beide gemessen statt vermutet:
* `Height` ist die STAMMlaenge, nicht die Baumhoehe - die Aeste ragen darueber
hinaus. Mit Height 4.8 kam der Baum auf 5.4-6.2 m.
* Der Kronenansatz zaehlt gegen die GESAMThoehe. `Branch Start` 0.37 ergab
gemessen 29 %, nicht 37 %.
`tests/test_obstbaum.py` prueft die funktionale Bedingung ueber alle neun Seeds.
**B/H gilt fuer dieses Preset nicht** (Entscheidung der Spielseite,
2026-08-06). Das urspruengliche Band 0.55-0.75 und Kronenradius >= 1.80 m bei
4-5 m Hoehe lassen zusammen nur ein 4 % breites Fenster zu, waehrend die
Streuung von Seed zu Seed rund 12 % betraegt. Zweimal ist die Abwaegung
zugunsten des Kronenradius ausgefallen - beim zweiten Mal wurde daraus eine
Regel: **Kronenradius >= 1.80 schlaegt B/H <= 0.75**, die gemessenen 0.77-0.82
sind der Preis der Frucht-Tragflaeche. Die Form sichert ohnehin das
BREITENPROFIL ab, und das trennt schaerfer: B/H haette sowohl den Trichter
(oben/mitte 1.07) als auch die Pilzform (unten/mitte 1.17) durchgelassen.
### Card-Groesse misst sich an der Baumhoehe
Die absolute Card-Diagonale sagt nichts - sie haengt daran, wie gross die Card
im Atlas gezeichnet ist. Aussagekraeftig ist ihr Verhaeltnis zur BAUMHOEHE, und
das war schief: mit 0.45/0.63/0.82/1.00 schrumpfte die Card ueber die Stufen um
Faktor 2.2, der Baum aber um 6.2. Gemessen an der Birke **27 % / 14 % / 11 % /
9 %**, an der Eiche sogar 33 % gegen 11 %. Der Setzling trug relativ dreimal so
grosses Laub wie der Altbaum - ein Laubpaket auf einem Stiel.
Jetzt 0.24/0.46/0.75/1.00, gemessen 14 % / 10 % / 10 % / 9 %. Nicht ganz flach:
ein junger Baum traegt seine Bueschel relativ groeber, und wer ihn auf
Altbaum-Proportion zwingt (0.18 durchgemessen, 11 %), macht ihn kahl.
Zwei Messfallen, an denen ich mich fast selbst verrannt haette:
* Die Card-Groesse ueber Face-Index-Paare zu messen setzt voraus, dass je zwei
aufeinanderfolgende Faces eine Card bilden. Die Insel-Zaehlung braucht die
Annahme nicht - beide stimmten hier zwar ueberein, aber nur die zweite belegt
das auch.
* Ein Kontrollrender zeigte eine riesige Card, die es laut Messung nicht gab.
Es war die QUELL-Card am Ursprung. Sie in Y aus dem Weg zu schieben half
nicht, weil eine orthografische Kamera Tiefe ignoriert - sie muss
`hide_render`. Zweimal beinahe eine Fehldiagnose aus einem Bild.
### Der Ursprung sitzt an der Stammbasis, nicht am tiefsten Ast
Zweimal war dieser Fehler schon "behoben". Erst kam der Ursprung aus der
Gesamt-Bbox - eine tief haengende Blatt-Card hob den ganzen Baum an. Dann aus
Material-Slot 0, was die Cards ausnahm, aber die AESTE gehoeren ebenfalls zum
Rinden-Slot. Gemessen an Tanne_01_S1 (Aeste ab 6 % Stammhoehe, haengend): der
tiefste Slot-0-Punkt lag bei **-5.35 cm**, der tiefste STAMM-Punkt bei
**-0.02 cm**. Die Stammbasis schwebte 5.3 cm ueber dem Boden, weil eine
Astspitze den Nullpunkt bestimmte.
Die exakte Trennung liefert `ast_id` (0 = Stamm) - dasselbe Attribut, das der
Baum ohnehin fuer die Faell-Teile traegt. Ein Schwellwert auf den Achsabstand
waere die naheliegende Alternative und faellt beim Kaktus um: dessen Stamm ist
dicker als jeder sinnvolle Schwellwert. X/Y kommt aus dem UNTERSTEN RING statt
aus der Stamm-Bbox; bei einem krummen Stamm (Bend 1.5 am toten Baum) liegt die
Bbox-Mitte irgendwo in der Kruemmung.
`tests/test_ursprung.py` misst am reimportierten FBX. Die erste Fassung des
Tests suchte den "tiefsten Punkt nahe der Achse" - und die Gegenprobe hat sie
umgeworfen: bei der Tanne faellt eine Astspitze ins Achsband, der Test meldete
Fuss = 0, obwohl der Stamm 5 cm darueber schwebte. Ein Schwellwert, der die
Bugfassung durchlaesst, prueft nichts. Jetzt wird ein STAMMQUERSCHNITT gesucht:
Vertices, die die Achse in allen Himmelsrichtungen umschliessen. Eine Astspitze
tut das nicht.
Nebenbefund: ein Baum ohne Aeste traegt gar kein `ast_id` - das Attribut
entsteht auf dem Ast-Zweig des Graphen, und den schaltet `Branch Count 0`
komplett ab (Kaktus-Setzling). Dann ist ohnehin alles Stamm, der Fallback ist
also exakt und nicht bloss ausreichend.
### Weide: Ziel-Cardzahl je Preset
"Volle Dichte" auf S4 heisst: das Ergebnis haengt an der Zweigflaeche, und die
faellt je Art verschieden aus. Die Weide traegt die meiste und landete gemessen
bei 5456-5594 Tris ueber drei Seeds, gegen ein Stufenbudget von 4300. Mit der
Deckelung auf 350 Cards: 3890-3962 Tris.
`LEAF_STUFEN_PRESET` haelt solche Ausnahmen. Das macht die Tabelle
preset-abhaengig - bewusste Entscheidung der Spielseite, statt das Budget fuer
alle anzuheben: die Weide ist der Ausreisser, nicht die Regel.
### Birke: keine Saeule
Gemessen war die alte Birke Breite/Hoehe **0.15** - schmaler als der
Standardbaum (0.22) und nicht einmal die Haelfte der Eiche (0.36). Ursache war
`Branch Up` 1.1: die Aeste zeigten fast senkrecht nach oben und klebten am
Stamm.
Zielband aus zwei Quellen, die sich ueberlappen: Van den Berk nennt fuer
*Betula pendula* 15-20 m Hoehe bei 8-12 m Kronenbreite (B/H 0.40-0.80), der
Woodland Trust 15-25 m bei 6-10 m (0.24-0.67). Schnittmenge rund 0.35-0.55;
gemessen jetzt **0.51**.
Den Habitus nennt Van den Berk woertlich: "spreading branches have clearly
pendulous and thin twigs". Das ist der Palmen-Bogen im Kleinen - der Ast steigt
erst und kippt dann ab -, deshalb `Branch Droop` mit spaeter `Droop Curve`.
Negatives `Sub Up` waere der naheliegende Griff gewesen und bewegt hier nichts:
durchgemessen von +2.2 bis -2.0 blieb der Anteil haengender Vertices bei 1-6 %,
weil die Sub-Aeste zu kurz sind, um die Silhouette zu tragen. Mit Droop 0.8 sind
es 26 % bei einem Fall von 0.033 x Hoehe - vergleichbar der Tanne (0.030), weit
weg von der Weide (0.213). Die Zweige haengen, der Baum nicht.
### Die Node-Gruppe wird WIEDERVERWENDET, nicht neu gebaut
`build_group()` loeschte die Gruppe `GN_Tree` bei jedem Erzeugen und legte sie
neu an — damit riss sie sie **allen bereits erzeugten Baeumen** unter dem
Modifier weg. Gemessen: nach vier Arten-Erzeugungen lebte nur noch die letzte,
alle frueheren evaluierten leer. Dass in der Praxis direkt nach dem Erzeugen
exportiert wurde, hat das lange verdeckt.
Jetzt prueft ein Waechter zwei Dinge, bevor er ueberhaupt neu baut: die
abgelegte **Schema-Version** (`TREE_SCHEMA`) *und* die tatsaechlich vorhandenen
Eingaenge. Die Version allein reichte nicht — wer waehrend der Entwicklung
Sockets aendert, ohne die Zahl anzufassen, bekaeme sonst eine veraltete Gruppe
untergeschoben. Muss doch neu gebaut werden, werden **bestehende Modifier auf
die neue Gruppe umgehaengt**. Beide Pfade sind gemessen: vier Arten
nacheinander leben alle, und nach einem erzwungenen `build_group(rebuild=True)`
ebenfalls.
### Saemlinge: Mindestradius sticht McMahon
Die Skalierung nach McMahon (Durchmesser ~ Hoehe^1.5) macht duennstaemmige
Arten am Setzling zu Draehten: gemessen kam die Birke auf **0.6 cm** Fussradius
gegen **1.9 cm** der Eiche — dreimal duenner, waehrend der Unterschied beim
Altbaum nur 1.2x betraegt. Die Regel gilt fuer die Statik ausgewachsener Baeume;
ein Saemling ist eine Pflanze, kein Balken.
`SEEDLING_MIN_RADIUS` und `SEEDLING_MIN_FLARE` heben die duennen Arten an, ohne
die dicken (Tanne, Palme, Kaktus) anzufassen. Danach liegen alle bei **2.8 cm**.
`tests/test_proportionen.py` laesst die McMahon-Abweichung genau dort zu, wo der
Boden greift — und prueft, dass sie nicht weiter geht.
### Faell-Teile (Punkt 4)
Kein Laufzeit-Zerlegen: beim Faellen tauscht das Spiel die ISM-Instanz gegen
einen Actor aus vordefinierten Teilen. Knopf **Faell-Teile exportieren**:
| Teil | Inhalt |
|---|---|
| `<Name>_Piece_Stamm01..03` | Stammsegmente, Schnittflaechen mit Deckel |
| `<Name>_Piece_Ast01..N` | je ein Ast **samt seinen Sub-Aesten und Blatt-Cards** |
| `<Name>_Pieces_All.fbx` | alle Teile an ihrer Originalposition, gemeinsamer Ursprung |
Je Teil: zwei Material-Slots, Pivot = **Massenmittelpunkt**, eigene `UCX_`-Huelle.
**Die Ast-Zugehoerigkeit ist ein Attribut, keine Schaetzung.** Der Generator legt
`ast_id` an den Astpunkten ab (0 = Stamm, 1..N = Aeste); Sub-Aeste erben die ID
ihres Astes automatisch, weil sie aus Punkten auf dessen Kurve entstehen —
gemessen: bei `Sub Count` 0 hat jeder Ast 28 Verts, bei 3 dann 64, der Stamm
bleibt bei 156. Beim Blatt-Streuen wird `ast_id` am Punkt abgegriffen und auf
die Card gespeichert. „Welche Card gehoert zu welchem Ast" ist damit eine
Nachschlage-Frage statt einer Nearest-Neighbor-Heuristik — und **exakt
nachzaehlbar**:
> `tests/test_faell_teile.py` prueft die **Blatt-Bilanz**: Summe der Cards ueber
> alle Teile == Cards des Stand-Baums. Gemessen **170 = 170**.
**Falle dabei:** Die Z-Schnitte am Stamm zerschnitten anfangs auch Blatt-Cards —
aus einer wurden zwei, die Bilanz stand auf 178 gegen 170, und die Atlas-UVs der
Haelften waeren kaputt gewesen. Eine Card ist ein **unteilbares** Billboard:
sie wandert jetzt als Ganzes in das Segment, in dem ihre Inselmitte liegt.
Nur die Rinde wird geschnitten.
Alles am **reimportierten** FBX gemessen: 12 Teile, UCX 1832 Verts im Budget,
Pivot maximal 16 mm vom Schwerpunkt, 0 non-manifold Kanten, Gesamtdatei 5.41 m
hoch (= volle Baumhoehe, die Teile sind also deckungsgleich rekonstruierbar).
### Proportionen: gemessen, nicht geschaetzt
Drei Presets sahen falsch aus. Die Ursachen wurden gemessen bzw. belegt, nicht
geraten - `tests/test_proportionen.py` haelt die Werte fest:
| Was | vorher (gemessen) | jetzt | Quelle fuer den Zielwert |
|---|---|---|---|
| **Palmenkrone** | Radius/Hoehe **0.11** - ein Mop-Kopf am Stamm | **0.30** | `tree-gen palm.py` (Weber&Penn): Wedellaenge 0.3 x Stamm; TreeIt-Referenzpalme 3.3 m bei 4.33 m = 0.38 |
| **Busch** | 1.81 m hoch, 0.84 m breit (B/H **0.46**) - ein Baeumchen | 1.20 m hoch, 2.0 m breit (B/H **1.6**) | Strauch = mehrere Stiele ab Boden, kein Leittrieb; gemessen nach Hoehe UND Breite |
| **Setzling** | relativ **2.4x dicker** als der Altbaum (r/H 0.025 gegen 0.0104) | proportional gleich schlank | McMahon, elastische Aehnlichkeit: Hoehe ~ Durchmesser^(2/3), also Faktor 0.16^1.5 |
Der Wedel-Winkel kam dazu: mit `Branch Up 0` standen die Wedel als gerade
Antennenspeichen ab. `tree-gen` gibt `down_angle 60` + `curve 40` an - also
schraeg raus und dann ueberbiegen. Gemessen steigen die Wedel jetzt 1.1 m ueber
den Ansatz und kippen dann ab.
**Nicht jede Pflanze waechst wie ein Laubbaum** (`GROWTH_OVERRIDES`):
- **Palme:** Einkeimblaettrige haben kein Dickenwachstum - erst der Durchmesser,
dann die Hoehe. „Once a palm stem emerges from the apical meristem, its
diameter is fixed for life" (UF/IFAS ENH1212). Der Setzling ist ein kurzer
Stumpf in fast voller Dicke, kein Schoessling.
- **Kaktus:** Sukkulent, gleiche Logik. Ausserdem **ohne Arme** - Saguaros
bilden die erst mit 50-70 Jahren, bei rund 2 m Hoehe (NPS).
- **Weide:** haengt erst, wenn die Zweige lang genug sind. Junge Weiden wachsen
aufrecht; mit dem allgemeinen Droop reichten ihre Zweige 0.27 m unter den
Stammfuss und das Asset waere in UE geschwebt.
**Dabei gefunden:** `Branch Count = 0` schaltete die Aeste **nicht** ab -
dieselbe Falle wie seinerzeit bei `Sub Count` (`Curve to Points` klemmt Count
auf mindestens 1). Fuer die Sub-Ebene gab es laengst einen Geometry-Switch, fuer
die Hauptaeste nicht. Jetzt liegen beide Ebenen hinter demselben Schalter.
Ebenfalls gefunden: im Busch-Preset stand `Branch Count` **zweimal** im selben
Dict - der zweite Eintrag hat den ersten still ueberschrieben.
Parametrischer **Geaest**-Generator (Stamm + Aeste + Sub-Aeste) als Geometry-Nodes-
Gruppe. Blaetter/Krone macht man selbst — das Addon liefert die Struktur.
- **Presets:** `baum`, `palme`, `busch`, `tanne`, `eiche`, `birke`, `weide`,
`toter_baum`, `kaktus` — die Silhouette steuert die **Kronenform**
(`Crown Taper` / `Crown Bulge`, Idee aus modular_tree / Weber-Penn).
- **Tri-Budget:** alle Presets liegen unter dem Projektlimit (Baum ≤2500,
Busch ≤800). Stellschrauben: `Detail` (Segmente) und `Sides` / `Branch Sides` /
`Sub Sides` (Profilaufloesung je Ebene — Stamm rund, Zweige nur Dreiecke).
- **Wachstums-Stufen:** Haken setzen + Anzahl waehlen → Setzling bis ausgewachsen,
**gleicher Seed = dieselbe Baum-Identitaet** (fuer Wachstums-Systeme im Spiel).
- **Live bearbeiten:** Nach dem Erzeugen liegen alle Regler am Modifier `GN_Tree`
im Viewport ziehen, der Baum aktualisiert sich sofort.
- **Blattwerk:** Ein Blatt-/Billboard-Objekt als *Blatt-Card* waehlen (z. B. eine
Diamond-Plane mit Alpha-Textur) → der Button streut es auf die **Zweige** und legt
das Ergebnis als `<Name>_Leaf` mit identischem Ursprung ab. Gefiltert wird ueber
den Astradius (`Max Radius`), damit keine Blaetter am Stamm kleben. Ohne Card
bleibt `_Leaf` leer zum Selbermodellieren.
- **Tri-Budget:** Der Blattwerk-Button meldet `Stamm + Blatt = Summe` und **warnt**
bei Ueberschreitung. Default-Dichte 150 haelt auch das schwerste Preset (Eiche,
1428 Tris) mit 2460 von 2500 Tris im Budget.
- **Export:** „Modifier anwenden" friert das Ergebnis als normales Mesh ein.
**v1.1.0:** Aeste folgen jetzt der Elternkurve (Tangent/Normal aus *Curve to
Points*, Goldener Winkel = Phyllotaxis) statt einer aus Weltkoordinaten
gerechneten Richtung — Sub-Aeste standen vorher wie Nadeln quer zum Ast.
Neu: `Tip Blunt` (Baum laeuft spitz aus, Kaktus endet stumpf).
Der `kaktus` nutzt `Droop Curve` fuer den Saguaro-Ellbogen (Arm laeuft flach raus
und knickt dann steil hoch). Offen ist dort nur, dass die Rippen (`Ribs`) im
Shading kaum sichtbar sind.
---
# Stylized Rock Generator (Blender Addon)
Batch-Generator fuer stylized Rocks in Blender, ausgelegt auf den Export nach
**Unreal Engine 5** ueber FBX.
Pipeline pro Rock: Basis-Mesh (Cube / Ico-Sphere) → Subdivision Surface →
Displace mit prozeduraler Noise-Textur (Voronoi / Clouds / Distorted Noise) →
zweistufiges Decimate → Limited Dissolve → Normalen-Reparatur →
UV0 (Smart Project) + UV1 (Lightmap) → optional Bevel/Weighted Normal, Material,
LOD-Set, randomisierte Skalierung/Rotation.
Getestet mit **Blender 5.1.2** (Minimum laut `bl_info`: 4.2).
---
## Installation
1. `stylized_rock_generator.py` herunterladen.
2. Blender → *Edit → Preferences → Add-ons → Install…* → die `.py` waehlen.
3. Haken bei **Stylized Rock Generator** setzen.
4. Panel: *View3D → Sidebar (N) → Reiter "Rock Gen"*.
Update: einfach die neue `.py` erneut installieren (gleicher Modulname) oder das
Repo lokal ziehen und den Addon-Pfad in den Preferences auf den Repo-Ordner
zeigen lassen.
## Installation als Extension (via URL, mit Auto-Update)
> **Ein Repo, vier Extensions.** `blender --command extension server-generate`
> scannt `dist/` und listet jedes gefundene Zip - eine URL liefert in Blender
> alle vier Addons. `build.ps1` ruft das mit `--html` auf, dann entsteht
> zusaetzlich eine `index.html` zum Durchblaettern (auf einem statischen
> Webserver; Gitea liefert rohes HTML als Text aus).
>
> **Kein Versions-Archiv:** zwei Zips derselben Extension-ID nebeneinander sind
> nicht der Weg, eine alte Version zum Zurueckrollen bereitzuhalten -
> server-generate warnt dann (`archive found with duplicates ... conflicting
> blender versions`). Mehrere Versionen einer ID sind nur erlaubt, wenn sie
> verschiedene Blender-Bereiche abdecken. Alte Version wieder her: per
> `git checkout` des Zips.
Ab Blender 4.2 / 5.x als **Remote-Extension-Repository** einmal die URL
eintragen, danach gehen Updates per Klick.
1. Das Gitea-Repo muss **oeffentlich** sein (sonst kann Blender die Raw-URL nicht
ohne Login laden).
2. Blender → *Edit → Preferences → Get Extensions → oben rechts das Dropdown →
Repositories → „Add Remote Repository"*.
3. Als URL eintragen:
```
https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator/raw/branch/main/dist/index.json
```
4. „Check for Updates" aktivieren, bestaetigen. Die Extension erscheint in der
Liste → *Install*.
**Updaten (Maintainer-Seite):**
1. `version` in `blender_manifest.toml` (und `bl_info["version"]` in der `.py`) erhoehen.
2. `./build.ps1` (oder `blender --command extension build …` + `… server-generate …`).
3. `git add -A && git commit -m "vX.Y.Z" && git push`.
4. Nutzer: in Blender *Check for Updates* → *Update*.
Das gebaute Zip + `index.json` liegen in `dist/` und werden bewusst mitversioniert,
damit die Raw-URL sie ausliefert.
---
## Features
- **Basis-Formen:** Wuerfel, **Quader** oder Ico-Sphere, einstellbare Subsurf-Stufen.
Der Quader hat frei einstellbare `Quader-Proportionen` (X/Y/Z) plus
`Proportions-Streuung` — damit entstehen flache **Platten** und langgestreckte
**Riegel** statt nur runder Brocken. Gemessen bei Proportion (2.0, 1.5, 0.35):
3.43 / 2.56 / 0.79 m — Z also 23 % von X.
Die Proportionen werden auf **gleiches Volumen normiert** — „M" bedeutet damit
bei jeder Form dasselbe. Eine Platte ist dann flacher UND breiter als ein
Wuerfel derselben Klasse, aber nicht schwerer; ihre laengste Kante ist
entsprechend groesser (das ist Absicht, kein Fehler).
Die Proportionen werden auf die MESH-DATEN angewendet, nicht auf das Objekt:
der Displace nutzt globale Texturkoordinaten, eine Objekt-Skalierung wuerde
das Rauschen mitverzerren statt die Form zu aendern.
- **Noise/Displace:** Voronoi / Clouds / Distorted Noise, Scale + Staerke, Seed.
Seed-Variation ueber einen moderaten Welt-Offset (±25) vor dem Displace-Apply
Blenders prozedurale Texturen haben keinen echten Seed-Parameter.
- **Cleanup:** zweistufiges Decimate + Limited Dissolve, danach
`normals_make_consistent` (verhindert eingefallene/dunkle Flaechen).
- **UV fuer Unreal:** UV0 (Smart Project) fuer Material-Texturen, UV1 (Lightmap)
als separater, nicht ueberlappender Kanal fuer Static-Mesh-Lightmaps.
- **Hard-Surface (optional):** Bevel + Weighted Normal fuer einen "clean"
Kantenlook.
- **Material (Platzhalter):** weist pro Rock ein Material zu und rotiert durch
eine kleine Palette (Granite/Basalt/Sandstone/Mossy), damit nichts materiallos
exportiert wird.
- **LOD-Set:** erzeugt LOD0LODn als LOD-Gruppe fuer Unreal (siehe unten).
- **Presets:** eingebaute Presets (Kleiner Kiesel, Grosser Bruchfels, Lava-Rock)
plus eigene Presets als JSON (`rock_presets.json` neben dem Addon).
- **Batch:** synchron fuer kleine Mengen, ab `modal_threshold` (Default 25)
automatisch **modal mit Progress-Bar** die UI bleibt bedienbar, ESC bricht ab.
- **Robustheit:** Faellt ein einzelner Rock aus (z. B. 0 Faces nach zu
aggressivem Decimate), wird nur dieser uebersprungen und aufgeraeumt, der Rest
des Batches bleibt erhalten. Fehler landen in der System-Konsole.
---
## LOD-Export nach Unreal
Blenders FBX-Exporter kennt **keine** eigene LOD-Option. Unreal erkennt eine
LOD-Gruppe aber, wenn das Eltern-**Empty** die Custom Property
`fbx_type = "LodGroup"` traegt und die LOD-Meshes in Reihenfolge (LOD0…LODn)
darunter geparentet sind. Genau das erzeugt der Generator.
Beim FBX-Export darauf achten:
- **Custom Properties** aktivieren (sonst geht `fbx_type` verloren).
- Nur die LOD-Gruppe (Empty + Kinder) selektieren, `use_selection` an.
Beim Import in Unreal:
- Im FBX-Import-Dialog **Import LODs** aktivieren.
Referenz:
[Importing Static Mesh LODs Using FBX (UE Docs)](https://dev.epicgames.com/documentation/en-us/unreal-engine/importing-static-mesh-lods-using-fbx-in-unreal-engine).
---
## Tests
Alles auf einmal:
```bash
pwsh -File run_tests.ps1
```
Einzeln geht weiterhin `blender --background --python tests/<datei>.py`,
gefiltert `.\run_tests.ps1 -Filter panels`.
> **Warum der Runner existiert.** Die Tests lagen einzeln herum und wurden nur
> beim Bauen des jeweiligen Features gestartet. Beim Aufraeumen der Panels stellte
> sich heraus, dass **fuenf davon still veraltet waren**: sie prueften gegen
> Objekt-Namen aus der Zeit vor dem Benennungssystem (`Rock_` statt `Fels_A_M_01`),
> gegen fest verdrahtete Zip-Versionen, oder zaehlten die spaeter dazugekommenen
> `_Leaf`-Objekte mit. Sie haetten eine echte Regression nicht gemeldet. Die drei
> Muster, die das ausgeloest haben, sind jetzt vermieden: Objekte ueber die
> **Differenz** vorher/nachher finden statt ueber ein Namens-Praefix, Zips per
> **glob** statt mit fester Version, und Zusatz-Ebenen beim Zaehlen herausfiltern.
| Datei | prueft |
|---|---|
| `tests/test_baum_export.py` | Stand-Baum: ein Mesh, zwei Slots, Ursprung, **Kugel-Normalen im exportierten FBX** |
| `tests/test_blatt_bueschel.py` | Bueschel-Defaults, Wachstums-Kopplung der Card-Groesse, Stufen-Budget 800/1200/4000 |
| `tests/test_baum_lod.py` | LOD-Kette: 4 Stufen unter LodGroup, Cards werden nach oben duenner, **Kugel-Normalen auf jeder Stufe**, kein Hoehenversatz |
| `tests/test_faell_teile.py` | Faell-Teile: **Blatt-Bilanz 170=170**, Pivot am Massenmittelpunkt, UCX je Teil, Gesamtdatei mit gemeinsamem Ursprung |
| `tests/test_blatt_varianten.py` | Card-Sammlung: alle Varianten werden gezogen, seed-stabil, Einzelobjekt weiter moeglich |
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
| `tests/test_bruchstuecke.py` | Bruchstuecke: Volumen 100 % des Muttersteins, dichte Topologie, genau ein Erz-Stueck, Material-Slots, Ursprung, UCX je Teil |
| `tests/test_schnellstart.py` | Schnellstart: jede Sorte liefert ohne Nacharbeit Geometrie + Slot, Erz-Sorte ueber der Dichte-Schwelle, Dissolve-Bereich streut je Stein |
| `tests/test_erz.py` | Erz-Maske: A=1.0 ohne Erz, Anteil monoton, Set-Namen/Groessen, Einstellungen zurueckgesetzt, FBX-Roundtrip, Tri-Fenster |
| `tests/test_export_robust.py` | Export am **reimportierten** FBX: Ursprung erzwungen, flach = liegend, UCX geschlossen/umschliessend/im Budget, LOD+Vertex-Farben |
| `tests/test_panels.py` | Alle vier Addons: registrieren, jedes Panel zeichnet, Unter-Panels im richtigen Tab, **jede Property irgendwo erreichbar** |
| `tests/test_tree_addon.py` | Tree-Addon: Presets, Wachstums-Stufen, UVs, Modifier-Apply |
| `tests/test_ecogame_rock.py` | Fels-Benennung, Bake liefert nur `_albedo`/`_normal`, AO im Albedo |
| `tests/test_ecogame_tree.py` | Baum-Benennung `<Art>_<Variante>_S<Stufe>`, Ursprung, Lightmap-Default |
| `tests/test_randomize.py` | Zufalls-Funktion von Rock und Tree, `_Leaf`/`_Frucht` mit gleichem Ursprung |
| `tests/test_leaves.py` | Blattwerk: Streuung, Ursprung, Tri-Budget, leeres `_Leaf` ohne Card |
| `tests/test_rock_classes.py` | Groessen-Klassen S/M/L/XL unterscheiden sich, Streuung bleibt formbezogen |
Die Suiten erwarten die gebauten Zips in `dist/` bzw. lesen die `.py` direkt.
Erzeugt 3 Rocks, prueft je Rock: keine losen Vertices, manifold, Normalen nach
aussen (signed volume > 0), UV0 + UV1 vorhanden, und gibt eine Zusammenfassung
(Vertex-/Face-Count) aus. Exit-Code 0 = alles bestanden, 1 = Fehler.
---
## Changelog v2.3.0
- **Textur-Bake auf Zell-Look umgestellt:** Voronoi „Distance to Edge" liefert
hand-painted Zellen mit dunklen Outlines statt organischem fBm-Rauschen.
Neu regelbar: **Grundfarbe** und **Zellgroesse**. Weiterhin nahtlos tileable.
## Changelog v2.2.0
- **Textur-Bake-Button:** backt ein nahtlos **tileables** Stylized-Rock-Set
(BaseColor / Normal / Roughness / AO) als PNG. Nahtlos durch **4D-Torus-
Projektion** der UV (`cos/sin` von U und V) → tilet in beide Richtungen ohne
Naht, ideal fuer **Triplanar** in Unreal.
- Anleitung zur UE-Material-Verdrahtung: siehe
[docs/UE_Material_Triplanar.md](docs/UE_Material_Triplanar.md).
## Changelog v2.1.0
- **FBX-Export-Button (UE-ready):** exportiert jeden Rock / jede LOD-Gruppe als
eigene FBX mit `use_custom_props=True` (fuer `fbx_type=LodGroup`), Triangulate
und passendem Smoothing (`EDGE` bei Auto-Smooth, sonst `FACE`).
*UE-Import-Tipp:* Normal Import Method auf „Import Normals" stellen, „Import LODs" an.
- **Shading-Toggle:** Faceted / Smooth / Auto-Smooth-nach-Winkel.
- **Pro-Rock-Varianz:** Noise-Scale & Displace-Staerke zwischen Min/Max plus
optionale nicht-uniforme Skalierung → ein Batch wirkt variantenreich.
- **Boden abflachen (Bisect) + Origin unten-mittig** → Rocks stehen in UE plan auf.
## Changelog v2.0.0
Gehaertet & erweitert gegenueber v1.0.0:
- **Bugfix:** `smart_project(angle_limit=…)` ist in Blender 4.x/5.x in **Radiant**
(`[0, 1.5708]`) der alte Wert `66.0` war ungueltig. Jetzt `radians(66°)`.
- **Bugfix:** `dissolve_limited(angle_limit=…)` erwartet **Radiant**; der
Grad-Wert wurde ungewandelt durchgereicht → uebermaessiges Dissolve. Jetzt
`radians(dissolve_angle)`.
- **Bugfix:** Der Lightmap-Kanal wurde vor `lightmap_pack` nicht als aktiver
UV-Layer gesetzt → UV0 konnte ueberschrieben werden. Jetzt explizit gesetzt,
UV0 danach wieder als Render-Kanal.
- Fehlerbehandlung um jeden `bpy.ops.*`-Aufruf; einzelne Rock-Fehler brechen den
Batch nicht mehr ab.
- Nicht-blockierender modaler Batch-Modus mit Progress-Bar.
- Bevel + Weighted Normal als Toggle.
- LOD-Set als `fbx_type=LodGroup`-Gruppe.
- Preset-System (JSON) mit Panel-Dropdown.
- Platzhalter-Material-Zuweisung.