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:
+194
-1
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "Stylized Rock Generator",
|
||||
"author": "D4rkst3r",
|
||||
"version": (2, 15, 0),
|
||||
"version": (2, 16, 0),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "View3D > Sidebar > Rock Gen",
|
||||
"description": (
|
||||
@@ -1007,6 +1007,26 @@ class RockGenSettings(PropertyGroup):
|
||||
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(
|
||||
name="G = Teil-Maske (Shells)", default=False,
|
||||
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"
|
||||
|
||||
|
||||
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):
|
||||
"""Stellt auf smooth und dicht um (~2700 Tris statt ~620)."""
|
||||
bl_idname = "rockgen.dense_preset"
|
||||
@@ -2389,6 +2572,15 @@ class VIEW3D_PT_rock_vcol(_RockSub):
|
||||
sub.prop(s, "ore_seed")
|
||||
sub.separator()
|
||||
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"
|
||||
% ("Teil-Maske" if s.part_mask_from_shells else "Moos",
|
||||
"Erz" if s.bake_ore else "1.0"),
|
||||
@@ -2503,6 +2695,7 @@ classes = (
|
||||
OBJECT_OT_bake_rock_textures,
|
||||
ROCKGEN_OT_bake_cavity_selection,
|
||||
ROCKGEN_OT_dense_preset,
|
||||
ROCKGEN_OT_fracture,
|
||||
ROCKGEN_OT_ore_set,
|
||||
ROCKGEN_OT_randomize,
|
||||
ROCKGEN_OT_preset_save,
|
||||
|
||||
Reference in New Issue
Block a user