"""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"): # Der Master rechnet (1 - A): Nuggets muessen auf A = 0 zulaufen. erz = [1.0 - x for x in a] if max(erz) < 0.9 or min(erz) > 0.01: fails.append("%s: traegt keine Erz-Maske ((1-A) %.2f..%.2f)" % (ob.name, min(erz), max(erz))) 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")