# 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 = 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: `__S`, z. B. `Birke_01_S1` … `Birke_01_S4`. Zusatz-Ebenen haengen hinten an: `_Leaf`, `_Frucht`. - Texturen: `_albedo` und `_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 | 17–25° | ~1160 V / ~2320 Tris | | **Platte** | flach, liegt satt auf | 17–25° | ~670 V / ~1340 Tris | | **Kantig** | Bevel + Weighted Normal, Flat | 8–14° | ~350 V / ~690 Tris | | **Erzbrocken** | dicht genug fuer die Nugget-Maske, Slot `M_Rock_Ore` | 17–25° | ~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 17–25°: 1936–2928 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 `_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 `_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 1500–3000. 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_` 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 `_` 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 4–6 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. ### LOD-Kette (Stand-Baum) Haken `LOD-Kette erzeugen`: exportiert `_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 laeuft ueber die **Dichte**, nicht ueber Decimate: eine Card besteht aus zwei Dreiecken und laesst sich nicht vereinfachen — man muss weniger davon setzen. Damit die Krone dabei nicht ausduennt, werden die verbleibenden Cards groesser. LOD3 ist der Impostor-Ersatz. | 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** — LOD1–3 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. 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. ### 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 | |---|---| | `_Piece_Stamm01..03` | Stammsegmente, Schnittflaechen mit Deckel | | `_Piece_Ast01..N` | je ein Ast **samt seinen Sub-Aesten und Blatt-Cards** | | `_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 18–32 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 `_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 LOD0–LODn 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/.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_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 `__S`, 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.