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:
2026-07-31 02:42:17 +02:00
co-authored by Claude Opus 4.8
parent 833e929f82
commit d58bfe05e8
7 changed files with 93 additions and 14 deletions
+50 -7
View File
@@ -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')