Files
D4rkst3randClaude Opus 4.8 a2d46424ec Rock Gen 2.17.0: Erz-Maske invertiert - Nuggets laufen auf A = 0 zu
Entscheidung der Spielseite: die A-Semantik bleibt eingefroren (1.0 = kein Erz),
der Master loest es ueber

    Erz = (1 - A) * ErzMenge

mit ErzMenge als MI-Regler, Standard 0. Normale Stein-MIs ignorieren den Kanal
damit vollstaendig, nur die Erz-MIs drehen ihn auf. Kein Re-Export des
25er-Sets noetig, keine Fallunterscheidung im Addon.

Dafuer musste die Maske umgedreht werden: bisher lag das Nugget-ZENTRUM auf
A = 1.0 - also auf demselben Wert wie "kein Erz". Unter der Master-Formel waere
das Erz damit unsichtbar gewesen und der ganze RESTLICHE Stein haette als Erz
gegolten. Jetzt laufen die Nuggets auf A = 0 zu.

Gemessen: ohne Erz A 1.00..1.00 und (1-A) max 0.000 - ein normaler Stein zeigt
im Master garantiert nichts. Mit Erz erreicht (1-A) 1.00, Abdeckung unveraendert
4/9/15 % bei Anteil 0.08/0.18/0.35. Die Erzstellen sitzen an denselben Punkten
wie vorher, nur andersherum kodiert.

Die Tests pruefen jetzt direkt gegen die Master-Formel statt gegen den Rohwert -
am Rohkanal ist die Richtung zu leicht zu verwechseln. Panel zeigt bei aktivem
Erz-Bake "A=Erz (0=Erz)" plus den Hinweis auf die Formel, damit das niemand
versehentlich zurueckdreht.

Nugget-Groesse bleibt wie entschieden ABSOLUT (Radius in Metern).

ACHTUNG: Erz-Assets, die mit 2.15/2.16 exportiert wurden, tragen die alte
Richtung und muessen neu exportiert werden. Steine ohne Erz-Bake sind nicht
betroffen - deren A war und ist durchgehend 1.0.

17 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 08:38:46 +02:00

163 lines
6.0 KiB
Python

"""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")