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:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user