Kein Laufzeit-Fracturing im Spiel - die Teile werden beim Anschlagen als Physik-Actors gespawnt, entstehen hier also als vollwertige Assets. Knopf "In Bruchstuecke zerlegen" arbeitet auf der Auswahl: <Name>_Piece_01..NN, 2-4 Teile, Bruch-Seed, optional Mutterstein ausblenden. Jedes Teil bekommt Vertex-Layout v2, Ursprung unten-mittig, die gleichen Material-Slots wie der Mutterstein und beim Export eine eigene UCX-Huelle. Punkt 4a: "Ein Stueck mit Erz" benennt ein zufaelliges Teil <Name>_Piece_Erz und schreibt Erz-Adern in dessen A-Kanal. Alle anderen Teile behalten A = 1.0. Ob es gespawnt wird, entscheidet die Loot-Logik im Spiel. Zusammenpassende Bruchkanten ohne Abstimmung: 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. Bewusst ohne das Cell-Fracture-Addon (muesste aktiviert sein, haengt an Operator-Kontext); bmesh laeuft immer. Die Falle, die mich mehrere Anlaeufe gekostet hat: jeder Schnitt muss SOFORT geschlossen werden, und zwar ueber genau die Kanten aus geom_cut. Der naheliegende Weg - am Ende alle Randkanten suchen und fuellen - erzeugte Kanten mit vier Flaechen (hauchduenne Splitter, 0.000007 m2 gegen Median 0.0025); raeumte man die ab, blieben Loecher. dissolve_degenerate half nicht, weil es nach KantenLAENGE geht und ein langer duenner Splitter keine kurze Kante hat. Aufgefallen ist das Ganze am Volumen, nicht am Auge: die Teile kamen zusammen auf 72.7 % des Muttersteins, weil calc_volume bei offenen Meshes Unsinn liefert. Jetzt 100.2 % bei 2, 3 und 4 Teilen, 0 offene und 0 non-manifold Kanten. Genau dieser Vergleich steht im Test - er ist zugleich das Warnsignal fuer kaputte Topologie. Neu: tests/test_bruchstuecke.py. 17 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
585 lines
30 KiB
Markdown
585 lines
30 KiB
Markdown
# 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 |
|
||
| 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: `Fels_<Form>_<Klasse>` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`.
|
||
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 — 1 = Erz, 0 = kein Erz. Ohne Erz-Bake bleibt der Kanal 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.
|
||
|
||
**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.
|
||
|
||
---
|
||
|
||
## 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`.
|
||
|
||
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. Alle anderen Teile behalten
|
||
A = 1.0. 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_<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
|
||
|
||
### 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 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/<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_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_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.
|