"""Erzfels-Set (Wunschliste Punkt 5) + Smooth/dicht (Punkt 6). Kernaussagen, die hier festgenagelt werden: * EIN neutrales Set - die A-Maske sagt nur WO Erz sitzt, nicht welches. * Ohne Erz bleibt A auf 1.0 (Layout v2, wie das bestehende 25er-Set). * Die Maske ueberlebt den FBX-Roundtrip. * Die Aufloesung der Maske haengt an der Vertex-Dichte - deshalb erzeugt das Erz-Set dicht. Bei ~150 Verts waren die Nugget-Zentren jeder dritte Vertex und liefen zu Schlieren zusammen statt einzelne Einschluesse zu bleiben. Aufruf: blender --background --factory-startup --python tests/test_erz.py """ import os import sys import tempfile import importlib.util import bpy 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 = [] for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) s = bpy.context.scene.rock_gen_settings s.use_modal = False s.assign_material = False s.generate_lods = False s.bake_vertex_cavity = True def einer(**kw): for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) s.batch_count = 1 for k, v in kw.items(): setattr(s, k, v) bpy.ops.object.generate_rocks_batch() ob = [o for o in bpy.context.scene.objects if o.type == 'MESH'][0] a = [c.color[3] for c in ob.data.color_attributes["Cavity"].data] tris = sum(len(p.vertices) - 2 for p in ob.data.polygons) return ob, a, tris # --- 1) Ohne Erz bleibt A auf 1.0 ---------------------------------------- _, a, _ = einer(bake_ore=False, subsurf_levels=4) if min(a) < 0.999: fails.append("Ohne Erz ist A nicht 1.0 (min %.3f) - das bricht das " "eingefrorene Layout v2 und das bestehende 25er-Set" % min(a)) # --- 2) Mit Erz: Maske vorhanden, Anteil steigt monoton ------------------- print("%-24s %7s %7s %9s %7s" % ("Variante", "A min", "A max", "A>0.5", "Tris")) anteile = [] for amt in (0.08, 0.18, 0.35): ob, a, tris = einer(bake_ore=True, ore_amount=amt, ore_nugget_size=0.10, subsurf_levels=5, decimate_ratio_1=0.40, decimate_ratio_2=0.80) anteil = sum(1 for x in a if x > 0.5) / len(a) anteile.append(anteil) print("%-24s %7.2f %7.2f %8.0f%% %7d" % ("Erz-Anteil %.2f" % amt, min(a), max(a), 100 * anteil, tris)) if max(a) < 0.9: fails.append("Erz-Anteil %.2f: A erreicht nur %.2f - keine Nuggets" % (amt, max(a))) if min(a) > 0.01: fails.append("Erz-Anteil %.2f: A faellt nie auf 0 - der ganze Stein " "waere Erz" % amt) if not (anteile[0] < anteile[1] < anteile[2]): fails.append("Erz-Anteil wirkt nicht monoton: %s" % ", ".join("%.0f%%" % (100 * x) for x in anteile)) # --- 3) Smooth & dicht landet im Tri-Fenster 1500-3000 ------------------- bpy.ops.rockgen.dense_preset() tris_liste = [] for seed in (1, 14, 27): _, _, tris = einer(seed=seed, bake_ore=False) tris_liste.append(tris) schnitt = sum(tris_liste) // len(tris_liste) print("") print("Smooth & dicht: %s Tris (Schnitt %d)" % ("/".join(str(t) for t in tris_liste), schnitt)) if not (1500 <= schnitt <= 3000): fails.append("Smooth & dicht liefert %d Tris, Zielfenster 1500-3000" % schnitt) if s.shading_mode != 'AUTO': fails.append("Smooth & dicht setzt kein Auto-Smooth (shading_mode=%s)" % s.shading_mode) # --- 4) Das Set: Namen, Groessen, Einstellungen wieder wie vorher --------- for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) s.batch_count = 2 s.subsurf_levels = 4 # absichtlich duenn: das Set muss selbst dicht stellen s.name_prefix = "Fels" s.name_form = "A" s.name_class = 'M' s.bake_ore = False merker = (s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels) OUT = tempfile.mkdtemp() s.export_dir = OUT bpy.ops.rockgen.ore_set() namen = sorted(o.name for o in bpy.context.scene.objects if o.type == 'MESH') erwartet = ["Rock_Ore_M_01", "Rock_Ore_M_02", "Rock_Ore_S_01", "Rock_Ore_S_02"] print("") print("Set: %s" % ", ".join(namen)) if namen != erwartet: fails.append("Set-Namen %s, erwartet %s" % (namen, erwartet)) hoehen = {} for ob in bpy.context.scene.objects: if ob.type != 'MESH': continue co = [ob.matrix_world @ v.co for v in ob.data.vertices] hoehen[ob.name] = max(c.z for c in co) - min(c.z for c in co) if len(ob.data.vertices) < 800: fails.append("%s hat nur %d Verts - zu grob fuer eine Nugget-Maske" % (ob.name, len(ob.data.vertices))) a = [c.color[3] for c in ob.data.color_attributes["Cavity"].data] if max(a) < 0.9: fails.append("%s traegt keine Erz-Maske (A max %.2f)" % (ob.name, max(a))) s_hoch = max(v for k, v in hoehen.items() if "_S_" in k) m_hoch = min(v for k, v in hoehen.items() if "_M_" in k) print("Groessen: S bis %.2f m, M ab %.2f m" % (s_hoch, m_hoch)) if s_hoch >= m_hoch: fails.append("S (%.2f m) ist nicht kleiner als M (%.2f m) - die Brocken " "muessen in Hoehlengaenge passen" % (s_hoch, m_hoch)) if (s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels) != merker: fails.append("Das Set hat die Einstellungen nicht zurueckgesetzt: %s" % str((s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels))) # --- 5) A ueberlebt den FBX-Roundtrip ------------------------------------ bpy.ops.object.export_rocks_fbx() dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx")) if not dateien: fails.append("Erz-Set: kein FBX exportiert") else: bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, dateien[0])) ob = [o for o in bpy.context.scene.objects if o.type == 'MESH' and not o.name.startswith("UCX_")][0] a = [c.color[3] for c in ob.data.color_attributes[0].data] anteil = sum(1 for x in a if x > 0.5) / len(a) print("Nach FBX (%s): A %.2f..%.2f, Erz auf %.0f%%" % (dateien[0], min(a), max(a), 100 * anteil)) if max(a) < 0.9 or min(a) > 0.01: fails.append("Erz-Maske ueberlebt den FBX-Roundtrip nicht " "(A %.2f..%.2f)" % (min(a), max(a))) print("") if fails: print("ERGEBNIS: %d FEHLER" % len(fails)) for x in fails: print(" - " + x) sys.exit(1) print("ERGEBNIS: ALLE CHECKS OK")