Die Zusammenbau-Datei 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 auf ihrem Massenmittelpunkt), der Stand-Baum folgt der Stammbasis seit 1.36 - nur diese eine Datei fiel heraus. Der Versatz kommt aus DERSELBEN Funktion wie beim Stand-Baum: die Rechnung ist aus _origin_bottom_center als _stammbasis_versatz herausgeloest und wird von beiden Wegen benutzt. Zwei Rechnungen fuer denselben Nullpunkt waeren genau die Fehlerklasse, die hier schon dreimal zugeschlagen hat - und hier haette sie besonders weh getan, weil die zusammengesetzten Truemmer exakt auf dem Platz des Baums liegen muessen. Angewendet nur fuer diesen Export, danach zurueckgenommen: mit "Teile behalten" staenden sie sonst verschoben neben dem Baum in der Szene. test_faell_teile stellt den Baum jetzt bewusst auf (18, -7.5) und prueft den Stammfuss ueber den QUERSCHNITT auf Fusshoehe, nicht ueber die Bbox-Mitte - die Krone haengt einseitig und wuerde den Fehler verwischen. Gegenprobe ohne Versatz: Stammfuss bei 18.000 / -7.500, exakt der gemeldete Wert. 27 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1310 lines
68 KiB
Markdown
1310 lines
68 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 · 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 | 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
|
||
`<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 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
|
||
|
||
### 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.
|
||
|
||
### 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 LOD1–3
|
||
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** — 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.
|
||
|
||
> **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 1–3 nur diese aufdrueckt, laesst ihre Weltmatrix
|
||
> ungebacken: sie landen bei **minus der Standposition**. Gemessen an einem Baum
|
||
> bei x = 18 m: LOD1–3 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.
|
||
|
||
### 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_`.
|
||
|
||
### 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 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 `<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_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.
|