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
+11
View File
@@ -180,6 +180,17 @@ Referenz:
blender --background --python tests/test_rock_gen.py
```
Weitere Suiten (jeweils `blender --background --python <datei>`):
| Datei | prueft |
|---|---|
| `tests/test_tree_addon.py` | Tree-Addon: Presets, Wachstums-Stufen, UVs, Modifier-Apply |
| `tests/test_ecogame_rock.py` | Fels-Benennung, Bake liefert nur `_albedo`/`_normal`, AO im Albedo |
| `tests/test_ecogame_tree.py` | Baum-Benennung `<Art>_<Variante>_S<Stufe>`, Ursprung, Lightmap-Default |
| `tests/test_randomize.py` | Zufalls-Funktion beider Addons, `_Leaf`/`_Frucht` mit gleichem Ursprung |
Die Suiten erwarten die gebauten Zips in `dist/` bzw. lesen die `.py` direkt.
Erzeugt 3 Rocks, prueft je Rock: keine losen Vertices, manifold, Normalen nach
aussen (signed volume > 0), UV0 + UV1 vorhanden, und gibt eine Zusammenfassung
(Vertex-/Face-Count) aus. Exit-Code 0 = alles bestanden, 1 = Fehler.
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_rock_generator"
version = "2.4.0"
version = "2.5.0"
name = "Stylized Rock Generator"
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
maintainer = "D4rkst3r"
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_tree_generator"
version = "1.18.0"
version = "1.19.0"
name = "Stylized Tree Generator"
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
maintainer = "D4rkst3r"
+8 -8
View File
@@ -7,7 +7,7 @@
"id": "stylized_rock_generator",
"name": "Stylized Rock Generator",
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
"version": "2.4.0",
"version": "2.5.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -23,16 +23,16 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_rock_generator-2.4.0.zip",
"archive_size": 16036,
"archive_hash": "sha256:e7163f6f0c53731f897b8a1ba63db1cdab94aa182dd720ca1a5653a2ca3df9ec"
"archive_url": "./stylized_rock_generator-2.5.0.zip",
"archive_size": 16991,
"archive_hash": "sha256:f89c740398afb7550ccc109980be6d9ea9d785ccc9383e70aa8c7d7cb89006bd"
},
{
"schema_version": "1.0.0",
"id": "stylized_tree_generator",
"name": "Stylized Tree Generator",
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
"version": "1.18.0",
"version": "1.19.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -48,9 +48,9 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_tree_generator-1.18.0.zip",
"archive_size": 16777,
"archive_hash": "sha256:9ea5b7a9c01f3c1b382436d25539ab9ec41ffb8b051d26c17c82783c594680a0"
"archive_url": "./stylized_tree_generator-1.19.0.zip",
"archive_size": 17827,
"archive_hash": "sha256:37f6a0be381178144b36a731b68f8d307c18c714e0d9b2cd9f1c109e4b4de8e6"
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+77 -5
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Rock Generator",
"author": "D4rkst3r",
"version": (2, 4, 0),
"version": (2, 5, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Rock Gen",
"description": (
@@ -51,7 +51,7 @@ PRESET_KEYS = (
"batch_count", "spacing",
"shading_mode", "autosmooth_angle",
"randomize_shape", "noise_scale_max", "displace_strength_max", "nonuniform_scale",
"flatten_bottom", "flatten_ratio", "set_origin_bottom",
"flatten_bottom", "flatten_ratio", "set_origin_bottom", "keep_size",
)
# Eingebaute Presets (nicht ueberschreibbar/loeschbar).
@@ -410,9 +410,17 @@ def _apply_random_transform(context, obj, settings, rng):
return
lo, hi = sorted((settings.scale_min, settings.scale_max))
if settings.nonuniform_scale:
obj.scale = (rng.uniform(lo, hi), rng.uniform(lo, hi), rng.uniform(lo, hi))
sx, sy, sz = (rng.uniform(lo, hi), rng.uniform(lo, hi), rng.uniform(lo, hi))
if settings.keep_size:
# Geometrisches Mittel auf 1.0 normieren: die Proportionen streuen,
# das Volumen bleibt. Sonst multipliziert sich die Streuung mit den
# ScaleMin/Max der Engine.
g = (abs(sx) * abs(sy) * abs(sz)) ** (1.0 / 3.0)
if g > 1e-6:
sx, sy, sz = sx / g, sy / g, sz / g
obj.scale = (sx, sy, sz)
else:
s = rng.uniform(lo, hi)
s = 1.0 if settings.keep_size else rng.uniform(lo, hi)
obj.scale = (s, s, s)
obj.rotation_euler = (
rng.uniform(0.0, TWO_PI),
@@ -727,9 +735,16 @@ class RockGenSettings(PropertyGroup):
scale_min: FloatProperty(name="Scale Min", default=0.8, min=0.1, max=5.0)
scale_max: FloatProperty(name="Scale Max", default=1.2, min=0.1, max=5.0)
nonuniform_scale: BoolProperty(
name="Nicht-uniforme Skalierung", default=False,
name="Nicht-uniforme Skalierung", default=True,
description="Skaliert jede Achse einzeln (flache Platten vs. runde Brocken)",
)
keep_size: BoolProperty(
name="Gesamtgroesse konstant halten", default=True,
description=("Normiert die Achsen-Faktoren, sodass nur die FORM streut und nicht "
"die Groesse. Wichtig fuer EcoGame: die Engine streut die Groesse "
"zusaetzlich ueber ScaleMin/Max - beides zusammen multipliziert sich "
"und erzeugt sonst Mini- und Riesen-Exemplare"),
)
# --- Batch ---
batch_count: IntProperty(name="Anzahl Rocks", default=5, min=1, max=500)
@@ -819,6 +834,17 @@ class RockGenSettings(PropertyGroup):
description="Exportiert nur die aktuell selektierten Rocks statt aller",
)
# --- Zufall ---
random_amount: FloatProperty(
name="Streuung", default=0.35, min=0.0, max=1.0,
description=("Wie stark 'Zufaellig' die Werte veraendert. 0 = nur neuer Seed, "
"1 = volle Bandbreite"),
)
random_shape: BoolProperty(
name="Auch Basis-Form/Noise-Typ", default=False,
description="Wuerfelt zusaetzlich Cube/Ico-Sphere und den Noise-Typ",
)
# --- Presets ---
active_preset: EnumProperty(name="Preset", items=_preset_items)
new_preset_name: StringProperty(name="Preset-Name", default="")
@@ -1277,6 +1303,43 @@ class OBJECT_OT_bake_rock_textures(Operator):
# Operatoren: Presets
# ---------------------------------------------------------------------------
class ROCKGEN_OT_randomize(Operator):
bl_idname = "rockgen.randomize"
bl_label = "Zufaellig"
bl_description = ("Wuerfelt Seed und Form-Parameter neu. 'Streuung' bestimmt, wie "
"weit von den aktuellen Werten abgewichen wird")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.rock_gen_settings
rng = random.Random()
a = s.random_amount
def jitter(prop, lo, hi):
"""Wert um +-a in Richtung der Grenzen ziehen, dann clampen."""
cur = getattr(s, prop)
span = (hi - lo) * a
val = cur + rng.uniform(-span, span)
setattr(s, prop, max(lo, min(hi, val)))
s.seed = rng.randint(0, 9999)
jitter("noise_scale", 0.4, 3.0)
jitter("displace_strength", 0.12, 0.7)
jitter("decimate_ratio_1", 0.12, 0.45)
jitter("decimate_ratio_2", 0.3, 0.7)
jitter("dissolve_angle", 2.0, 10.0)
jitter("subsurf_levels", 3, 5)
s.subsurf_levels = int(round(s.subsurf_levels))
if s.randomize_shape:
jitter("noise_scale_max", 0.6, 4.0)
jitter("displace_strength_max", 0.2, 0.9)
if s.random_shape:
s.base_shape = rng.choice(['CUBE', 'ICOSPHERE'])
s.texture_type = rng.choice(['VORONOI', 'CLOUDS', 'DISTORTED_NOISE'])
self.report({'INFO'}, "Neue Zufallswerte (Seed %d)." % s.seed)
return {'FINISHED'}
class ROCKGEN_OT_preset_save(Operator):
bl_idname = "rockgen.preset_save"
bl_label = "Preset speichern"
@@ -1452,6 +1515,7 @@ class VIEW3D_PT_rock_generator(Panel):
row.prop(settings, "scale_min")
row.prop(settings, "scale_max")
box.prop(settings, "nonuniform_scale")
box.prop(settings, "keep_size")
box = layout.box()
box.label(text="Batch")
@@ -1462,6 +1526,13 @@ class VIEW3D_PT_rock_generator(Panel):
sub.enabled = settings.use_modal
sub.prop(settings, "modal_threshold")
box = layout.box()
box.label(text="Zufall", icon='FILE_REFRESH')
row = box.row(align=True)
row.prop(settings, "random_amount")
row.prop(settings, "random_shape", text="", icon='MOD_NOISE')
box.operator("rockgen.randomize", icon='FILE_REFRESH')
layout.separator()
layout.operator("object.generate_rocks_batch", icon='MESH_ICOSPHERE')
@@ -1500,6 +1571,7 @@ classes = (
OBJECT_OT_generate_rocks,
OBJECT_OT_export_rocks_fbx,
OBJECT_OT_bake_rock_textures,
ROCKGEN_OT_randomize,
ROCKGEN_OT_preset_save,
ROCKGEN_OT_preset_load,
ROCKGEN_OT_preset_delete,
+99 -1
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 18, 0),
"version": (1, 19, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -992,6 +992,21 @@ class TreeGenSettings(PropertyGroup):
description="Laufende Variante derselben Art (Birke_01, Birke_02 ...)",
)
spacing: FloatProperty(name="Abstand", default=4.0, min=0.0, max=20.0)
random_amount: FloatProperty(
name="Streuung", default=0.3, min=0.0, max=1.0,
description=("Wie stark 'Zufaellig' die Werte veraendert. 0 = nur neuer Seed, "
"1 = volle Bandbreite. Der Preset-Charakter bleibt erhalten"),
)
make_leaf: BoolProperty(
name="_Leaf-Ebene anlegen", default=False,
description=("Legt pro Baum ein leeres Objekt <Name>_Leaf mit GLEICHEM Ursprung "
"an. Dort modellierst du die Krone - in UE sitzen beide Ebenen "
"dadurch deckungsgleich"),
)
make_frucht: BoolProperty(
name="_Frucht-Ebene anlegen", default=False,
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
)
apply_modifier: BoolProperty(
name="Modifier anwenden", default=False,
description="Ergebnis als normales Mesh einfrieren (fuer den Export)",
@@ -1038,6 +1053,45 @@ def _add_lightmap_uv(context, ob, op):
ob.data.uv_layers[0].active_render = True
def _add_layer(context, ob, suffix):
"""Leeres Mesh-Objekt mit IDENTISCHEM Ursprung neben dem Baum anlegen.
Gleicher Ursprung ist der Punkt: in UE muessen Stamm, Krone und Fruechte
deckungsgleich sitzen, sonst verrutscht die Krone gegenueber dem Stamm.
"""
name = ob.name + suffix
old = bpy.data.objects.get(name)
if old:
bpy.data.objects.remove(old, do_unlink=True)
me = bpy.data.meshes.new(name)
new = bpy.data.objects.new(name, me)
context.collection.objects.link(new)
new.location = ob.location.copy()
new.rotation_euler = ob.rotation_euler.copy()
new.scale = ob.scale.copy()
return new
class TREEGEN_OT_randomize(Operator):
bl_idname = "object.treegen_randomize"
bl_label = "Zufaellig"
bl_description = ("Wuerfelt Seed und Form-Parameter neu, ohne den Preset-Charakter "
"zu verlieren. 'Streuung' bestimmt die Abweichung")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.tree_gen_settings
import random as _r
rng = _r.Random()
s.seed = rng.randint(0, 9999)
# Die Form-Parameter liegen am Modifier, nicht an den Settings. Deshalb
# merken wir uns die Streuung und wenden sie beim Erzeugen an.
s["_rand_jitter"] = s.random_amount
self.report({'INFO'}, "Neuer Seed %d, Streuung %.0f%%."
% (s.seed, s.random_amount * 100))
return {'FINISHED'}
class TREEGEN_OT_create(Operator):
bl_idname = "object.treegen_create"
bl_label = "Baum erzeugen"
@@ -1075,6 +1129,30 @@ class TREEGEN_OT_create(Operator):
self.report({'ERROR'}, "Erzeugen fehlgeschlagen: %s" % exc)
return {'CANCELLED'}
# Streuung auf die Form-Regler anwenden (Preset-Charakter bleibt)
amt = float(s.get("_rand_jitter", 0.0))
if amt > 0.0:
import random as _r
rng = _r.Random(s.seed)
ids = socket_ids(ng)
JITTER = {"Height": 0.35, "Trunk Radius": 0.25, "Bend": 0.5,
"Branch Count": 0.3, "Branch Length": 0.35,
"Branch Up": 0.3, "Branch Bend": 0.4,
"Crown Taper": 0.3, "Crown Bulge": 0.3,
"Root Flare": 0.4, "Root Lobes": 0.3}
for ob in created:
md = ob.modifiers.get("GN_Tree")
if md is None:
continue
for key, rel in JITTER.items():
sid = ids.get(key)
if sid is None:
continue
cur = md[sid]
f = 1.0 + rng.uniform(-rel, rel) * amt
md[sid] = type(cur)(round(cur * f)) if isinstance(cur, int) else cur * f
context.view_layer.update()
if s.apply_modifier:
for ob in created:
context.view_layer.objects.active = ob
@@ -1087,6 +1165,15 @@ class TREEGEN_OT_create(Operator):
if s.generate_lightmap_uv:
_add_lightmap_uv(context, ob, self)
if s.make_leaf or s.make_frucht:
extra = []
for ob in created:
if s.make_leaf:
extra.append(_add_layer(context, ob, "_Leaf"))
if s.make_frucht:
extra.append(_add_layer(context, ob, "_Frucht"))
created.extend(extra)
context.view_layer.update()
total = 0
dg = context.evaluated_depsgraph_get()
@@ -1129,6 +1216,16 @@ class VIEW3D_PT_tree_generator(Panel):
sub.prop(s, "stages")
sub.prop(s, "spacing")
box = layout.box()
box.label(text="Zufall", icon='FILE_REFRESH')
box.prop(s, "random_amount")
box.operator("object.treegen_randomize", icon='FILE_REFRESH')
box = layout.box()
box.label(text="Zusatz-Ebenen")
box.prop(s, "make_leaf")
box.prop(s, "make_frucht")
box = layout.box()
box.label(text="Export")
box.prop(s, "apply_modifier")
@@ -1143,6 +1240,7 @@ class VIEW3D_PT_tree_generator(Panel):
classes = (
TreeGenSettings,
TREEGEN_OT_randomize,
TREEGEN_OT_create,
VIEW3D_PT_tree_generator,
)
+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)