Zufalls-Funktion in beiden Addons + Zusatz-Ebenen + Form-statt-Groesse

Rock Generator 2.5.0:
- "Zufaellig"-Button: wuerfelt Seed und Form-Parameter, "Streuung" steuert die
  Abweichung von den aktuellen Werten (0 = nur neuer Seed). Optional werden auch
  Basis-Form und Noise-Typ gewuerfelt. Werte werden auf ihre Grenzen geklemmt.
- Skalierung streut jetzt die FORM, nicht die Groesse: nicht-uniform ist Default,
  und "Gesamtgroesse konstant halten" normiert das geometrische Mittel auf 1.
  Gemessen ueber 6 Rocks: Volumen-Spanne 1.92x -> 1.11x. Wichtig, weil EcoGame
  die Groesse zusaetzlich ueber ScaleMin/Max streut.

Tree Generator 1.19.0:
- "Zufaellig"-Button: neuer Seed plus Streuung auf Hoehe, Radius, Biegung,
  Ast-Anzahl/-Laenge, Kronenform und Wurzelanlauf. Preset-Charakter bleibt, weil
  relativ zu den aktuellen Werten gestreut wird (gemessen: Hoehe 6.65 -> 8.34 m).
- "_Leaf"- und "_Frucht"-Ebenen: leere Objekte mit IDENTISCHEM Ursprung neben dem
  Baum (gemessen: Abstand 0.000000), damit die Ebenen in UE deckungsgleich sitzen.

