Rock Generator 2.6.0: Groessen-Klassen wirken jetzt + Randomize-Bug gefixt
1) KLASSEN-GROESSE (vom User gemeldet): S und M kamen gleich gross raus. Ursache war eine Luecke im Design, nicht in der Bedienung: "keep_size" normiert die Zufalls-Streuung auf konstantes Volumen - damit wurde alles, was ueber scale_min/max eingestellt war, wieder herausgerechnet. Fuer die Groesse der KLASSE gab es schlicht keinen Regler. Neu: CLASS_SIZES (S=0.45, M=1.0, L=1.9, XL=3.2) plus "Groesse aus Klasse" und eine freie "Basis-Groesse". Die Klassengroesse ist bewusst gesetzt und von der Streuung getrennt; sie greift auch bei abgeschalteter Randomisierung. Gemessen: S 0.85 m | M 1.9 m | L 3.5 m | XL 6.0 m, Volumen-Streuung INNERHALB jeder Klasse bleibt bei 1.11x (nur Form). 2) BUG im Zufalls-Button (kam mit 2.5.0): jitter() schrieb einen float in die int-Property subsurf_levels -> TypeError, der Operator brach komplett ab. jitter() rundet jetzt fuer Int-Properties. Von tests/test_randomize.py gefangen. Neuer Test tests/test_rock_classes.py deckt die Klassengroessen ab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,12 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
|
||||
|
||||
**Benennung**
|
||||
- Felsen: `Fels_<Form>_<Klasse>` — 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: `<Art>_<Variante>_S<Stufe>`, z. B. `Birke_01_S1` … `Birke_01_S4`.
|
||||
Zusatz-Ebenen haengen hinten an: `_Leaf`, `_Frucht`.
|
||||
- Texturen: `<name>_albedo` und `<name>_normal` — mehr nicht.
|
||||
@@ -197,6 +201,7 @@ Weitere Suiten (jeweils `blender --background --python <datei>`):
|
||||
| `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 |
|
||||
| `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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+4
-4
@@ -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",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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')
|
||||
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user