Files
stylized-rock-generator/tests/test_erz.py
T
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

182 lines
7.2 KiB
Python

"""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).
* A wird INVERTIERT gebacken: 1.0 = kein Erz, 0.0 = Erz. Der Master rechnet
`(1 - A) * ErzMenge` mit ErzMenge als MI-Regler (Standard 0) - normale
Stein-MIs ignorieren den Kanal damit komplett. Geprueft wird deshalb direkt
gegen diese Formel und nicht gegen den Rohwert.
* 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
def erz_aus_a(a):
"""Was der Master sieht: (1 - A). 0 = kein Erz, 1 = volles Erz."""
return [1.0 - x for x in a]
# --- 1) Ohne Erz sieht der Master NICHTS ---------------------------------
_, 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))
if max(erz_aus_a(a)) > 0.001:
fails.append("Ohne Erz liefert (1-A) bis %.3f - ein normaler Stein wuerde "
"im Master Erz zeigen" % max(erz_aus_a(a)))
# --- 2) Mit Erz: Maske vorhanden, Anteil steigt monoton -------------------
print("%-24s %7s %7s %9s %7s" % ("Variante", "A min", "A max", "Erz-Anteil", "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)
erz = erz_aus_a(a)
anteil = sum(1 for x in erz if x > 0.5) / len(erz)
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(erz) < 0.9:
fails.append("Erz-Anteil %.2f: (1-A) erreicht nur %.2f - die Nuggets "
"laufen nicht auf A=0 zu" % (amt, max(erz)))
if min(erz) > 0.01:
fails.append("Erz-Anteil %.2f: (1-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(erz_aus_a(a)) < 0.9:
fails.append("%s traegt keine Erz-Maske ((1-A) max %.2f)"
% (ob.name, max(erz_aus_a(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]
erz = erz_aus_a(a)
anteil = sum(1 for x in erz if x > 0.5) / len(erz)
print("Nach FBX (%s): A %.2f..%.2f, Erz auf %.0f%%"
% (dateien[0], min(a), max(a), 100 * anteil))
if max(erz) < 0.9 or min(erz) > 0.01:
fails.append("Erz-Maske ueberlebt den FBX-Roundtrip nicht "
"((1-A) %.2f..%.2f)" % (min(erz), max(erz)))
print("")
if fails:
print("ERGEBNIS: %d FEHLER" % len(fails))
for x in fails:
print(" - " + x)
sys.exit(1)
print("ERGEBNIS: ALLE CHECKS OK")