Stylized Rock Generator v2.0.0: Haertung, Presets, LODs, Material, Tests

- Fix: smart_project/dissolve_limited angle_limit jetzt in Radiant (Blender 4.x/5.x)
- Fix: Lightmap-UV-Kanal vor lightmap_pack aktiv setzen (UV0 nicht mehr ueberschrieben)
- Fehlerbehandlung pro bpy.ops-Call; einzelner Rock-Fehler bricht Batch nicht ab
- Modaler Batch-Modus mit Progress-Bar (ab modal_threshold)
- Bevel + Weighted Normal Toggle
- LOD-Set als fbx_type=LodGroup-Gruppe fuer Unreal
- Preset-System (JSON) + eingebaute Presets
- Platzhalter-Material-Zuweisung
- Headless-Testskript (tests/test_rock_gen.py)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 06:24:06 +02:00
co-authored by Claude Opus 4.8
commit c5bb3e2612
5 changed files with 1159 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
"""Headless-Test fuer den Stylized Rock Generator.
Aufruf (aus dem Repo-Root oder beliebig):
blender --background --python tests/test_rock_gen.py
Exit-Code 0 = alle Checks bestanden, 1 = mindestens ein Check fehlgeschlagen.
Der Test:
* registriert das Addon
* erzeugt einen Batch von 3 Rocks mit Standard-Settings (Subsurf reduziert
fuer Speed)
* prueft je Rock: keine losen Vertices, manifold, Normalen nach aussen
(signed volume > 0), UV0 + UV1 vorhanden
* gibt am Ende eine Zusammenfassung (Face-/Vertex-Count) aus
"""
import os
import sys
import importlib.util
import bpy
import bmesh
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
ADDON_FILE = os.path.normpath(os.path.join(THIS_DIR, "..", "stylized_rock_generator.py"))
BATCH = 3
def load_addon():
spec = importlib.util.spec_from_file_location("stylized_rock_generator", ADDON_FILE)
mod = importlib.util.module_from_spec(spec)
sys.modules["stylized_rock_generator"] = mod
spec.loader.exec_module(mod)
try:
mod.unregister()
except Exception:
pass
mod.register()
return mod
def mesh_stats(obj):
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.normal_update()
loose = sum(1 for v in bm.verts if not v.link_faces)
nonmanifold = sum(1 for e in bm.edges if not e.is_manifold)
volume = bm.calc_volume(signed=True)
stats = dict(
name=obj.name,
verts=len(bm.verts),
faces=len(bm.faces),
loose=loose,
nonmanifold=nonmanifold,
volume=volume,
uv_layers=[layer.name for layer in obj.data.uv_layers],
)
bm.free()
return stats
def main():
failures = []
# 1. Saubere, leere Szene
bpy.ops.wm.read_factory_settings(use_empty=True)
# 2. Addon registrieren (Scene-Property lebt am Typ -> nach Reset ok)
load_addon()
settings = bpy.context.scene.rock_gen_settings
settings.batch_count = BATCH
settings.subsurf_levels = 4 # Test-Speed
settings.use_modal = False # synchron -> deterministisch
settings.generate_uv0 = True
settings.generate_lightmap_uv = True
settings.generate_lods = False
settings.assign_material = False
# 3. Batch erzeugen
result = bpy.ops.object.generate_rocks_batch()
if 'FINISHED' not in result:
failures.append("Operator lieferte nicht {'FINISHED'}: %s" % (result,))
rocks = [o for o in bpy.context.scene.objects
if o.type == 'MESH' and o.name.startswith("Rock_")]
if len(rocks) != BATCH:
failures.append("Erwartet %d Rocks, gefunden %d" % (BATCH, len(rocks)))
# 4. Validierung + Zusammenfassung
print("\n" + "=" * 64)
print(" Rock-Generator Test - Zusammenfassung")
print("=" * 64)
print(" %-16s %7s %7s %6s %6s %9s %s" %
("Name", "Verts", "Faces", "Loose", "NonMf", "Volume", "UVs"))
print("-" * 64)
for obj in sorted(rocks, key=lambda o: o.name):
st = mesh_stats(obj)
print(" %-16s %7d %7d %6d %6d %9.3f %s" % (
st["name"], st["verts"], st["faces"], st["loose"],
st["nonmanifold"], st["volume"], ",".join(st["uv_layers"])))
if st["faces"] == 0:
failures.append("%s: keine Faces" % st["name"])
if st["loose"] != 0:
failures.append("%s: %d lose Vertices" % (st["name"], st["loose"]))
if st["nonmanifold"] != 0:
failures.append("%s: %d non-manifold Kanten" % (st["name"], st["nonmanifold"]))
if st["volume"] <= 0.0:
failures.append("%s: signed volume <= 0 (Normalen invertiert?)" % st["name"])
if "Lightmap" not in st["uv_layers"]:
failures.append("%s: kein Lightmap-UV (UV1)" % st["name"])
if len(st["uv_layers"]) < 2:
failures.append("%s: weniger als 2 UV-Layer" % st["name"])
print("=" * 64)
if failures:
print("\nFEHLGESCHLAGEN (%d):" % len(failures))
for f in failures:
print(" - " + f)
print("")
sys.exit(1)
print("\nAlle Checks bestanden.\n")
sys.exit(0)
if __name__ == "__main__":
main()