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:
@@ -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