Rock Gen 2.16.0: Bruchstuecke (Punkt 4) + Erz-Stueck (Punkt 4a)
Kein Laufzeit-Fracturing im Spiel - die Teile werden beim Anschlagen als Physik-Actors gespawnt, entstehen hier also als vollwertige Assets. Knopf "In Bruchstuecke zerlegen" arbeitet auf der Auswahl: <Name>_Piece_01..NN, 2-4 Teile, Bruch-Seed, optional Mutterstein ausblenden. Jedes Teil bekommt Vertex-Layout v2, Ursprung unten-mittig, die gleichen Material-Slots wie der Mutterstein und beim Export eine eigene UCX-Huelle. Punkt 4a: "Ein Stueck mit Erz" benennt ein zufaelliges Teil <Name>_Piece_Erz und schreibt Erz-Adern in dessen A-Kanal. Alle anderen Teile behalten A = 1.0. Ob es gespawnt wird, entscheidet die Loot-Logik im Spiel. Zusammenpassende Bruchkanten ohne Abstimmung: Voronoi ueber Halbraeume. Fuer jedes andere Zentrum wird an der Mittelsenkrechten geschnitten und die abgewandte Seite verworfen - benachbarte Zellen schneiden dadurch an GENAU derselben Ebene. Bewusst ohne das Cell-Fracture-Addon (muesste aktiviert sein, haengt an Operator-Kontext); bmesh laeuft immer. Die Falle, die mich mehrere Anlaeufe gekostet hat: jeder Schnitt muss SOFORT geschlossen werden, und zwar ueber genau die Kanten aus geom_cut. Der naheliegende Weg - am Ende alle Randkanten suchen und fuellen - erzeugte Kanten mit vier Flaechen (hauchduenne Splitter, 0.000007 m2 gegen Median 0.0025); raeumte man die ab, blieben Loecher. dissolve_degenerate half nicht, weil es nach KantenLAENGE geht und ein langer duenner Splitter keine kurze Kante hat. Aufgefallen ist das Ganze am Volumen, nicht am Auge: die Teile kamen zusammen auf 72.7 % des Muttersteins, weil calc_volume bei offenen Meshes Unsinn liefert. Jetzt 100.2 % bei 2, 3 und 4 Teilen, 0 offene und 0 non-manifold Kanten. Genau dieser Vergleich steht im Test - er ist zugleich das Warnsignal fuer kaputte Topologie. Neu: tests/test_bruchstuecke.py. 17 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,40 @@ 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
|
der Oberflaeche. A faellt auf 0.00 und erreicht 1.00, und die Maske ueberlebt
|
||||||
den FBX-Roundtrip.
|
den FBX-Roundtrip.
|
||||||
|
|
||||||
|
## Bruchstuecke (Steine zerschlagen)
|
||||||
|
|
||||||
|
Kein Laufzeit-Fracturing im Spiel: die Teile werden beim Anschlagen als
|
||||||
|
Physik-Actors gespawnt. Sie entstehen deshalb hier als vollwertige Assets.
|
||||||
|
Knopf **In Bruchstuecke zerlegen** arbeitet auf der Auswahl und liefert
|
||||||
|
`<Name>_Piece_01..NN`. Regler: `Bruchstuecke` (2-4), `Bruch-Seed`,
|
||||||
|
`Ein Stueck mit Erz`, `Mutterstein ausblenden`.
|
||||||
|
|
||||||
|
Jedes Teil bekommt: Vertex-Layout v2, Ursprung unten-mittig, die **gleichen
|
||||||
|
Material-Slots** wie der Mutterstein (die UE-Skripte mappen darueber) und beim
|
||||||
|
Export eine eigene `UCX_`-Huelle.
|
||||||
|
|
||||||
|
**Punkt 4a:** mit `Ein Stueck mit Erz` heisst ein zufaelliges Teil
|
||||||
|
`<Name>_Piece_Erz` und traegt Erz-Adern im A-Kanal. Alle anderen Teile behalten
|
||||||
|
A = 1.0. Ob das Erz-Stueck gespawnt wird, entscheidet die Loot-Logik im Spiel.
|
||||||
|
|
||||||
|
**Wie die Bruchkanten zusammenpassen:** Voronoi ueber Halbraeume. Fuer jedes
|
||||||
|
andere Zentrum wird an der Mittelsenkrechten geschnitten und die abgewandte
|
||||||
|
Seite verworfen. Benachbarte Zellen schneiden dadurch an *genau derselben*
|
||||||
|
Ebene - die Bruchflaechen passen zusammen, ohne dass man sie abstimmen muesste.
|
||||||
|
Bewusst ohne das Cell-Fracture-Addon: das muesste beim User aktiviert sein und
|
||||||
|
haengt an Operator-Kontext, bmesh laeuft immer.
|
||||||
|
|
||||||
|
> **Die entscheidende Falle.** Jeder Schnitt muss **sofort** geschlossen werden,
|
||||||
|
> und zwar ueber genau die Kanten, die `bisect_plane` als `geom_cut` zurueckgibt.
|
||||||
|
> Der naheliegende Weg - am Ende alle Randkanten suchen und fuellen - erzeugte
|
||||||
|
> Kanten mit vier Flaechen; raeumte man die ab, blieben Loecher. Aufgefallen ist
|
||||||
|
> das am **Volumen**: die Teile kamen zusammen auf 72.7 % des Muttersteins,
|
||||||
|
> weil `calc_volume` bei offenen Meshes Unsinn liefert. Jetzt sind es **100.2 %**
|
||||||
|
> bei 2, 3 und 4 Teilen, mit 0 offenen und 0 non-manifold Kanten.
|
||||||
|
|
||||||
|
`tests/test_bruchstuecke.py` prueft genau diesen Volumen-Vergleich - er ist
|
||||||
|
zugleich das Warnsignal fuer kaputte Topologie.
|
||||||
|
|
||||||
## Smooth & dicht
|
## Smooth & dicht
|
||||||
|
|
||||||
Knopf im Form-Panel: setzt Unterteilung 5, Decimate 0.40/0.80 und Auto-Smooth.
|
Knopf im Form-Panel: setzt Unterteilung 5, Decimate 0.40/0.80 und Auto-Smooth.
|
||||||
@@ -484,6 +518,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`.
|
|||||||
| Datei | prueft |
|
| Datei | prueft |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
|
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
|
||||||
|
| `tests/test_bruchstuecke.py` | Bruchstuecke: Volumen 100 % des Muttersteins, dichte Topologie, genau ein Erz-Stueck, Material-Slots, Ursprung, UCX je Teil |
|
||||||
| `tests/test_erz.py` | Erz-Maske: A=1.0 ohne Erz, Anteil monoton, Set-Namen/Groessen, Einstellungen zurueckgesetzt, FBX-Roundtrip, Tri-Fenster |
|
| `tests/test_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_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_panels.py` | Alle vier Addons: registrieren, jedes Panel zeichnet, Unter-Panels im richtigen Tab, **jede Property irgendwo erreichbar** |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "stylized_rock_generator"
|
id = "stylized_rock_generator"
|
||||||
version = "2.15.0"
|
version = "2.16.0"
|
||||||
name = "Stylized Rock Generator"
|
name = "Stylized Rock Generator"
|
||||||
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
|
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
|
||||||
maintainer = "D4rkst3r"
|
maintainer = "D4rkst3r"
|
||||||
|
|||||||
Vendored
+3
-3
@@ -40,14 +40,14 @@
|
|||||||
<td>4.9KB</td>
|
<td>4.9KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><tt><a href="./stylized_rock_generator-2.15.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.15.0</a></tt></td>
|
<td><tt><a href="./stylized_rock_generator-2.16.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.16.0</a></tt></td>
|
||||||
<td>Stylized Rock Generator</td>
|
<td>Stylized Rock Generator</td>
|
||||||
<td>Batch-Generator fuer stylized Rocks mit UE5-Export</td>
|
<td>Batch-Generator fuer stylized Rocks mit UE5-Export</td>
|
||||||
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
|
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
|
||||||
<td>4.2.0 - ~</td>
|
<td>4.2.0 - ~</td>
|
||||||
<td>all</td>
|
<td>all</td>
|
||||||
<td>all</td>
|
<td>all</td>
|
||||||
<td>28.9KB</td>
|
<td>31.3KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><tt><a href="./stylized_tree_generator-1.25.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.25.0</a></tt></td>
|
<td><tt><a href="./stylized_tree_generator-1.25.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.25.0</a></tt></td>
|
||||||
@@ -61,6 +61,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<center><p>Built 2026-08-03, 06:13</p></center>
|
<center><p>Built 2026-08-03, 06:23</p></center>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Vendored
+4
-4
@@ -57,7 +57,7 @@
|
|||||||
"id": "stylized_rock_generator",
|
"id": "stylized_rock_generator",
|
||||||
"name": "Stylized Rock Generator",
|
"name": "Stylized Rock Generator",
|
||||||
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
|
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
|
||||||
"version": "2.15.0",
|
"version": "2.16.0",
|
||||||
"type": "add-on",
|
"type": "add-on",
|
||||||
"maintainer": "D4rkst3r",
|
"maintainer": "D4rkst3r",
|
||||||
"license": [
|
"license": [
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
"Mesh",
|
"Mesh",
|
||||||
"Modeling"
|
"Modeling"
|
||||||
],
|
],
|
||||||
"archive_url": "./stylized_rock_generator-2.15.0.zip",
|
"archive_url": "./stylized_rock_generator-2.16.0.zip",
|
||||||
"archive_size": 29580,
|
"archive_size": 32002,
|
||||||
"archive_hash": "sha256:f57604a8176506626352d61e79a5a449f8d1aedced3aeed3a288a1abe5efcd51"
|
"archive_hash": "sha256:cb9cb1d36073da86d67d5997b9266a7d3192587591995a0fd59f6fa12f57b3e1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"schema_version": "1.0.0",
|
"schema_version": "1.0.0",
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+194
-1
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Stylized Rock Generator",
|
"name": "Stylized Rock Generator",
|
||||||
"author": "D4rkst3r",
|
"author": "D4rkst3r",
|
||||||
"version": (2, 15, 0),
|
"version": (2, 16, 0),
|
||||||
"blender": (4, 2, 0),
|
"blender": (4, 2, 0),
|
||||||
"location": "View3D > Sidebar > Rock Gen",
|
"location": "View3D > Sidebar > Rock Gen",
|
||||||
"description": (
|
"description": (
|
||||||
@@ -1007,6 +1007,26 @@ class RockGenSettings(PropertyGroup):
|
|||||||
description="Andere Verteilung der Nuggets bei gleicher Form",
|
description="Andere Verteilung der Nuggets bei gleicher Form",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Bruchstuecke ---
|
||||||
|
piece_count: IntProperty(
|
||||||
|
name="Bruchstuecke", default=3, min=2, max=4,
|
||||||
|
description="In wie viele Teile ein Stein zerfaellt",
|
||||||
|
)
|
||||||
|
piece_with_ore: BoolProperty(
|
||||||
|
name="Ein Stueck mit Erz", default=False,
|
||||||
|
description=("Ein zufaelliges Bruchstueck heisst <Name>_Piece_Erz und "
|
||||||
|
"traegt Erz im A-Kanal. Ob es gespawnt wird, entscheidet "
|
||||||
|
"die Loot-Logik im Spiel"),
|
||||||
|
)
|
||||||
|
piece_seed: IntProperty(
|
||||||
|
name="Bruch-Seed", default=0, min=0, max=9999,
|
||||||
|
description="Andere Bruchlinien bei gleicher Form",
|
||||||
|
)
|
||||||
|
piece_hide_source: BoolProperty(
|
||||||
|
name="Mutterstein ausblenden", default=True,
|
||||||
|
description="Blendet den ungebrochenen Stein aus, statt ihn zu loeschen",
|
||||||
|
)
|
||||||
|
|
||||||
part_mask_from_shells: BoolProperty(
|
part_mask_from_shells: BoolProperty(
|
||||||
name="G = Teil-Maske (Shells)", default=False,
|
name="G = Teil-Maske (Shells)", default=False,
|
||||||
description=("Statt der Moos-Maske: getrennte Mesh-Teile markieren. "
|
description=("Statt der Moos-Maske: getrennte Mesh-Teile markieren. "
|
||||||
@@ -1565,6 +1585,169 @@ def _safe_filename(name):
|
|||||||
return "".join(c for c in name if c.isalnum() or c in keep).strip() or "Rock"
|
return "".join(c for c in name if c.isalnum() or c in keep).strip() or "Rock"
|
||||||
|
|
||||||
|
|
||||||
|
def _voronoi_cells(me, punkte):
|
||||||
|
"""Zerlegt ein Mesh in Voronoi-Zellen. Liefert je Zelle ein neues Mesh.
|
||||||
|
|
||||||
|
Verfahren: eine Zelle ist der Schnitt aller Halbraeume. Fuer jedes andere
|
||||||
|
Zentrum wird an der Mittelsenkrechten zwischen beiden Zentren geschnitten und
|
||||||
|
die abgewandte Seite verworfen. Der Clou: benachbarte Zellen schneiden an
|
||||||
|
GENAU DERSELBEN Ebene - dadurch passen die Bruchflaechen zusammen, ohne dass
|
||||||
|
man sie aufeinander abstimmen muesste.
|
||||||
|
|
||||||
|
Bewusst ohne das Cell-Fracture-Addon: das muesste beim User aktiviert sein
|
||||||
|
und arbeitet ueber Operatoren mit Kontext-Anforderungen. bmesh laeuft immer.
|
||||||
|
|
||||||
|
Wichtig ist, JEDEN Schnitt sofort zu schliessen und dafuer genau die Kanten
|
||||||
|
zu nehmen, die `bisect_plane` als `geom_cut` zurueckgibt. Der Umweg ueber
|
||||||
|
"am Ende alle Randkanten suchen und fuellen" war nicht verlaesslich: er hat
|
||||||
|
Kanten mit vier Flaechen erzeugt, und wenn man die abgeraeumt hat, blieben
|
||||||
|
Loecher. Ein offenes Bruchstueck ist als Physik-Actor unbrauchbar.
|
||||||
|
"""
|
||||||
|
zellen = []
|
||||||
|
for i, pi in enumerate(punkte):
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(me)
|
||||||
|
for j, pj in enumerate(punkte):
|
||||||
|
if i == j:
|
||||||
|
continue
|
||||||
|
normale = pj - pi
|
||||||
|
laenge = normale.length
|
||||||
|
if laenge < 1e-6:
|
||||||
|
continue
|
||||||
|
normale = normale / laenge
|
||||||
|
mitte = (pi + pj) * 0.5
|
||||||
|
ergebnis = bmesh.ops.bisect_plane(
|
||||||
|
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||||
|
plane_co=mitte, plane_no=normale,
|
||||||
|
clear_outer=True, # alles jenseits der Ebene verwerfen
|
||||||
|
use_snap_center=False,
|
||||||
|
)
|
||||||
|
if not bm.faces:
|
||||||
|
break
|
||||||
|
# Genau die Schnittkanten schliessen - das ist die Bruchflaeche.
|
||||||
|
schnitt = [g for g in ergebnis.get("geom_cut", [])
|
||||||
|
if isinstance(g, bmesh.types.BMEdge) and g.is_valid]
|
||||||
|
if schnitt:
|
||||||
|
try:
|
||||||
|
bmesh.ops.edgeloop_fill(bm, edges=schnitt)
|
||||||
|
except (TypeError, ValueError, RuntimeError):
|
||||||
|
bmesh.ops.holes_fill(bm, edges=schnitt, sides=0)
|
||||||
|
|
||||||
|
if not bm.faces:
|
||||||
|
bm.free()
|
||||||
|
zellen.append(None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sicherheitsnetz, falls ein Schnitt doch etwas offen gelassen hat.
|
||||||
|
rand = [e for e in bm.edges if len(e.link_faces) == 1]
|
||||||
|
if rand:
|
||||||
|
bmesh.ops.holes_fill(bm, edges=rand, sides=0)
|
||||||
|
|
||||||
|
gross = [f for f in bm.faces if len(f.verts) > 4]
|
||||||
|
if gross:
|
||||||
|
bmesh.ops.triangulate(bm, faces=gross)
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
|
||||||
|
bm.normal_update()
|
||||||
|
|
||||||
|
neu = bpy.data.meshes.new(me.name + "_cell%d" % i)
|
||||||
|
bm.to_mesh(neu)
|
||||||
|
bm.free()
|
||||||
|
zellen.append(neu)
|
||||||
|
return zellen
|
||||||
|
|
||||||
|
|
||||||
|
class ROCKGEN_OT_fracture(Operator):
|
||||||
|
"""Zerlegt die ausgewaehlten Steine in Bruchstuecke.
|
||||||
|
|
||||||
|
Kein Laufzeit-Fracturing im Spiel: die Teile werden beim Anschlagen als
|
||||||
|
Physik-Actors gespawnt. Deshalb entstehen sie hier als vollwertige Assets -
|
||||||
|
Vertex-Layout v2, Ursprung unten-mittig, gleiche Material-Slots, und beim
|
||||||
|
Export bekommt jedes Teil seine eigene UCX-Huelle.
|
||||||
|
"""
|
||||||
|
bl_idname = "rockgen.fracture"
|
||||||
|
bl_label = "In Bruchstuecke zerlegen"
|
||||||
|
bl_description = ("Erzeugt <Name>_Piece_01..NN mit zusammenpassenden "
|
||||||
|
"Bruchkanten. Optional ein Stueck mit Erz-Adern")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.rock_gen_settings
|
||||||
|
_ensure_object_mode(context)
|
||||||
|
quellen = [o for o in context.selected_objects if o.type == 'MESH']
|
||||||
|
if not quellen:
|
||||||
|
self.report({'ERROR'}, "Keine Mesh-Objekte ausgewaehlt.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
rng = random.Random(s.piece_seed)
|
||||||
|
gesamt, leer = 0, 0
|
||||||
|
|
||||||
|
for src in quellen:
|
||||||
|
me = src.data
|
||||||
|
co = [v.co for v in me.vertices]
|
||||||
|
if not co:
|
||||||
|
continue
|
||||||
|
lo = Vector((min(c.x for c in co), min(c.y for c in co),
|
||||||
|
min(c.z for c in co)))
|
||||||
|
hi = Vector((max(c.x for c in co), max(c.y for c in co),
|
||||||
|
max(c.z for c in co)))
|
||||||
|
|
||||||
|
# Zentren im Inneren streuen, mit Abstand zum Rand: sonst entstehen
|
||||||
|
# hauchduenne Splitter statt brauchbarer Brocken.
|
||||||
|
n = s.piece_count
|
||||||
|
punkte = [Vector((
|
||||||
|
lo.x + (hi.x - lo.x) * (0.2 + 0.6 * rng.random()),
|
||||||
|
lo.y + (hi.y - lo.y) * (0.2 + 0.6 * rng.random()),
|
||||||
|
lo.z + (hi.z - lo.z) * (0.2 + 0.6 * rng.random()),
|
||||||
|
)) for _ in range(n)]
|
||||||
|
|
||||||
|
zellen = _voronoi_cells(me, punkte)
|
||||||
|
# Punkt 4a: ein Stueck traegt optional das Erz. Ob es gespawnt wird,
|
||||||
|
# entscheidet die Loot-Logik im Spiel.
|
||||||
|
erz_idx = rng.randrange(n) if s.piece_with_ore else -1
|
||||||
|
|
||||||
|
for i, zelle in enumerate(zellen):
|
||||||
|
if zelle is None or len(zelle.polygons) == 0:
|
||||||
|
leer += 1
|
||||||
|
if zelle is not None:
|
||||||
|
bpy.data.meshes.remove(zelle)
|
||||||
|
continue
|
||||||
|
ist_erz = (i == erz_idx)
|
||||||
|
name = "%s_Piece_%s" % (src.name,
|
||||||
|
"Erz" if ist_erz else "%02d" % (i + 1))
|
||||||
|
zelle.name = name
|
||||||
|
ob = bpy.data.objects.new(name, zelle)
|
||||||
|
context.collection.objects.link(ob)
|
||||||
|
ob.matrix_world = src.matrix_world.copy()
|
||||||
|
|
||||||
|
# Gleiche Material-Slots wie der Mutterstein - die UE-Skripte
|
||||||
|
# mappen ueber die Slot-Namen.
|
||||||
|
for mat in me.materials:
|
||||||
|
zelle.materials.append(mat)
|
||||||
|
|
||||||
|
_apply_shading(context, ob, s)
|
||||||
|
if s.bake_vertex_cavity:
|
||||||
|
merker = s.bake_ore
|
||||||
|
s.bake_ore = ist_erz
|
||||||
|
try:
|
||||||
|
_bake_vertex_cavity(context, ob, s)
|
||||||
|
except RockGenError as exc:
|
||||||
|
print("[RockGen] Cavity auf %s: %s" % (name, exc))
|
||||||
|
finally:
|
||||||
|
s.bake_ore = merker
|
||||||
|
_set_origin_bottom(context, ob)
|
||||||
|
gesamt += 1
|
||||||
|
|
||||||
|
if s.piece_hide_source:
|
||||||
|
src.hide_set(True)
|
||||||
|
|
||||||
|
if leer:
|
||||||
|
self.report({'WARNING'}, "%d Bruchstueck(e), %d Zelle(n) leer "
|
||||||
|
"(Zentren lagen zu dicht)." % (gesamt, leer))
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, "%d Bruchstueck(e) erzeugt." % gesamt)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class ROCKGEN_OT_dense_preset(Operator):
|
class ROCKGEN_OT_dense_preset(Operator):
|
||||||
"""Stellt auf smooth und dicht um (~2700 Tris statt ~620)."""
|
"""Stellt auf smooth und dicht um (~2700 Tris statt ~620)."""
|
||||||
bl_idname = "rockgen.dense_preset"
|
bl_idname = "rockgen.dense_preset"
|
||||||
@@ -2389,6 +2572,15 @@ class VIEW3D_PT_rock_vcol(_RockSub):
|
|||||||
sub.prop(s, "ore_seed")
|
sub.prop(s, "ore_seed")
|
||||||
sub.separator()
|
sub.separator()
|
||||||
sub.operator("rockgen.ore_set", icon='MOD_EXPLODE')
|
sub.operator("rockgen.ore_set", icon='MOD_EXPLODE')
|
||||||
|
layout.separator()
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Bruchstuecke", icon='MOD_BUILD')
|
||||||
|
row = box.row(align=True)
|
||||||
|
row.prop(s, "piece_count")
|
||||||
|
row.prop(s, "piece_seed", text="")
|
||||||
|
box.prop(s, "piece_with_ore")
|
||||||
|
box.prop(s, "piece_hide_source")
|
||||||
|
box.operator("rockgen.fracture", icon='MOD_BUILD')
|
||||||
col.label(text="R=Cavity G=%s B=Hoehe A=%s"
|
col.label(text="R=Cavity G=%s B=Hoehe A=%s"
|
||||||
% ("Teil-Maske" if s.part_mask_from_shells else "Moos",
|
% ("Teil-Maske" if s.part_mask_from_shells else "Moos",
|
||||||
"Erz" if s.bake_ore else "1.0"),
|
"Erz" if s.bake_ore else "1.0"),
|
||||||
@@ -2503,6 +2695,7 @@ classes = (
|
|||||||
OBJECT_OT_bake_rock_textures,
|
OBJECT_OT_bake_rock_textures,
|
||||||
ROCKGEN_OT_bake_cavity_selection,
|
ROCKGEN_OT_bake_cavity_selection,
|
||||||
ROCKGEN_OT_dense_preset,
|
ROCKGEN_OT_dense_preset,
|
||||||
|
ROCKGEN_OT_fracture,
|
||||||
ROCKGEN_OT_ore_set,
|
ROCKGEN_OT_ore_set,
|
||||||
ROCKGEN_OT_randomize,
|
ROCKGEN_OT_randomize,
|
||||||
ROCKGEN_OT_preset_save,
|
ROCKGEN_OT_preset_save,
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""Bruchstuecke (Wunschliste 4 und 4a).
|
||||||
|
|
||||||
|
Kein Laufzeit-Fracturing im Spiel: die Teile werden beim Anschlagen als
|
||||||
|
Physik-Actors gespawnt. Sie muessen deshalb vollwertige Assets sein.
|
||||||
|
|
||||||
|
Der wichtigste Test ist der VOLUMEN-Vergleich: die Teile muessen zusammen den
|
||||||
|
Mutterstein ergeben. Er ist auch das Warnsignal fuer schlechte Topologie -
|
||||||
|
`calc_volume` liefert bei offenen Meshes Unsinn. Genau daran ist eine fruehere
|
||||||
|
Fassung aufgefallen: sie kam auf 72.7 %, weil das Fuellen der Bruchflaechen
|
||||||
|
Loecher hinterliess.
|
||||||
|
|
||||||
|
Aufruf: blender --background --factory-startup --python tests/test_bruchstuecke.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
|
||||||
|
def topologie(ob):
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
offen = sum(1 for e in bm.edges if len(e.link_faces) == 1)
|
||||||
|
nm = sum(1 for e in bm.edges if len(e.link_faces) > 2)
|
||||||
|
draht = sum(1 for e in bm.edges if len(e.link_faces) == 0)
|
||||||
|
bm.free()
|
||||||
|
return offen, nm, draht
|
||||||
|
|
||||||
|
|
||||||
|
def volumen(ob):
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bm.transform(ob.matrix_world)
|
||||||
|
v = abs(bm.calc_volume(signed=True))
|
||||||
|
bm.free()
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
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 = 1
|
||||||
|
s.use_modal = False
|
||||||
|
s.assign_material = True # Material-Slots muessen mitwandern
|
||||||
|
s.generate_lods = False
|
||||||
|
s.bake_vertex_cavity = True
|
||||||
|
bpy.ops.rockgen.dense_preset()
|
||||||
|
bpy.ops.object.generate_rocks_batch()
|
||||||
|
|
||||||
|
mutter = [o for o in bpy.context.scene.objects if o.type == 'MESH'][0]
|
||||||
|
v_mutter = volumen(mutter)
|
||||||
|
slots_mutter = [m.name for m in mutter.data.materials]
|
||||||
|
|
||||||
|
for anzahl in (2, 3, 4):
|
||||||
|
for o in list(bpy.context.scene.objects):
|
||||||
|
if "_Piece_" in o.name:
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
mutter.hide_set(False)
|
||||||
|
for o in bpy.context.scene.objects:
|
||||||
|
o.select_set(o is mutter)
|
||||||
|
bpy.context.view_layer.objects.active = mutter
|
||||||
|
|
||||||
|
s.piece_count = anzahl
|
||||||
|
s.piece_with_ore = True
|
||||||
|
s.piece_seed = 2
|
||||||
|
bpy.ops.rockgen.fracture()
|
||||||
|
|
||||||
|
teile = [o for o in bpy.context.scene.objects if "_Piece_" in o.name]
|
||||||
|
if len(teile) != anzahl:
|
||||||
|
fails.append("%d Teile angefordert, %d bekommen" % (anzahl, len(teile)))
|
||||||
|
|
||||||
|
v_teile = sum(volumen(o) for o in teile)
|
||||||
|
quote = 100.0 * v_teile / v_mutter if v_mutter else 0.0
|
||||||
|
print("%d Teile: Volumen %.1f %% des Muttersteins" % (anzahl, quote))
|
||||||
|
# Die Teile muessen den Stein ergeben. Toleranz fuer die triangulierten
|
||||||
|
# Bruchflaechen; deutlich weniger heisst Loecher oder verlorene Zellen.
|
||||||
|
if not (97.0 <= quote <= 103.0):
|
||||||
|
fails.append("%d Teile ergeben %.1f %% des Muttersteins (erwartet 97-103)"
|
||||||
|
% (anzahl, quote))
|
||||||
|
|
||||||
|
erz = [o for o in teile if o.name.endswith("_Piece_Erz")]
|
||||||
|
if len(erz) != 1:
|
||||||
|
fails.append("%d Teile: %d Stueck mit Erz, erwartet genau 1"
|
||||||
|
% (anzahl, len(erz)))
|
||||||
|
|
||||||
|
for ob in sorted(teile, key=lambda o: o.name):
|
||||||
|
offen, nm, draht = topologie(ob)
|
||||||
|
lay = ob.data.color_attributes.get("Cavity")
|
||||||
|
a = [c.color[3] for c in lay.data] if lay else []
|
||||||
|
co = [ob.matrix_world @ v.co for v in ob.data.vertices]
|
||||||
|
unten = ob.matrix_world.translation.z - min(c.z for c in co)
|
||||||
|
|
||||||
|
if offen or nm or draht:
|
||||||
|
fails.append("%s: offen %d, non-manifold %d, Draht %d - als "
|
||||||
|
"Physik-Actor unbrauchbar" % (ob.name, offen, nm, draht))
|
||||||
|
if lay is None:
|
||||||
|
fails.append("%s: kein Vertex-Layout v2" % ob.name)
|
||||||
|
elif ob.name.endswith("_Piece_Erz"):
|
||||||
|
if max(a) < 0.9 or min(a) > 0.01:
|
||||||
|
fails.append("%s: traegt keine Erz-Maske (A %.2f..%.2f)"
|
||||||
|
% (ob.name, min(a), max(a)))
|
||||||
|
else:
|
||||||
|
if min(a) < 0.999:
|
||||||
|
fails.append("%s: A ist nicht 1.0 (min %.2f) - nur das "
|
||||||
|
"Erz-Stueck darf Erz tragen" % (ob.name, min(a)))
|
||||||
|
if abs(unten) > 0.01:
|
||||||
|
fails.append("%s: Ursprung nicht unten (%.3f m)" % (ob.name, unten))
|
||||||
|
if [m.name for m in ob.data.materials] != slots_mutter:
|
||||||
|
fails.append("%s: Material-Slots %s statt %s - die UE-Skripte "
|
||||||
|
"mappen darueber"
|
||||||
|
% (ob.name, [m.name for m in ob.data.materials],
|
||||||
|
slots_mutter))
|
||||||
|
|
||||||
|
# --- Export: je Teil eine eigene UCX-Huelle ------------------------------
|
||||||
|
OUT = tempfile.mkdtemp()
|
||||||
|
s.export_dir = OUT
|
||||||
|
s.export_selected_only = False
|
||||||
|
s.export_ucx = True
|
||||||
|
bpy.ops.object.export_rocks_fbx()
|
||||||
|
|
||||||
|
dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx"))
|
||||||
|
piece_fbx = [f for f in dateien if "_Piece_" in f]
|
||||||
|
print("")
|
||||||
|
print("Export: %d FBX, davon %d Bruchstuecke" % (len(dateien), len(piece_fbx)))
|
||||||
|
if not piece_fbx:
|
||||||
|
fails.append("Bruchstuecke wurden nicht exportiert")
|
||||||
|
else:
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, piece_fbx[0]))
|
||||||
|
meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||||||
|
hull = [o for o in meshes if o.name.startswith("UCX_")]
|
||||||
|
koerper = [o for o in meshes if not o.name.startswith("UCX_")]
|
||||||
|
print(" %s: %d Mesh, %d UCX" % (piece_fbx[0], len(koerper), len(hull)))
|
||||||
|
if not hull:
|
||||||
|
fails.append("%s: keine UCX-Huelle - fuer die Physik-Bruchstuecke "
|
||||||
|
"brauchen wir echte Konvexe" % piece_fbx[0])
|
||||||
|
if koerper and not koerper[0].data.color_attributes:
|
||||||
|
fails.append("%s: Vertex-Farben nach dem Roundtrip weg" % piece_fbx[0])
|
||||||
|
|
||||||
|
print("")
|
||||||
|
if fails:
|
||||||
|
print("ERGEBNIS: %d FEHLER" % len(fails))
|
||||||
|
for x in fails:
|
||||||
|
print(" - " + x)
|
||||||
|
sys.exit(1)
|
||||||
|
print("ERGEBNIS: ALLE CHECKS OK")
|
||||||
Reference in New Issue
Block a user