Punkt 5 - EIN neutrales Erzfels-Set fuer ALLE Erzsorten A-Kanal traegt die Erz-Maske: sie sagt nur WO Erz sitzt. WELCHES Erz (Kupfer/Eisen/Gold = Tint + Gluehen) macht die Material-Instanz in UE. Knopf "Erzfels-Set erzeugen (S + M)" liefert Rock_Ore_S_NN und Rock_Ore_M_NN und setzt alle Einstellungen danach zurueck. Regler: Erz-Anteil, Nugget-Groesse (Radius in m), Erz-Seed. Nuggets statt Noise: gestreute Zentren mit weichem Abfall. Ein Noise-Feld gaebe wolkige Schlieren, Erz sitzt aber in abgegrenzten Einschluessen. Die Zentren bevorzugen Fugen - Adern folgen Rissen, und das Gluehen liest sich in einer Vertiefung besser. Ohne Erz-Bake bleibt A auf 1.0 - wie im eingefrorenen Layout v2 und wie das bestehende 25er-Set es traegt. Punkt 6 vorgezogen, weil Punkt 5 davon abhaengt Die Maskenaufloesung haengt an der Vertex-Dichte. Bei den bisherigen ~150-Vert-Steinen war jedes dritte Vertex ein Nugget-Zentrum: im Render liefen die Nuggets zu grossen Schlieren zusammen statt einzelne Einschluesse zu bleiben. Bei ~1350 Verts liest es sich richtig. Deshalb gibt es jetzt "Smooth & dicht" (Unterteilung 5, Decimate 0.40/0.80, Auto-Smooth) und das Erz-Set stellt selbst darauf um. Gemessen ueber drei Seeds: 2708/2658/2716 Tris - im Zielfenster 1500-3000. Zusaetzlich ist die Nuggetzahl auf ein Zentrum je ~10 Verts gedeckelt. Nebenbei: _asset_name schrieb die Form-Bezeichnung pauschal gross, daraus wurde "Rock_ORE_S_01" statt des vereinbarten "Rock_Ore_S_01". Einzelne Buchstaben (Form A, B, C) werden weiter gross geschrieben, laengere Bezeichner behalten ihre Schreibweise. Gemessen: Erz-Anteil 0.08 -> 4 %, 0.18 -> 9 %, 0.35 -> 15 % der Oberflaeche, A 0.00..1.00, S bis 0.80 m / M ab 1.71 m, Maske ueberlebt den FBX-Roundtrip. Neu: tests/test_erz.py. 16 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
167 lines
6.5 KiB
Python
167 lines
6.5 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).
|
|
* 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")
|