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:
@@ -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,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,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"
|
||||
|
||||
Vendored
+8
-8
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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