From 20a751edca094a27fa3beafc687b98e275e38d45 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Mon, 3 Aug 2026 08:02:14 +0200 Subject: [PATCH] Rock Gen 2.14.0: Export-Haertung (Wunschliste 1-3) + LOD/Vertex-Farben geklaert Punkt 1 - Ursprung erzwingen (keine Option mehr) Der Export schrieb die Objekt-Transformation mit ins FBX. Dadurch trug das 25er-Set seine Szenen-Rasterpositionen (bis 20 m) im Asset und die Steine schwebten im Spiel neben ihrem Platzierungspunkt. Jetzt wandert jedes Objekt an den Welt-Ursprung, Origin unten-mittig der Bbox. Am REIMPORTIERTEN FBX gemessen: Bbox-Mitte 0.000/0.000, min.Z 0.000. Punkt 2 - flache Steine hinlegen Kuerzeste Bbox-Achse zeigt nach oben. Gedreht wird nur, wenn es etwas bringt: bei 2.53/1.40/1.54 wuerde Kippen die Form nur willkuerlich drehen. Punkt 3 - UCX-Kollisionshuellen Konvexe Huelle als UCX_ im selben FBX, Budget einstellbar (Default 32 Verts). Gemessen 29-32 Verts, geschlossen, umschliesst den Stein. Alles auf temporaeren KOPIEN - die Szene des Users bleibt unveraendert (Test prueft Namen und Objektzahl vorher/nachher). Offener Punkt der Spielseite beantwortet: LOD-Gruppen + Vertex-Farben gingen NICHT. Der LOD-Zweig in make_one_rock sprang komplett am Cavity-Bake vorbei - gemessen: kein Farbattribut auf einer der vier Stufen, dazu fehlten Origin und Material. Jetzt wird auf LOD0 gebacken, BEVOR dupliziert wird; das Decimate interpoliert mit, damit alle Stufen dieselbe Cavity zeigen und der LOD-Wechsel nicht poppt. Nach Roundtrip: LOD0 40 V R 0.30..1.00, LOD1 21 V, LOD2 11 V, LOD3 6 V - alle mit Farben. Vier Fallen, die der Test aufgedeckt hat: - Blender haengt an Kopien ".001" an; Unreal ordnet UCX ueber den EXAKTEN Mesh-Namen zu -> Original bekommt fuer die Exportdauer einen Hilfsnamen. - bmesh.from_mesh bringt die Originalflaechen mit, convex_hull legt seine Huelle obendrauf -> 16-21 offene Kanten. Nur die PUNKTE einlesen. - Jede Vereinfachung schneidet Ecken ab, die Huelle wird KLEINER als der Stein (der Spieler wuerde einsinken). Jetzt exakt so weit aufgeblasen, dass wieder alles hineinpasst - fuer Kollision ist minimal zu gross unkritisch. - ob.matrix_world = Identity loescht die Transformation NICHT bei geparenteten Objekten; bei einer LOD-Gruppe auf (13/-7.5/2.2) landete das Mesh bei MINUS dieser Position. matrix_basis und matrix_parent_inverse direkt nullen. Neu: tests/test_export_robust.py misst am reimportierten FBX statt an der Szene - nur so faellt eine mitgeschriebene Transformation ueberhaupt auf. 15 Tests bestehen. Co-Authored-By: Claude Opus 4.8 --- README.md | 61 +++++ blender_manifest.toml | 2 +- dist/index.html | 6 +- dist/index.json | 8 +- dist/stylized_rock_generator-2.13.0.zip | Bin 22263 -> 0 bytes dist/stylized_rock_generator-2.14.0.zip | Bin 0 -> 26683 bytes stylized_rock_generator.py | 311 +++++++++++++++++++++++- tests/test_export_robust.py | 244 +++++++++++++++++++ 8 files changed, 619 insertions(+), 13 deletions(-) delete mode 100644 dist/stylized_rock_generator-2.13.0.zip create mode 100644 dist/stylized_rock_generator-2.14.0.zip create mode 100644 tests/test_export_robust.py diff --git a/README.md b/README.md index b69a781..7dde1c5 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,66 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung: --- +## Export-Haertung (Rock Gen) + +Drei Dinge passieren beim FBX-Export jetzt automatisch. Alles laeuft auf +**temporaeren Kopien** - die Szene bleibt unveraendert. + +**1. Ursprung wird erzwungen** (keine Option). Jedes Objekt wandert an den +Welt-Ursprung, der Ursprung sitzt unten-mittig der Bounding-Box. Anlass: das +25er-Set trug seine Szenen-Rasterpositionen (bis 20 m) im Mesh, die Steine +schwebten im Spiel neben ihrem Platzierungspunkt. Nachgemessen am +**reimportierten** FBX: Bbox-Mitte 0.000/0.000, `min.Z` 0.000. + +**2. Flache Steine werden hingelegt** (`Flache Steine hinlegen`, Default an). +Die kuerzeste Bbox-Achse wird nach oben gedreht. Gedreht wird nur, wenn es etwas +bringt - bei einem 2.53/1.40/1.54-Stein wuerde Kippen die Form nur willkuerlich +drehen. Im Spiel gibt es nur zufaelligen Yaw, die gebackene Ausrichtung ist also +endgueltig. + +**3. UCX-Kollision** (`UCX-Kollision mitexportieren`, Default an). Konvexe Huelle +als `UCX_` im selben FBX, Unreal nimmt sie als Simple Collision. Budget +ueber `UCX max. Verts` (Default 32). + +Drei Fallen, die dabei aufgefallen sind: + +- Blender haengt an Kopien `.001` an. Unreal ordnet die Kollision aber ueber den + **exakten** Mesh-Namen zu - das Original bekommt fuer die Dauer des Exports + einen Hilfsnamen, die Kopie den echten. +- `bmesh.from_mesh` bringt die **Originalflaechen** mit, `convex_hull` legt seine + Huelle dann obendrauf: gemessen 16-21 offene Kanten. Es duerfen nur die + **Punkte** eingelesen werden. +- Jede Vereinfachung der Huelle schneidet Ecken ab, sie wird dadurch **kleiner** + als der Stein - eine Kollision kleiner als das Mesh laesst den Spieler + einsinken. Deshalb wird sie danach exakt so weit aufgeblasen, dass wieder alles + hineinpasst. + +### LOD-Gruppen + Vertex-Farben (war offen, jetzt geklaert) + +Ging **nicht**: der LOD-Zweig sprang komplett am Cavity-Bake vorbei, LOD-Gruppen +kamen ohne Farbattribut aus dem Export - auf keiner der vier Stufen. Jetzt wird +auf LOD0 gebacken, **bevor** dupliziert wird; das Decimate interpoliert die +Farben mit. Wuerde je Stufe neu gebacken, haette jede Stufe ihre eigene Cavity +und der LOD-Wechsel wuerde poppen. Nach dem FBX-Roundtrip gemessen: + +| Stufe | Verts | R-Bereich | +|---|---|---| +| LOD0 | 40 | 0.30..1.00 | +| LOD1 | 21 | 0.41..0.92 | +| LOD2 | 11 | 0.45..0.92 | +| LOD3 | 6 | 0.49..0.88 | + +Dazu die zweite Falle: `ob.matrix_world = Identity` loescht die Transformation +**nicht**, wenn das Objekt ein Eltern-Objekt hat - Blender rechnet eine lokale +Gegen-Matrix aus. Bei einer LOD-Gruppe auf (13/-7.5/2.2) landete das Mesh dadurch +bei genau MINUS dieser Position. `matrix_basis` und `matrix_parent_inverse` +muessen direkt genullt werden. + +Die UCX-Huelle haengt bewusst **neben** dem LodGroup-Empty, nicht darunter - +Unreal wuerde jedes Kind der Gruppe als weitere LOD-Stufe lesen. + +--- + ## Stylized Asset Utils Fuer alles, was **von Hand** gebaut wird (Pilze, Baumstumpf, Erzfels, Props) und @@ -391,6 +451,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`. | Datei | prueft | |---|---| | `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 | +| `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 | diff --git a/blender_manifest.toml b/blender_manifest.toml index 135c553..a811693 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.13.0" +version = "2.14.0" name = "Stylized Rock Generator" tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export" maintainer = "D4rkst3r" diff --git a/dist/index.html b/dist/index.html index 0999c73..bd963b4 100644 --- a/dist/index.html +++ b/dist/index.html @@ -40,14 +40,14 @@ 4.9KB - stylized_rock_generator-2.13.0 + stylized_rock_generator-2.14.0 Stylized Rock Generator Batch-Generator fuer stylized Rocks mit UE5-Export link 4.2.0 - ~ all all - 21.7KB + 26.1KB stylized_tree_generator-1.25.0 @@ -61,6 +61,6 @@ -