Vier Test-Suiten liegen jetzt unter tests/ statt nur im Scratchpad.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 13:30:11 +02:00
co-authored by Claude Opus 4.8
parent f5bbd1bc35
commit 1bb547c70c
14 changed files with 431 additions and 16 deletions
+51
View File
@@ -0,0 +1,51 @@
import bpy, sys, importlib.util, os, glob, tempfile, numpy as np
ADDON=r"A:\eco\stylized_rock_generator\stylized_rock_generator.py"
spec=importlib.util.spec_from_file_location("srg",ADDON)
m=importlib.util.module_from_spec(spec); sys.modules["srg"]=m; spec.loader.exec_module(m)
try: m.unregister()
except Exception: pass
m.register()
fails=[]
s=bpy.context.scene.rock_gen_settings
for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True)
# --- Namensschema ---
s.batch_count=3; s.subsurf_levels=3; s.use_modal=False; s.generate_lods=False
s.name_prefix="Fels"; s.name_form="a"; s.name_class='L'
bpy.ops.object.generate_rocks_batch()
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
print("NAMEN:", names)
if not all(n.startswith("Fels_A_L") for n in names): fails.append("naming")
# Einzeln (ohne Nummer)
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
s.batch_count=1
bpy.ops.object.generate_rocks_batch()
single=[o.name for o in bpy.context.scene.objects if o.type=='MESH']
print("EINZELN:", single)
if single and single[0]!="Fels_A_L": fails.append("naming_single")
print("LIGHTMAP-DEFAULT aus:", not m.RockGenSettings.bl_rna.properties['generate_lightmap_uv'].default)
# --- Bake: nur _albedo + _normal, AO drin ---
out=tempfile.mkdtemp()
s.bake_dir=out; s.bake_prefix="fels_a"; s.bake_resolution='512'
s.bake_color=True; s.bake_normal=True; s.bake_roughness=False; s.bake_ao=False
def run(ao_on, strength=1.0):
for f in glob.glob(os.path.join(out,"*.png")): os.remove(f)
s.bake_ao_into_albedo=ao_on; s.bake_ao_strength=strength
bpy.ops.object.bake_rock_textures()
files=sorted(os.path.basename(f) for f in glob.glob(os.path.join(out,"*.png")))
img=bpy.data.images.load(os.path.join(out,"fels_a_albedo.png"))
px=np.array(img.pixels[:],dtype=np.float32).reshape(-1,4)[:,:3]
bpy.data.images.remove(img)
return files, float(px.mean()), float(px.std())
f_on, mean_on, std_on = run(True)
f_off, mean_off, std_off = run(False)
print("DATEIEN:", f_on)
print("AO an : mean=%.4f std=%.4f" % (mean_on, std_on))
print("AO aus: mean=%.4f std=%.4f" % (mean_off, std_off))
if f_on != ["fels_a_albedo.png","fels_a_normal.png"]: fails.append("dateinamen")
if not (mean_on < mean_off): fails.append("ao_nicht_eingerechnet")
m.unregister()
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
sys.exit(1 if fails else 0)
+39
View File
@@ -0,0 +1,39 @@
import bpy, sys, importlib.util
ADDON=r"A:\eco\stylized_rock_generator\stylized_tree_generator.py"
spec=importlib.util.spec_from_file_location("stg",ADDON)
m=importlib.util.module_from_spec(spec); sys.modules["stg"]=m; spec.loader.exec_module(m)
try: m.unregister()
except Exception: pass
m.register()
fails=[]
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
s=bpy.context.scene.tree_gen_settings
print("LIGHTMAP-Default aus:", not m.TreeGenSettings.bl_rna.properties['generate_lightmap_uv'].default)
if m.TreeGenSettings.bl_rna.properties['generate_lightmap_uv'].default: fails.append("lightmap_default")
# Wachstums-Stufen mit Art-Name
s.preset='birke'; s.art_name="Birke"; s.variant=1
s.use_growth=True; s.stages=4; s.apply_modifier=False
bpy.ops.object.treegen_create()
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
print("STUFEN:", names)
if names != ["Birke_01_S1","Birke_01_S2","Birke_01_S3","Birke_01_S4"]: fails.append("stufen_namen")
# Einzelbaum, Art aus Preset abgeleitet
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
s.use_growth=False; s.art_name=""; s.preset='eiche'; s.variant=2
bpy.ops.object.treegen_create()
single=[o.name for o in bpy.context.scene.objects if o.type=='MESH']
print("EINZELN:", single)
if single != ["Eiche_02"]: fails.append("einzel_name")
# Ursprung unten-mittig + echte Meter
ob=bpy.context.scene.objects[single[0]]
ev=ob.evaluated_get(bpy.context.evaluated_depsgraph_get())
zs=[v.co.z for v in ev.data.vertices]
print("URSPRUNG z=%.3f min_z=%.3f Hoehe=%.2f m" % (ob.location.z, min(zs), max(zs)-min(zs)))
if abs(min(zs)) > 0.02: fails.append("ursprung")
m.unregister()
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
sys.exit(1 if fails else 0)
+64
View File
@@ -0,0 +1,64 @@
import bpy, sys, importlib.util
fails=[]
# ---------- ROCK ----------
spec=importlib.util.spec_from_file_location("srg", r"A:\eco\stylized_rock_generator\stylized_rock_generator.py")
r=importlib.util.module_from_spec(spec); sys.modules["srg"]=r; spec.loader.exec_module(r)
try: r.unregister()
except Exception: pass
r.register()
s=bpy.context.scene.rock_gen_settings
before=(s.seed, round(s.noise_scale,3), round(s.displace_strength,3), round(s.decimate_ratio_1,3))
s.random_amount=0.5
bpy.ops.rockgen.randomize()
after=(s.seed, round(s.noise_scale,3), round(s.displace_strength,3), round(s.decimate_ratio_1,3))
print("ROCK vorher:", before)
print("ROCK nachher:", after)
if before==after: fails.append("rock_randomize_ohne_wirkung")
# Grenzen eingehalten?
if not (0.01<=s.noise_scale<=10 and 0<=s.displace_strength<=2 and 0.01<=s.decimate_ratio_1<=1):
fails.append("rock_grenzen")
# Erzeugt der Rock danach noch gueltige Geometrie?
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
s.batch_count=1; s.use_modal=False; s.subsurf_levels=3
res=bpy.ops.object.generate_rocks_batch()
meshes=[o for o in bpy.context.scene.objects if o.type=='MESH']
print("ROCK nach Randomize erzeugt:", [o.name for o in meshes], "faces=", len(meshes[0].data.polygons) if meshes else 0)
if not meshes or len(meshes[0].data.polygons)==0: fails.append("rock_leer")
r.unregister()
# ---------- TREE ----------
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
spec2=importlib.util.spec_from_file_location("stg", r"A:\eco\stylized_rock_generator\stylized_tree_generator.py")
t=importlib.util.module_from_spec(spec2); sys.modules["stg"]=t; spec2.loader.exec_module(t)
try: t.unregister()
except Exception: pass
t.register()
ts=bpy.context.scene.tree_gen_settings
ts.preset='birke'; ts.art_name="Birke"; ts.variant=1; ts.use_growth=False
ts.make_leaf=True; ts.make_frucht=True; ts.random_amount=0.5
ts.seed=0
bpy.ops.object.treegen_randomize()
print("TREE Seed nach Randomize:", ts.seed)
if ts.seed==0: fails.append("tree_seed")
bpy.ops.object.treegen_create()
names=sorted(o.name for o in bpy.context.scene.objects)
print("TREE Objekte:", names)
if not {"Birke_01","Birke_01_Leaf","Birke_01_Frucht"} <= set(names): fails.append("zusatz_ebenen")
# Ursprung identisch?
base=bpy.context.scene.objects["Birke_01"]; leaf=bpy.context.scene.objects["Birke_01_Leaf"]
d=(base.location-leaf.location).length
print("URSPRUNG-Abstand Stamm<->Leaf: %.6f" % d)
if d>1e-6: fails.append("ursprung_leaf")
# Zwei Randomize-Durchlaeufe -> unterschiedliche Geometrie?
def height():
ev=bpy.context.scene.objects["Birke_01"].evaluated_get(bpy.context.evaluated_depsgraph_get())
zs=[v.co.z for v in ev.data.vertices]; return round(max(zs)-min(zs),3)
h1=height()
bpy.ops.object.treegen_randomize(); bpy.ops.object.treegen_create()
h2=height()
print("TREE Hoehe Lauf1=%.3f Lauf2=%.3f" % (h1,h2))
if abs(h1-h2)<1e-4: fails.append("tree_keine_variation")
t.unregister()
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
sys.exit(1 if fails else 0)
+80
View File
@@ -0,0 +1,80 @@
"""Prueft das gebaute Tree-Addon: registrieren, Operator ausfuehren, Ergebnis messen."""
import bpy, sys, zipfile, os, tempfile, importlib.util, traceback
ZIP = r"A:\eco\stylized_rock_generator\dist\stylized_tree_generator-1.19.0.zip"
fails = []
tmp = tempfile.mkdtemp()
with zipfile.ZipFile(ZIP) as z:
z.extractall(tmp)
init = None
for root, _, files in os.walk(tmp):
if "__init__.py" in files:
init = os.path.join(root, "__init__.py"); break
if not init:
print("FAIL: __init__.py nicht im Zip"); sys.exit(1)
print("Zip-Inhalt ok:", os.path.relpath(init, tmp))
spec = importlib.util.spec_from_file_location("stylized_tree_generator", init)
m = importlib.util.module_from_spec(spec)
sys.modules["stylized_tree_generator"] = m
try:
spec.loader.exec_module(m)
m.register()
print("registriert ok")
except Exception:
traceback.print_exc(); print("FAIL: register"); sys.exit(1)
s = bpy.context.scene.tree_gen_settings
# 1) einzelner Baum je Preset
for p in ("baum", "palme", "busch", "kaktus"):
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
s.preset = p; s.use_growth = False; s.seed = 3
r = bpy.ops.object.treegen_create()
obs = [o for o in bpy.data.objects if o.type == 'MESH']
bpy.context.view_layer.update()
dg = bpy.context.evaluated_depsgraph_get()
v = len(obs[0].evaluated_get(dg).data.vertices) if obs else 0
ok = ('FINISHED' in r) and v > 0
print("preset %-7s -> %s verts=%d" % (p, "ok" if ok else "FAIL", v))
if not ok: fails.append(p)
# 2) Wachstums-Stufen
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
s.preset = "baum"; s.use_growth = True; s.stages = 4
r = bpy.ops.object.treegen_create()
obs = sorted([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: o.name)
bpy.context.view_layer.update()
dg = bpy.context.evaluated_depsgraph_get()
counts = [len(o.evaluated_get(dg).data.vertices) for o in obs]
growing = all(counts[i] < counts[i+1] for i in range(len(counts)-1))
print("stufen: %d Objekte verts=%s aufsteigend=%s" % (len(obs), counts, growing))
if len(obs) != 4 or not growing: fails.append("stages")
# 3) Modifier anwenden (Export-Pfad)
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
s.use_growth = False; s.apply_modifier = True
s.generate_lightmap_uv = True # Default ist AUS (EcoGame: dynamisches Licht)
bpy.ops.object.treegen_create()
obs = [o for o in bpy.data.objects if o.type == 'MESH']
real = len(obs[0].data.vertices) if obs else 0 # echte Mesh-Daten, nicht evaluated
uvs = [l.name for l in obs[0].data.uv_layers] if obs else []
print("apply_modifier -> echte Verts=%d mods=%d uvs=%s" % (real, len(obs[0].modifiers) if obs else -1, uvs))
if real == 0: fails.append("apply")
if "UVMap" not in uvs: fails.append("uv0")
if "Lightmap" not in uvs: fails.append("uv1")
# UV muss world-space sein: V-Bereich ~ Baumhoehe
if obs and obs[0].data.uv_layers.get("UVMap"):
d = obs[0].data.uv_layers["UVMap"].data
vs = [x.uv[1] for x in d]
h = max(v.co.z for v in obs[0].data.vertices)
print("UV world-space check: V-max=%.2f Baumhoehe=%.2f" % (max(vs), h))
if max(vs) < h * 0.5: fails.append("uv_scale")
m.unregister()
print("ERGEBNIS:", "ALLE TESTS BESTANDEN" if not fails else ("FEHLER: " + ", ".join(fails)))
sys.exit(1 if fails else 0)