Files
stylized-rock-generator/tests/test_asset_utils.py
T
D4rkst3randClaude Opus 4.8 ef57050739 Neu: Stylized Asset Utils 1.0.0 (viertes Addon)
Fuer handgebaute Assets, die nicht durch einen Generator laufen (Pilze,
Baumstumpf, Erzfels, Props). Panel in fuenf einklappbaren Unter-Panels statt
einer langen Liste:

1 Vorbereiten  - Rotation/Scale anwenden, Ursprung unten-mittig, Normalen
2 Vertex-Farben - R=Cavity / G=Teil-oder-Moos-Maske / B=Hoehe / A=1
3 Benennung    - <Basis>_<NN>, Mesh-Daten ziehen mit
4 Pruefen      - Asset-Check auf alles, was in UE Aerger macht
5 Export       - FBX je Mesh mit colors_type='SRGB'

Zwei Bugs beim Bauen gefunden und gefixt:
- v.link_faces gibt es nur in bmesh, nicht auf Mesh.vertices -> lose Vertices
  ueber die tatsaechlich benutzten Indizes zaehlen.
- bpy.ops.paint.vertex_color_set braucht den Vertex-Paint-Modus und scheitert im
  Object-Mode mit "poll() failed". Zusammen mit vertex_color_dirt im selben try
  wurde dadurch der GANZE Bake stillschweigend uebersprungen (R/G/B blieben 1.0).
  Initialisierung laeuft jetzt direkt ueber Python.

Verifiziert am nachgebauten Pilz: Check meldet vorher Scale/Rotation/Ursprung,
nach "Vorbereiten" Scale 1/1/1, Rot 0/0/0, min_z 0.0000 und "ok". Teil-Maske
G=1 im Hut, 0 im Stiel. Nach FBX-Roundtrip G-Werte [0.0, 1.0] erhalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 11:51:26 +02:00

86 lines
4.1 KiB
Python

import bpy, sys, importlib.util, os, tempfile, math
spec=importlib.util.spec_from_file_location("sau", r"A:\eco\stylized_rock_generator\stylized_asset_utils.py")
u=importlib.util.module_from_spec(spec); sys.modules["sau"]=u; spec.loader.exec_module(u)
try: u.unregister()
except Exception: pass
u.register()
fails=[]
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
# Pilz wie beim User: Hut + Stiel getrennt, dann Join. Mit ABSICHT unangewendeter
# Transform und Ursprung nicht unten - das soll der Check finden.
bpy.ops.mesh.primitive_cylinder_add(vertices=10, radius=0.028, depth=0.126, location=(0,0,0.5))
st=bpy.context.active_object
bpy.ops.mesh.primitive_uv_sphere_add(segments=14, ring_count=7, radius=0.042, location=(0,0,0.567))
hu=bpy.context.active_object; hu.scale=(1,1,0.5); bpy.ops.object.transform_apply(scale=True)
for o in bpy.context.selected_objects: o.select_set(False)
st.select_set(True); hu.select_set(True); bpy.context.view_layer.objects.active=st
bpy.ops.object.join()
p=bpy.context.active_object; p.name="Pilz_roh"
p.rotation_euler=(0.16,0.11,-2.26); p.scale=(0.6,0.6,0.6)
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.05, location=(1,0,0)) # UV vorhanden
p.select_set(True); bpy.context.view_layer.objects.active=p
for o in bpy.context.selected_objects:
if o is not p: o.select_set(False)
s=bpy.context.scene.asset_utils_settings
s.tri_budget=400; s.g_mode='SHELLS'
print("--- Check VOR dem Vorbereiten (soll meckern) ---")
bpy.ops.assetutils.check()
print("Scale vorher: %s Rot: %s" % ([round(v,2) for v in p.scale],
[round(math.degrees(v),1) for v in p.rotation_euler]))
bpy.ops.assetutils.prepare()
zs=[(p.matrix_world @ v.co).z - p.location.z for v in p.data.vertices]
print("NACH prepare: Scale %s Rot %s min_z %.4f"
% ([round(v,3) for v in p.scale],
[round(math.degrees(v),1) for v in p.rotation_euler], min(zs)))
if any(abs(v-1)>1e-4 for v in p.scale): fails.append("scale_nicht_angewendet")
if any(abs(v)>1e-4 for v in p.rotation_euler): fails.append("rot_nicht_angewendet")
if abs(min(zs))>0.01: fails.append("ursprung_nicht_unten")
bpy.ops.assetutils.bake_vcol()
ca=p.data.color_attributes.get("Cavity")
if ca is None: fails.append("keine_vcol")
else:
shells=u._mesh_shells(p.data)
infos=[]
for sh in shells:
vs={vi for pi in sh for vi in p.data.polygons[pi].vertices}
infos.append((sh, max(p.data.vertices[v].co.z for v in vs)))
infos.sort(key=lambda t:-t[1])
g_top={round(ca.data[li].color[1],3) for pi in infos[0][0] for li in p.data.polygons[pi].loop_indices}
g_rest={round(ca.data[li].color[1],3) for sh,_ in infos[1:] for pi in sh for li in p.data.polygons[pi].loop_indices}
R=[d.color[0] for d in ca.data]; B=[d.color[2] for d in ca.data]; A=[d.color[3] for d in ca.data]
print("VCol: G_Hut=%s G_Stiel=%s | R %.2f..%.2f | B %.2f..%.2f | A %.2f"
% (sorted(g_top), sorted(g_rest), min(R),max(R),min(B),max(B),min(A)))
if g_top!={1.0}: fails.append("G_hut")
if g_rest!={0.0}: fails.append("G_stiel")
if max(B)-min(B)<0.8: fails.append("B_flach")
if min(A)<0.99: fails.append("A")
s.rename_base="Pilz_01"; s.rename_numbered=False
bpy.ops.assetutils.rename()
print("Umbenannt zu: %s (Mesh: %s)" % (p.name, p.data.name))
if p.name!="Pilz_01": fails.append("rename")
print("--- Check NACH dem Vorbereiten (soll sauber sein) ---")
bpy.ops.assetutils.check()
out=tempfile.mkdtemp(); s.export_dir=out
bpy.ops.assetutils.export()
fbx=[f for f in os.listdir(out) if f.endswith(".fbx")]
print("Export:", fbx)
if not fbx: fails.append("export")
else:
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
bpy.ops.import_scene.fbx(filepath=os.path.join(out,fbx[0]))
imp=[o for o in bpy.data.objects if o.type=='MESH'][0]
ic=[a.name for a in imp.data.color_attributes]
ig={round(d.color[1],2) for d in imp.data.color_attributes[0].data} if ic else set()
print("Nach FBX: VCol=%s G-Werte=%s" % (ic, sorted(ig)))
if not ic: fails.append("fbx_vcol_verloren")
u.unregister()
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))