Built 2026-08-01, 20:46

+

Built 2026-08-03, 06:02

diff --git a/dist/index.json b/dist/index.json index 3a3577b..adc6c38 100644 --- a/dist/index.json +++ b/dist/index.json @@ -57,7 +57,7 @@ "id": "stylized_rock_generator", "name": "Stylized Rock Generator", "tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export", - "version": "2.13.0", + "version": "2.14.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -73,9 +73,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.13.0.zip", - "archive_size": 22263, - "archive_hash": "sha256:5ae39f5035435ae51803f63e1a0d06b67d8c68a5477e9fdb663a4a21c65d2492" + "archive_url": "./stylized_rock_generator-2.14.0.zip", + "archive_size": 26683, + "archive_hash": "sha256:f60aa4d5c8d73cabfccbccf19020ea30d31e77ad22dca5f8a7151e46c7ec1c42" }, { "schema_version": "1.0.0", diff --git a/dist/stylized_rock_generator-2.13.0.zip b/dist/stylized_rock_generator-2.13.0.zip deleted file mode 100644 index 193e963333b6aadf34ffc97321ed8f31d86bd594..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22263 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
5#`7=PAUd<mQRlaS%5qWk>w1P%v@ z8sK#20Y$&qgc>?pb{M!bpEOMe%4QY!W|i|Y#1*w1Em5%34^Au$wh@#@wi?JZR6fc` zVmhZ?aR$OqOu(RH$)%J_a*krarUC^Krf42$zbSL-SkZNNZBNqMlBny3ymi*;XfogA zOO|(IPpy}(&Nj=^!{tWgi2Ed9p%9J@&cwFg(f zd08f5Z(ivezF6|AZBWkcKReRky0H@0uhepN*(?;iKqU35=k0XSXZ^Dd?Xsdi*F^RD z44Bfjz_P1Gr5gE=#EvEk$hP}xQyizVHNQ+iE>|-&n5T3;+GLlpHGO8P${#}f2!7k( zN%uBgP$1AxM=Vw5-H;_qvq45{2xU%Vfi-@%9DVi=&6Kd#YEl1Eb$5d#Kf94yl5$=x zrTNN#1H9{&%C@*QoXFcr9!NR;eOFIxCOPv_a?_u0v6vo<*mDh)5)D;#YSEpuuig01 zg{?XVI-~n)GJrVt4B6s$>lC2CZss)V_Ge~{DM zrF#MwsKdpy2a!hW@o@dL(*z?;$>O(Tv4Kd1Mx2Ju?7w1AF5@geN%aSWd-Q&; zGCq9mB!71jNpw=oO|b2sTrc)3+y*vdd~gxoj#H_(w_Y<0=6=%ly~#OcuhX0hpxz&U zX;4J8gM1A#FNmWClZogUrmNZ>0yp^)>-Ah2w=5hnGZQeix6tF%_be}t(*0-E!AH@L zCJ^%B-8ge(2rwV2}1e}f* zd||Q(nUWy6kyH7FGKj3UFjA#S-}p+u`Q_*WOz;i!ROo|$;}1k@Bg;sMr@+*v&$Q)6 zkmRivNP0!%Jq+`Jf=6qHD^*D<#iE$#8qqXGxGPyR8?4kfWxjQV@QZZ5vBu5FFT&fM z7DOudZ{u-N(2(zU)FwL*y}*EbFStHlir{V@TnRP4QcA)pX6vcm(2vEJV4s1b;qt}uv zGhV;uQXNqwW|wgI0Bku-2xpvv-;6ZWSoB+mHcNB%2MqRw#tJ)DCMRl_+Z=s`4Ps1A zzNm|%H77B-`M3p--eB-J|FQV<&qYb}IJ~?@ei4=Ipfc`89Pj<~_KZ2f!%#C}87<2@ zGkh|3QWM41!^_t_F4Bzs;kO%POWJF9ehy`==(PJyb`mip#lK3}f^hgHXdpU;OsUXw zcT9!@4rrTCFZHZ!_t5hNpr@X?1I15qtYWozaU#a?uS^FiW-|CV3!DaAX)Yca2eN^C-!mG?-i#4I*gqOITaMp-I8%XcDddf@ zrbxii+U(#+7M~ul?ej}Ec)l`JN*0x4etM~O*7oS!?O^l3q2$VZIA^T|C1`_`m?X$W zuDc~IC0tQg7SMdz*b*t@z|@>2fHb8mKqb=oVrnIlmu)##sIdNSkN-h5<|)X{>a*CD z)P@-L`sJ_L;DWg}>(Ys}NhQu*B>CQ>PUk;Lk6O!2gT}LPK2enCrwZ=ea3apKB=pUc_t05|^L^-m$(?Zbzg{ zHbW1DShy6#@<6x)*ph1^wZ+-FjC!zapwT9Mh4jfY8YXsTL*O9XtFT%(Kk=uzXo9j` zI}HQDWh+&&PuDx4?OLXJJD@Zy^g`v&g$Y%s?kY@7|3itRU#&G+;7_Z`W`*8~oEl3n z>~X5Q*%#N>JWg6eV-eK8NYvX9>>L^~3QW$cMTL&J=)x|X5NAdO-|)LzG9U9bMhzhg zC>|{n)rT9Q3RA+br1D{)=&>Cv%fk77^F!jlInh4n9}(#*G(#0G_@3fev?<_74g>wC zqx>vjyw1wVD;fEucjrm_6#g!QwxaT>Tr6u}pTv$L^wRBjQ5}|(Zp(+c*WL8;VkU;~ z&jIN~m^7p2Xb~L2LGA_;6yYAyKP?{~6|w!KqPzcCBT?2P+85#>1^buWe-Ou>mvm_4 zM-w!1nOWWwwe5SsdC%~lC7BABTlAGT)BEh2jxFiv9jJhq!X zXpn{8xZNW}OKf4J&DK0FpKWvb95Eh_V^m%hKhXiGwO1#^C=fSz-jpF-{K|!t{~>q# z&^*k**l!b;E5IgAVS*nGGx!kTx;BS>=s$v$yON_)7F>0H^9WtQs~yf9z)%ale!8u> zzSdo)nGc8iL>_jfHp=(XQvK>Q4p4pg0D~ zuCo*#cn?}L=?HUB{WhW9%=^@-V3Rp0uxcEJQi*IiqXU38oIRq3?1S*GsxZ}~U!_-~ zYvNv9paUi{r7;w8@welSaqGWvau-TgTGK6ra=)x0{X}5_;yBnDl4GE24RU_MMqess zS{pMWyV2jx1k1}%6DF+*1yfchfO#3>o7TFA$aRmkPPto&Nhn06gD=ih3^^Tp^&Y0# zNxnHy=bJgT?jBzRlI%@a*#7-hgGHq-w>oq{q_NQ$LQ+iOEY zTkC%CftDm*B*)4G4&^yarZgq_IvXQ0(gftQ-h;-m-djuiN_dnBH%9#@TdjK z_Es3_nwJdlud1Dmi;)~Ko31BqAXZUZDscr8yog7&8Go&AQKsW;U|6SFCjDcdqTD2u z5}DqEFF-a|NN5Nen>PDzI7^Tf9_OALXBod4a^{E&x%r$74LRd^!%8EAVgi63$Xj_&@jei)IIYLn7qpRn}i zRN7E|x9+^{*>J+X8SNXG{~80}U+uE;-l(n&+)hZ@;pt-b@Hw~A>5E|*Up39e4#BA) zq-m&vUHvr&SW~8IaTQMEne%M(&F(Z^YcQeiDK9;W+eg$B)6D=eD8_H4S@5J2-B&%z z;w|y-{DP7r&qZ@6Oh>wYWIL*F_l7c-*f@(N$_pfh)zSHuKZ1g^j|I^UjZx1UvXN9{ zDuTWnESg6mP=bm*s%t0yf@HaD@=v=cW-%&eKlv&Kfh3cS52WHENj+Y~F&x!d{D^&P zt0T7!JcEJ6XF9^Vui=eGwMA0js_$HS`#LC~jM^`QGr_T-1WnS`o~g!it5?Ii7#=9G z5j63|jWFkNg`vUF!K#xXb>qZK{=9YhSB#@muF7?LZeMCRbG$VjXPrx(h9c10XNB+f zRwNfUGG%K?7$$Eh=rSkh0;5o@9&JA$ya3g9*e;bPBEgF za!e0*4Y(l#^Io6#+4pnuUw%4 zxp=+j)HB$N_a5|=mOlgN**V}2V^8+|bk$5wx3=L%no-V1> z`sDMA{*?~4Z-+nZc2r(Cwe#pKdu*)~C^Hp}Jg)Zs4ORmqA`v0{|C!l;@ag}a*3kci s|0g>8-zEMVPW?Xx5D;2JUH|#D{}-dG21Z5u&m+>m8}Kh$>HH`AABpA(#{d8T diff --git a/dist/stylized_rock_generator-2.14.0.zip b/dist/stylized_rock_generator-2.14.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..6af1f84cf5ad2f92584f5fd91b942cc9aefcbd6c GIT binary patch literal 26683 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 literal 0 HcmV?d00001 diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index be1b22c..b2f62df 100644 --- a/stylized_rock_generator.py +++ b/stylized_rock_generator.py @@ -1,7 +1,7 @@ bl_info = { "name": "Stylized Rock Generator", "author": "D4rkst3r", - "version": (2, 13, 0), + "version": (2, 14, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( @@ -13,6 +13,7 @@ bl_info = { } import bpy +import bmesh import os import json import math @@ -24,6 +25,7 @@ from bpy.props import ( StringProperty, PointerProperty, ) from bpy.types import Operator, Panel, PropertyGroup +from mathutils import Matrix, Vector # --------------------------------------------------------------------------- @@ -651,6 +653,16 @@ def _build_lod_group(context, settings, index, location, rng, base_name=None): lod0 = _build_rock_mesh(context, settings, index, location, rng, name=base_name) _apply_hardsurface(context, lod0, settings) _apply_random_transform(context, lod0, settings, rng) + + # Vertex-Farben JETZT backen, vor dem Duplizieren. Zwei Gruende: + # 1. Der LOD-Zweig sprang vorher komplett am Bake vorbei - LOD-Gruppen kamen + # OHNE Vertex-Farben aus dem Export (gemessen: kein Farbattribut auf einer + # der vier Stufen). Genau der offene Punkt der Spielseite. + # 2. Das Decimate interpoliert die Farben mit. Wuerde je Stufe neu gebacken, + # haette jede Stufe ihre eigene Cavity und der LOD-Wechsel wuerde poppen. + if settings.bake_vertex_cavity: + _bake_vertex_cavity(context, lod0, settings) + if settings.assign_material: _assign_material(lod0, index) lod0.name = base_name + "_LOD0" @@ -1032,6 +1044,25 @@ class RockGenSettings(PropertyGroup): name="Nur Auswahl exportieren", default=False, description="Exportiert nur die aktuell selektierten Rocks statt aller", ) + # Ursprung unten-mittig + Nullpunkt ist KEINE Option, sondern passiert immer - + # ein Stein, der seine Szenenposition mit ins Asset nimmt, ist schlicht kaputt. + export_lay_flat: BoolProperty( + name="Flache Steine hinlegen", default=True, + description=("Dreht vor dem Export die kuerzeste Bbox-Achse nach oben, " + "damit plattige Steine liegen statt zu stehen. Im Spiel gibt " + "es nur zufaelligen Yaw - die Ausrichtung ist endgueltig"), + ) + export_ucx: BoolProperty( + name="UCX-Kollision mitexportieren", default=True, + description=("Legt je Stein eine konvexe Huelle 'UCX_' ins FBX. " + "Unreal nimmt sie automatisch als Simple Collision"), + ) + ucx_max_verts: IntProperty( + name="UCX max. Verts", default=32, min=8, max=128, + description=("Obergrenze fuer die Kollisionshuelle. Simple Collision soll " + "wenige Ebenen haben - eine volle Huelle waere keine " + "Vereinfachung"), + ) # --- Zufall --- random_amount: FloatProperty( @@ -1143,6 +1174,245 @@ class OBJECT_OT_generate_rocks(Operator): # FBX-Export (UE-ready) # --------------------------------------------------------------------------- +def _lay_flat_matrix(dims, tolerance=0.9): + """Rotation, die die KUERZESTE Bbox-Achse nach oben dreht (flache Seite unten). + + Im Spiel gibt es nur zufaelligen Yaw - die gebackene Ausrichtung ist also + endgueltig. Plattige Steine muessen deshalb schon im FBX liegen. + + Gedreht wird nur, wenn die kuerzeste Achse deutlich kuerzer ist als die + aktuelle Hoehe (Toleranz). Sonst wuerde ein annaehernd wuerfeliger Stein + willkuerlich gekippt, obwohl er richtig steht. + """ + dx, dy, dz = dims + shortest = min(dx, dy, dz) + if shortest >= dz * tolerance: + return None # steht schon richtig (oder ist rundlich) + if shortest == dx: + return Matrix.Rotation(math.radians(90.0), 4, 'Y') + return Matrix.Rotation(math.radians(90.0), 4, 'X') + + +def _bake_transform(ob, world, extra=None): + """Backt die uebergebene Weltmatrix in die Mesh-Daten und nullt das Objekt. + + Noetig, weil der FBX-Export die Objekt-Transformation MITSCHREIBT: das + 25er-Set trug seine Szenen-Raster-Positionen (bis 20 m) im Asset und schwebte + im Spiel neben dem Platzierungspunkt. + + FALLE: `ob.matrix_world = Identity` loescht die Transformation NICHT, wenn das + Objekt ein Eltern-Objekt hat - Blender rechnet dann eine lokale Gegen-Matrix + aus, damit die Welt-Position stimmt. Bei einer LOD-Gruppe an Position + (13/-7.5/2.2) landete das Mesh dadurch bei MINUS derselben Position. Deshalb + werden matrix_basis UND matrix_parent_inverse direkt genullt. + """ + mat = extra @ world if extra is not None else world + ob.data.transform(mat) + # Bei gespiegelter Skalierung dreht sich die Flaechen-Orientierung um. + if mat.determinant() < 0.0: + ob.data.flip_normals() + ob.matrix_basis = Matrix.Identity(4) + ob.matrix_parent_inverse = Matrix.Identity(4) + + +def _bbox_of(objs): + """Gemeinsame Bounding-Box mehrerer Meshes in deren (bereits gebackenen) Koordinaten.""" + lo = [float("inf")] * 3 + hi = [float("-inf")] * 3 + for o in objs: + if o.type != 'MESH': + continue + for v in o.data.vertices: + for i in range(3): + lo[i] = min(lo[i], v.co[i]) + hi[i] = max(hi[i], v.co[i]) + if lo[0] == float("inf"): + return None, None + return Vector(lo), Vector(hi) + + +def _make_ucx(context, src_objs, mesh_name, max_verts=32): + """Konvexe Huelle als `UCX_` - Unreal nimmt sie als Simple Collision. + + Wichtig fuer die Physik-Bruchstuecke: "Complex as Simple" taugt dafuer nicht. + Die Huelle wird auf max_verts eingedampft (planares Aufloesen), weil UE fuer + Simple Collision wenige Ebenen will - eine 150-Vert-Huelle ist keine + Vereinfachung mehr. + """ + # FALLE: nur die PUNKTE einlesen, nicht das ganze Mesh. Mit from_mesh kommen + # die Originalflaechen mit, und convex_hull legt seine Huelle dann obendrauf - + # das Ergebnis hatte gemessen 16-21 offene Kanten und war keine geschlossene + # Huelle mehr. + bm = bmesh.new() + for o in src_objs: + if o.type == 'MESH': + for v in o.data.vertices: + bm.verts.new(v.co) + if not bm.verts: + bm.free() + return None + bm.verts.ensure_lookup_table() + res = bmesh.ops.convex_hull(bm, input=bm.verts[:], use_existing_faces=False) + # Punkte im Inneren und uebrig gebliebene Geometrie gehoeren nicht zur Huelle. + weg = list(set(res.get("geom_interior", []) + res.get("geom_unused", []))) + if weg: + bmesh.ops.delete(bm, geom=weg, context='VERTS') + bm.verts.ensure_lookup_table() + bm.normal_update() + + # Alle Quellpunkte merken - die Huelle muss sie danach noch umschliessen. + quelle = [v.co.copy() for v in bm.verts] + + # Aufs Budget eindampfen. Jede Vereinfachung schneidet zwangslaeufig Ecken ab, + # die Huelle wird dadurch KLEINER als der Stein - eine Kollision, die kleiner + # ist als das Mesh, laesst den Spieler einsinken. Deshalb wird sie danach + # exakt so weit aufgeblasen, dass wieder alles hineinpasst: fuer Kollision ist + # minimal zu gross unkritisch, zu klein nicht. + angle = math.radians(1.0) + while len(bm.verts) > max_verts and angle < math.radians(60.0): + bmesh.ops.dissolve_limit( + bm, angle_limit=angle, verts=bm.verts[:], edges=bm.edges[:]) + bmesh.ops.delete( + bm, geom=[v for v in bm.verts if not v.link_faces], context='VERTS') + bm.verts.ensure_lookup_table() + if not bm.faces: + break + angle += math.radians(3.0) + bm.normal_update() + + if bm.faces and quelle: + mitte = Vector((0.0, 0.0, 0.0)) + for v in bm.verts: + mitte += v.co + mitte /= max(len(bm.verts), 1) + # Kleinster Faktor, mit dem jede Ebene alle Quellpunkte einschliesst. + faktor = 1.0 + for f in bm.faces: + n = f.normal + if n.length_squared < 1e-12: + continue + d = n.dot(f.verts[0].co) + basis = d - n.dot(mitte) + if basis <= 1e-9: + continue + for co in quelle: + noetig = (n.dot(co) - n.dot(mitte)) / basis + if noetig > faktor: + faktor = noetig + if faktor > 1.0: + for v in bm.verts: + v.co = mitte + (v.co - mitte) * faktor + + bmesh.ops.triangulate(bm, faces=bm.faces[:]) + + me = bpy.data.meshes.new("UCX_" + mesh_name) + bm.to_mesh(me) + bm.free() + ob = bpy.data.objects.new("UCX_" + mesh_name, me) + context.scene.collection.objects.link(ob) + return ob + + +def _prepare_export_copies(context, top, lay_flat=True, want_ucx=True, + ucx_max_verts=32): + """Baut eine export-fertige Kopie der Hierarchie. Gibt (objekte, aufraeumen). + + Auf KOPIEN, damit die Szene des Users unangetastet bleibt - Punkt 1 und 2 + veraendern die Geometrie ja dauerhaft. + """ + made = [] + renamed = [] # (Original, urspruenglicher Name) zum Zuruecksetzen + # JETZT merken: gleich traegt das Original einen Hilfsnamen, und die + # Kollisionshuelle haette sonst "UCX_.__src" geheissen. + export_name = _safe_filename(top.name) + + def _copy(src): + # FALLE: Blender vergibt Namen eindeutig - eine Kopie von "Rock_A_S_01" + # heisst "Rock_A_S_01.001". Unreal ordnet die Kollision aber ueber den + # EXAKTEN Mesh-Namen zu (UCX_), und der Asset-Name haengt auch + # daran. Deshalb bekommt das Original fuer die Dauer des Exports einen + # Hilfsnamen und die Kopie den echten. + name = src.name + src.name = name + ".__src" + renamed.append((src, name)) + cp = src.copy() + if src.data is not None: + cp.data = src.data.copy() + cp.data.name = name + cp.name = name + context.scene.collection.objects.link(cp) + made.append(cp) + return cp + + src_children = [c for c in top.children_recursive if c.type == 'MESH'] + top_copy = _copy(top) + child_copies = [] + for c in src_children: + cc = _copy(c) + cc.parent = top_copy + cc.matrix_parent_inverse = c.matrix_parent_inverse.copy() + child_copies.append(cc) + + # FALLE: matrix_world wird faul berechnet. Direkt nach dem Setzen von parent + # und matrix_parent_inverse liefert es noch alte Werte - bei LOD-Gruppen wurde + # dadurch die Gruppenposition (13/-7.5/2.2) NICHT herausgerechnet und landete + # im Asset. Ein view_layer-Update erzwingt die Neuberechnung. + context.view_layer.update() + + meshes = [o for o in ([top_copy] + child_copies) if o.type == 'MESH'] + + # Weltmatrizen EINSAMMELN, bevor irgendetwas genullt wird - jedes Nullen + # veraendert die Matrizen der Kinder. + welt = {o: o.matrix_world.copy() for o in meshes} + + # 2) Hinlegen: EINE Drehung fuer alle LOD-Stufen desselben Steins, sonst + # liegen die Stufen unterschiedlich herum. + extra = None + if lay_flat and meshes: + ref = meshes[0] + co = [welt[ref] @ v.co for v in ref.data.vertices] + if co: + dims = [max(c[i] for c in co) - min(c[i] for c in co) for i in range(3)] + extra = _lay_flat_matrix(dims) + + # 1) Transform backen -> alle Objekte stehen auf Identitaet, das Mesh traegt alles. + for o in meshes: + _bake_transform(o, welt[o], extra) + if top_copy.type != 'MESH': + top_copy.matrix_basis = Matrix.Identity(4) + top_copy.matrix_parent_inverse = Matrix.Identity(4) + context.view_layer.update() + + # 1) Ursprung auf unten-mittig der gemeinsamen Bbox. + lo, hi = _bbox_of(meshes) + if lo is not None: + shift = Vector((-(lo.x + hi.x) * 0.5, -(lo.y + hi.y) * 0.5, -lo.z)) + off = Matrix.Translation(shift) + for o in meshes: + o.data.transform(off) + + # 3) UCX. Bei einer LOD-Gruppe darf die Huelle NICHT unter das LodGroup-Empty: + # Unreal wuerde jedes Kind als LOD-Stufe lesen. Also als Geschwister. + if want_ucx and meshes: + ucx = _make_ucx(context, [meshes[0]], export_name, + max_verts=ucx_max_verts) + if ucx is not None: + made.append(ucx) + + def cleanup(): + for o in reversed(made): + data = o.data + bpy.data.objects.remove(o, do_unlink=True) + if data is not None and data.users == 0: + if isinstance(data, bpy.types.Mesh): + bpy.data.meshes.remove(data) + # Erst die Kopien weg, DANN die Namen zurueck - sonst kollidieren sie. + for ob, name in renamed: + ob.name = name + + return made, cleanup + + def _collect_export_targets(context, settings): """Top-Level-Rocks: LOD-Group-Empties + freistehende Rock-Meshes ohne Parent.""" tops = [] @@ -1188,12 +1458,30 @@ class OBJECT_OT_export_rocks_fbx(Operator): _ensure_object_mode(context) exported, failed = 0, 0 for top in targets: - objs = list(top.children_recursive) + [top] + # Dateiname VOR dem Kopieren merken: waehrend des Exports traegt das + # Original einen Hilfsnamen (siehe _prepare_export_copies). + filepath = os.path.join(directory, _safe_filename(top.name) + ".fbx") + + # Punkt 1-3: Ursprung erzwingen, flache Steine hinlegen, UCX bauen. + # Laeuft auf Kopien, die Szene des Users bleibt unveraendert. + try: + objs, cleanup = _prepare_export_copies( + context, top, + lay_flat=settings.export_lay_flat, + want_ucx=settings.export_ucx, + ucx_max_verts=settings.ucx_max_verts, + ) + except Exception as exc: # ein Stein darf den Batch nicht killen + failed += 1 + print("[RockGen] Export-Vorbereitung '%s' fehlgeschlagen: %s" + % (top.name, exc)) + traceback.print_exc() + continue + _deselect_all(context) for o in objs: o.select_set(True) - context.view_layer.objects.active = top - filepath = os.path.join(directory, _safe_filename(top.name) + ".fbx") + context.view_layer.objects.active = objs[0] try: bpy.ops.export_scene.fbx( filepath=filepath, @@ -1212,7 +1500,12 @@ class OBJECT_OT_export_rocks_fbx(Operator): exported += 1 except RuntimeError as exc: failed += 1 - print("[RockGen] FBX-Export '%s' fehlgeschlagen: %s" % (top.name, exc)) + print("[RockGen] FBX-Export '%s' fehlgeschlagen: %s" + % (os.path.basename(filepath), exc)) + finally: + # MUSS auch bei Fehlern laufen: sonst bleiben die Kopien in der + # Szene stehen und die Originale behalten ihren Hilfsnamen. + cleanup() if failed: self.report({'WARNING'}, "%d FBX exportiert, %d fehlgeschlagen (Konsole)." % (exported, failed)) @@ -1938,6 +2231,14 @@ class VIEW3D_PT_rock_export(_RockSub): s = context.scene.rock_gen_settings layout.prop(s, "export_dir") layout.prop(s, "export_selected_only") + layout.separator() + layout.prop(s, "export_lay_flat") + layout.prop(s, "export_ucx") + row = layout.row() + row.active = s.export_ucx + row.prop(s, "ucx_max_verts") + layout.label(text="Ursprung unten-mittig: immer", icon='CHECKMARK') + layout.separator() layout.operator("object.export_rocks_fbx", icon='EXPORT') diff --git a/tests/test_export_robust.py b/tests/test_export_robust.py new file mode 100644 index 0000000..6745731 --- /dev/null +++ b/tests/test_export_robust.py @@ -0,0 +1,244 @@ +"""Prueft die Export-Haertung: Ursprung, Hinlegen, UCX - am reimportierten FBX. + +Gemessen wird NICHT an der Szene, sondern an dem, was tatsaechlich in der Datei +landet: exportieren, in eine leere Szene reimportieren, nachmessen. Nur so faellt +auf, wenn der Export die Objekt-Transformation mitschreibt - genau der Fehler, +durch den das 25er-Set seine Raster-Positionen (bis 20 m) im Asset trug und die +Steine im Spiel neben ihrem Platzierungspunkt schwebten. + +Aufruf: blender --background --factory-startup --python tests/test_export_robust.py +""" +import os +import sys +import math +import tempfile +import importlib.util + +import bpy + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +spec = importlib.util.spec_from_file_location( + "rg", os.path.join(ROOT, "stylized_rock_generator.py")) +rg = importlib.util.module_from_spec(spec) +sys.modules["rg"] = rg +spec.loader.exec_module(rg) +rg.register() + +fails = [] +OUT = tempfile.mkdtemp() + +# Startup-Szene leeren: --factory-startup liefert Wuerfel, Kamera und Licht mit, +# der Wuerfel wuerde als "Stein" mitgezaehlt. +for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) + +s = bpy.context.scene.rock_gen_settings +s.batch_count = 3 +s.subsurf_levels = 3 +s.use_modal = False +s.generate_lods = False +s.assign_material = False +s.bake_vertex_cavity = True +s.export_dir = OUT +s.export_lay_flat = True +s.export_ucx = True + +bpy.ops.object.generate_rocks_batch() +rocks = [o for o in bpy.context.scene.objects if o.type == 'MESH'] +if len(rocks) != 3: + fails.append("Aufbau: %d Steine statt 3" % len(rocks)) + +# Szenen-Raster nachstellen: weit weg vom Ursprung, gedreht und skaliert. +# Genau diese Kombination hat das 25er-Set kaputtgemacht. +for i, ob in enumerate(rocks): + ob.location = (4.0 + i * 8.0, -6.0 - i * 3.0, 1.7) + ob.rotation_euler = (math.radians(23), math.radians(-41), math.radians(75)) + ob.scale = (1.3, 0.8, 1.1) + +# Einen Stein absichtlich plattig UND aufrecht stellen: duenn in X, hoch in Z. +# Achsparallel, damit der Fall eindeutig ist - er MUSS hingelegt werden. +platte = rocks[0] +platte.rotation_euler = (0.0, 0.0, 0.0) +platte.scale = (0.25, 1.4, 1.6) + +namen_vorher = sorted(o.name for o in bpy.context.scene.objects) +anzahl_vorher = len(bpy.context.scene.objects) + +bpy.ops.object.export_rocks_fbx() + +# --- Szene darf sich NICHT veraendert haben -------------------------------- +if sorted(o.name for o in bpy.context.scene.objects) != namen_vorher: + fails.append("Szene veraendert: Namen stimmen nach dem Export nicht mehr " + "(%d vorher, %d nachher)" + % (anzahl_vorher, len(bpy.context.scene.objects))) +if any(o.name.endswith(".__src") for o in bpy.context.scene.objects): + fails.append("Hilfsnamen '.__src' nicht zurueckgesetzt") +if any(o.name.startswith("UCX_") for o in bpy.context.scene.objects): + fails.append("UCX-Kopie in der Szene stehengeblieben") + +fbx = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx")) +if len(fbx) != 3: + fails.append("%d FBX statt 3: %s" % (len(fbx), fbx)) + +print("") +print("%-22s %7s %7s %7s %8s %6s %5s" % + ("Datei", "|X|mid", "|Y|mid", "min Z", "kuerzeste", "UCX", "VCol")) + +for f in fbx: + # Leere Szene, damit nur das Importierte gemessen wird. + bpy.ops.wm.read_factory_settings(use_empty=True) + bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, f)) + + meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] + ucx = [o for o in meshes if o.name.startswith("UCX_")] + body = [o for o in meshes if not o.name.startswith("UCX_")] + if not body: + fails.append("%s: kein Mesh im FBX" % f) + continue + ob = body[0] + + co = [ob.matrix_world @ v.co for v in ob.data.vertices] + lo = [min(c[i] for c in co) for i in range(3)] + hi = [max(c[i] for c in co) for i in range(3)] + midx = (lo[0] + hi[0]) * 0.5 + midy = (lo[1] + hi[1]) * 0.5 + dims = [hi[i] - lo[i] for i in range(3)] + kurz = "XYZ"[dims.index(min(dims))] + has_vcol = bool(ob.data.color_attributes) + + print("%-22s %7.3f %7.3f %7.3f %8s %6s %5s" + % (f, abs(midx), abs(midy), lo[2], kurz, + ("%d V" % len(ucx[0].data.vertices)) if ucx else "-", + "ja" if has_vcol else "NEIN")) + + # 1) Ursprung erzwungen: Bbox mittig, Unterkante auf 0 + if abs(midx) > 0.10 or abs(midy) > 0.10: + fails.append("%s: Bbox nicht zentriert (|X| %.3f, |Y| %.3f, erlaubt 0.10)" + % (f, abs(midx), abs(midy))) + if abs(lo[2]) > 0.01: + fails.append("%s: min.Z = %.4f, erwartet 0" % (f, lo[2])) + + # 2) Flach heisst liegend. Gefordert wird nicht "Z ist exakt am kuerzesten", + # sondern "Z ist nicht nennenswert laenger als die kuerzeste Achse" - + # bei einem 2.53/1.40/1.54-Stein braechte Kippen nichts und wuerde die + # Form nur willkuerlich drehen. + if min(dims) < dims[2] * 0.9: + fails.append("%s: steht hochkant - kuerzeste Achse ist %s (%.2f), Z ist " + "%.2f (%.2f/%.2f/%.2f)" + % (f, kurz, min(dims), dims[2], dims[0], dims[1], dims[2])) + + # 3) UCX vorhanden, geschlossen, im Budget + if not ucx: + fails.append("%s: keine UCX-Huelle im FBX" % f) + else: + u = ucx[0] + erwartet = "UCX_" + os.path.splitext(f)[0] + if u.name != erwartet: + fails.append("%s: Huelle heisst '%s', Unreal erwartet '%s'" + % (f, u.name, erwartet)) + nv = len(u.data.vertices) + if nv > s.ucx_max_verts: + fails.append("%s: UCX hat %d Verts, Budget %d" + % (f, nv, s.ucx_max_verts)) + # Huelle muss den Koerper umschliessen + uco = [u.matrix_world @ v.co for v in u.data.vertices] + for i in range(3): + if min(c[i] for c in uco) > lo[i] + 0.02 or \ + max(c[i] for c in uco) < hi[i] - 0.02: + fails.append("%s: UCX umschliesst den Stein nicht (Achse %d)" + % (f, i)) + break + # geschlossen? + offen = [e for e in u.data.edges if True] + import bmesh as _bm + bm = _bm.new(); bm.from_mesh(u.data) + rand = [e for e in bm.edges if len(e.link_faces) != 2] + bm.free() + if rand: + fails.append("%s: UCX hat %d offene Kanten - keine geschlossene Huelle" + % (f, len(rand))) + + # Vertex-Farben duerfen dabei nicht verlorengehen + if not has_vcol: + fails.append("%s: Vertex-Farben fehlen nach dem Roundtrip" % f) + +# --------------------------------------------------------------------------- +# Offener Punkt der Spielseite: LOD-Gruppen UND Vertex-Farben zusammen. +# Relevant vor den Bruchstuecken - die sind klein und LODen frueh. +# --------------------------------------------------------------------------- +# read_factory_settings leert die SZENE, die Addon-Klassen bleiben registriert - +# ein zweites register() wirft "already registered as a subclass". +bpy.ops.wm.read_factory_settings(use_empty=True) +if not hasattr(bpy.context.scene, "rock_gen_settings"): + rg.register() +LOD_OUT = tempfile.mkdtemp() + +s = bpy.context.scene.rock_gen_settings +s.batch_count = 1 +s.subsurf_levels = 3 +s.use_modal = False +s.assign_material = False +s.bake_vertex_cavity = True +s.generate_lods = True +s.lod_count = 4 +s.export_dir = LOD_OUT +s.export_lay_flat = True +s.export_ucx = True + +bpy.ops.object.generate_rocks_batch() + +# Auch die LOD-Gruppe weit weg vom Ursprung stellen. +for o in bpy.context.scene.objects: + if o.parent is None: + o.location = (13.0, -7.5, 2.2) + +bpy.ops.object.export_rocks_fbx() + +lod_fbx = sorted(f for f in os.listdir(LOD_OUT) if f.lower().endswith(".fbx")) +if not lod_fbx: + fails.append("LOD: keine FBX exportiert") +else: + bpy.ops.wm.read_factory_settings(use_empty=True) + bpy.ops.import_scene.fbx(filepath=os.path.join(LOD_OUT, lod_fbx[0])) + meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] + stufen = sorted((o for o in meshes if not o.name.startswith("UCX_")), + key=lambda o: -len(o.data.vertices)) + hull = [o for o in meshes if o.name.startswith("UCX_")] + + print("") + print("LOD-Gruppe %s:" % lod_fbx[0]) + for o in stufen: + vc = o.data.color_attributes + rng = "-" + if vc: + werte = [c.color[0] for c in vc[0].data] + rng = "R %.2f..%.2f" % (min(werte), max(werte)) + print(" %-26s %5d Verts VCol %-4s %s" + % (o.name, len(o.data.vertices), "ja" if vc else "NEIN", rng)) + if not vc: + fails.append("LOD %s: Vertex-Farben fehlen - LOD-Gruppe und " + "Vertex-Farben vertragen sich nicht" % o.name) + + if len(stufen) != 4: + fails.append("LOD: %d Stufen im FBX, erwartet 4" % len(stufen)) + if not hull: + fails.append("LOD: keine UCX-Huelle") + elif hull[0].parent is not None and hull[0].parent.type == 'EMPTY': + # Unter dem LodGroup-Empty wuerde Unreal die Huelle als LOD-Stufe lesen. + fails.append("LOD: UCX haengt unter dem LodGroup-Empty") + if stufen: + co = [stufen[0].matrix_world @ v.co for v in stufen[0].data.vertices] + lo = [min(c[i] for c in co) for i in range(3)] + hi = [max(c[i] for c in co) for i in range(3)] + if abs((lo[0] + hi[0]) * 0.5) > 0.10 or abs((lo[1] + hi[1]) * 0.5) > 0.10 \ + or abs(lo[2]) > 0.01: + fails.append("LOD: Ursprung nicht erzwungen (mid %.3f/%.3f, minZ %.3f)" + % ((lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5, lo[2])) + +print("") +if fails: + print("ERGEBNIS: %d FEHLER" % len(fails)) + for x in fails: + print(" - " + x) + sys.exit(1) +print("ERGEBNIS: ALLE CHECKS OK")