diff --git a/README.md b/README.md index c55f037..b526221 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,12 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung: **Benennung** - Felsen: `Fels_
_` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`. - Die Klasse meint eine eigene **Silhouette**, nicht blosse Groesse: rein - groesser/kleiner macht die Engine ueber ScaleMin/Max. + Die Klasse setzt eine **Basis-Groesse** (S=0.45, M=1.0, L=1.9, XL=3.2) und ist + damit ein bewusst anderes Asset — gemessen: S 0.85 m, M 1.9 m, L 3.5 m, XL 6.0 m. + Davon strikt getrennt ist die **Zufalls-Streuung** (`scale_min/max`): die variiert + nur die FORM und wird von „Gesamtgroesse konstant halten" auf konstantes Volumen + normiert (1.11× statt 1.92×), damit sie sich nicht mit den ScaleMin/Max der Engine + multipliziert. - Baeume: `__S`, z. B. `Birke_01_S1` … `Birke_01_S4`. Zusatz-Ebenen haengen hinten an: `_Leaf`, `_Frucht`. - Texturen: `_albedo` und `_normal` — mehr nicht. @@ -197,6 +201,7 @@ Weitere Suiten (jeweils `blender --background --python `): | `tests/test_ecogame_tree.py` | Baum-Benennung `__S`, Ursprung, Lightmap-Default | | `tests/test_randomize.py` | Zufalls-Funktion beider Addons, `_Leaf`/`_Frucht` mit gleichem Ursprung | | `tests/test_leaves.py` | Blattwerk: Streuung, Ursprung, Tri-Budget, leeres `_Leaf` ohne Card | +| `tests/test_rock_classes.py` | Groessen-Klassen S/M/L/XL unterscheiden sich, Streuung bleibt formbezogen | Die Suiten erwarten die gebauten Zips in `dist/` bzw. lesen die `.py` direkt. diff --git a/blender_manifest.toml b/blender_manifest.toml index 806bc1a..9d70fde 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.5.0" +version = "2.6.0" name = "Stylized Rock Generator" tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export" maintainer = "D4rkst3r" diff --git a/dist/index.json b/dist/index.json index 82493f3..4483d2f 100644 --- a/dist/index.json +++ b/dist/index.json @@ -7,7 +7,7 @@ "id": "stylized_rock_generator", "name": "Stylized Rock Generator", "tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export", - "version": "2.5.0", + "version": "2.6.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -23,9 +23,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.5.0.zip", - "archive_size": 16991, - "archive_hash": "sha256:f89c740398afb7550ccc109980be6d9ea9d785ccc9383e70aa8c7d7cb89006bd" + "archive_url": "./stylized_rock_generator-2.6.0.zip", + "archive_size": 17621, + "archive_hash": "sha256:f20343bd1aae3c03585e7621816b8934719e8855d7d4c411a0973df3d21ac397" }, { "schema_version": "1.0.0", diff --git a/dist/stylized_rock_generator-2.5.0.zip b/dist/stylized_rock_generator-2.5.0.zip deleted file mode 100644 index a8d5240..0000000 Binary files a/dist/stylized_rock_generator-2.5.0.zip and /dev/null differ diff --git a/dist/stylized_rock_generator-2.6.0.zip b/dist/stylized_rock_generator-2.6.0.zip new file mode 100644 index 0000000..69ab35b Binary files /dev/null and b/dist/stylized_rock_generator-2.6.0.zip differ diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index 86adb1f..b917b7c 100644 --- a/stylized_rock_generator.py +++ b/stylized_rock_generator.py @@ -1,7 +1,7 @@ bl_info = { "name": "Stylized Rock Generator", "author": "D4rkst3r", - "version": (2, 5, 0), + "version": (2, 6, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( @@ -40,6 +40,14 @@ MATERIAL_PALETTE = ( ("M_Rock_Mossy", (0.24, 0.30, 0.18, 1.0)), ) +# Basis-Groesse je Groessen-Klasse (Meter-Faktor auf das Grundmesh). +# WICHTIG - Unterschied zur Zufalls-Streuung: DIESE Groesse ist gewollt und +# unterscheidet die Klassen. Die Streuung (scale_min/max) variiert dagegen nur +# die FORM innerhalb einer Klasse und wird von "keep_size" auf konstantes +# Volumen normiert, damit sie sich nicht mit den ScaleMin/Max der Engine +# multipliziert. +CLASS_SIZES = {'S': 0.45, 'M': 1.0, 'L': 1.9, 'XL': 3.2} + # Property-Keys, die von Presets gespeichert/geladen werden. PRESET_KEYS = ( "base_shape", "subsurf_levels", "texture_type", "noise_scale", @@ -52,6 +60,7 @@ PRESET_KEYS = ( "shading_mode", "autosmooth_angle", "randomize_shape", "noise_scale_max", "displace_strength_max", "nonuniform_scale", "flatten_bottom", "flatten_ratio", "set_origin_bottom", "keep_size", + "use_class_size", "base_size", ) # Eingebaute Presets (nicht ueberschreibbar/loeschbar). @@ -407,7 +416,18 @@ def _apply_hardsurface(context, obj, settings): def _apply_random_transform(context, obj, settings, rng): if not settings.randomize_transform: + # Klassengroesse gilt trotzdem - sie ist keine Zufalls-Streuung. + base = (CLASS_SIZES.get(settings.name_class, 1.0) + if settings.use_class_size else settings.base_size) + if abs(base - 1.0) > 1e-6: + obj.scale = (base, base, base) + _deselect_all(context) + context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) return + base = (CLASS_SIZES.get(settings.name_class, 1.0) + if settings.use_class_size else settings.base_size) lo, hi = sorted((settings.scale_min, settings.scale_max)) if settings.nonuniform_scale: sx, sy, sz = (rng.uniform(lo, hi), rng.uniform(lo, hi), rng.uniform(lo, hi)) @@ -418,10 +438,10 @@ def _apply_random_transform(context, obj, settings, rng): 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) + obj.scale = (sx * base, sy * base, sz * base) else: s = 1.0 if settings.keep_size else rng.uniform(lo, hi) - obj.scale = (s, s, s) + obj.scale = (s * base, s * base, s * base) obj.rotation_euler = ( rng.uniform(0.0, TWO_PI), rng.uniform(0.0, TWO_PI), @@ -781,6 +801,17 @@ class RockGenSettings(PropertyGroup): "macht die Engine ueber ScaleMin/Max - eine eigene Klasse nur, " "wenn sich die Silhouette aendert"), ) + use_class_size: BoolProperty( + name="Groesse aus Klasse", default=True, + description=("Leitet die Basisgroesse aus der Klasse ab " + "(S=0.45, M=1.0, L=1.9, XL=3.2). Ausschalten, um " + "'Basis-Groesse' frei zu setzen"), + ) + base_size: FloatProperty( + name="Basis-Groesse", default=1.0, min=0.05, max=20.0, + description=("Absolute Groesse des Steins (Faktor). Getrennt von der " + "Zufalls-Streuung, die nur die Form variiert"), + ) name_numbered: BoolProperty( name="Bei Batch durchnummerieren", default=True, description="Haengt bei mehreren Rocks _01, _02 ... an (sonst Blender-Suffix)", @@ -1316,11 +1347,15 @@ class ROCKGEN_OT_randomize(Operator): a = s.random_amount def jitter(prop, lo, hi): - """Wert um +-a in Richtung der Grenzen ziehen, dann clampen.""" + """Wert um +-a in Richtung der Grenzen ziehen, dann clampen. + + Int-Properties brauchen einen int - ein float wirft sonst + TypeError und der ganze Operator bricht ab. + """ cur = getattr(s, prop) span = (hi - lo) * a - val = cur + rng.uniform(-span, span) - setattr(s, prop, max(lo, min(hi, val))) + val = max(lo, min(hi, cur + rng.uniform(-span, span))) + setattr(s, prop, int(round(val)) if isinstance(cur, int) else val) s.seed = rng.randint(0, 9999) jitter("noise_scale", 0.4, 3.0) @@ -1329,7 +1364,6 @@ class ROCKGEN_OT_randomize(Operator): 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) @@ -1444,6 +1478,15 @@ class VIEW3D_PT_rock_generator(Panel): row.prop(settings, "name_form", text="") row.prop(settings, "name_class", text="") box.prop(settings, "name_numbered") + row = box.row(align=True) + row.prop(settings, "use_class_size") + sub = row.row() + sub.enabled = not settings.use_class_size + sub.prop(settings, "base_size", text="") + if settings.use_class_size: + box.label(text="Basis-Groesse aus Klasse %s: x%.2f" + % (settings.name_class, CLASS_SIZES.get(settings.name_class, 1.0)), + icon='FIXED_SIZE') box.label(text="Ergebnis: %s" % _asset_name(settings, 0, settings.batch_count), icon='INFO') diff --git a/tests/test_rock_classes.py b/tests/test_rock_classes.py new file mode 100644 index 0000000..69c114d --- /dev/null +++ b/tests/test_rock_classes.py @@ -0,0 +1,31 @@ +import bpy, sys, importlib.util +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 +s.batch_count=5; s.subsurf_levels=3; s.use_modal=False; s.spacing=3 +s.scale_min=0.75; s.scale_max=1.25; s.keep_size=True +fails=[]; means={} +print("Klasse | Groesse min..max (m) | Volumen min..max | Streuung") +for kl in ('S','M','L','XL'): + for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True) + s.name_class=kl + bpy.ops.object.generate_rocks_batch() + vols=[]; dims=[] + for o in bpy.context.scene.objects: + if o.type!='MESH': continue + bb=o.bound_box + d=[(max(v[i] for v in bb)-min(v[i] for v in bb))*o.scale[i] for i in range(3)] + dims.append(max(d)); vols.append(d[0]*d[1]*d[2]) + means[kl]=sum(vols)/len(vols) + print(" %-3s | %.2f .. %.2f | %.2f .. %.2f | %.2fx" + % (kl, min(dims), max(dims), min(vols), max(vols), max(vols)/min(vols))) + if max(vols)/min(vols) > 1.6: fails.append("streuung_%s"%kl) +print("Volumen-Verhaeltnis M/S = %.1fx | L/M = %.1fx | XL/L = %.1fx" + % (means['M']/means['S'], means['L']/means['M'], means['XL']/means['L'])) +if not (means['S'] < means['M'] < means['L'] < means['XL']): fails.append("reihenfolge") +if means['M']/means['S'] < 3: fails.append("S_zu_gross") +r.unregister() +print("ERGEBNIS:", "OK" if not fails else "FEHLER: "+", ".join(fails))