Rock Gen 2.19.0: Schnellstart-Sorten + Dissolve als Bereich

Weniger einstellen muessen: oben im Panel Sorte waehlen, Haken druecken,
erzeugen. Die neun Unter-Panels bleiben fuer den Feinschliff.

  Findling     rund, weiche Kanten          ~1160 V / ~2320 Tris
  Platte       flach, liegt satt auf         ~670 V / ~1340 Tris
  Kantig       Bevel + Weighted Normal       ~350 V /  ~690 Tris
  Erzbrocken   dicht genug fuer die Maske   ~1280 V / ~2560 Tris, Slot M_Rock_Ore

dissolve_angle ist jetzt ein BEREICH statt eines festen Werts - direkt aus der
Rueckmeldung "hab ich immer auf 17-25 je nach Stein". Je Stein wird einer
gezogen; gemessen an sechs Findlingen bei 17-25 Grad: 1936-2928 Tris. Beide
Regler gleich setzen = fester Winkel wie frueher.

Zwei Sachen fielen beim Messen auf:

1) Der Dissolve-Winkel frisst deutlich Geometrie. Derselbe dichte Satz liefert
   bei niedrigem Standardwinkel ~2700 Tris, bei 17-25 Grad nur noch ~1030 und
   faellt damit unter ORE_MIN_VERTS - das Erz-Set haette seine EIGENE Warnung
   ausgeloest, sobald vorher eine Sorte mit hohem Winkel gewaehlt war. Der
   Bereich gehoert deshalb mit in DENSE_PRESET. Ueber vier Seeds und beide
   Groessenklassen nachgemessen: 0.70/0.92 haelt den Worst Case bei 847 Verts.

2) Erz-Dichte und Tri-Fenster kollidieren. Die Maske braucht ~800 Verts auch
   beim kleinen S-Brocken, das treibt einen M-Stein auf ~3400 Tris - ueber das
   Fenster 1500-3000. Deshalb entkoppelt: normale Sorten leichter (0.40/0.80),
   Erzbrocken knapper (0.45/0.82), das S+M-SET dichter (0.70/0.92), weil dort
   die kleinen Brocken die Untergrenze setzen.

Nebenbei gefunden und behoben: der erzwungene Ursprung wurde aus der
GEMEINSAMEN Bbox aller LOD-Stufen berechnet. Eine stark dezimierte Stufe kann
tiefer reichen als LOD0 - dann sass LOD0 sieben Zentimeter ueber dem Pivot und
der Stein haette in UE geschwebt. Unreal platziert nach LOD0, also gibt LOD0
jetzt den Ursprung vor.

Neu: tests/test_schnellstart.py. 18 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 14:21:14 +02:00
co-authored by Claude Opus 4.8
parent f996832d75
commit 74df6dfa1b
8 changed files with 295 additions and 16 deletions
+143 -8
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Rock Generator",
"author": "D4rkst3r",
"version": (2, 18, 0),
"version": (2, 19, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Rock Gen",
"description": (
@@ -51,6 +51,76 @@ MATERIAL_PALETTE = (
("M_Rock_Mossy", (0.24, 0.30, 0.18, 1.0)),
)
# Schnellstart: fertige Einstellungs-Saetze fuer die Steinsorten, die in
# EcoGame tatsaechlich vorkommen. Deckt den Alltag ab, ohne dass man sich durch
# neun Unter-Panels klickt - die bleiben fuer den Feinschliff.
#
# Der Limited-Dissolve-Winkel ist bewusst ein BEREICH: ein fester Wert passt nie
# fuer alle Steine einer Sorte, man stellt ihn sonst bei jedem Stein nach.
ROCK_KINDS = {
'FINDLING': {
"label": "Findling",
"desc": "Runder Brocken, weiche Kanten - der Allerwelts-Stein",
"values": {
"base_shape": 'ICOSPHERE', "subsurf_levels": 6,
# Leichter als der Erz-Satz: ein normaler Stein braucht die hohe
# Dichte nicht und bleibt so im Tri-Fenster 1500-3000. Nur die
# Erz-Maske haengt an der Vertex-Dichte.
"decimate_ratio_1": 0.40, "decimate_ratio_2": 0.80,
"dissolve_angle": 17.0, "dissolve_angle_max": 25.0,
"shading_mode": 'AUTO', "add_bevel": False,
"flatten_bottom": True, "randomize_shape": True,
},
},
'PLATTE': {
"label": "Platte",
"desc": "Flacher Brocken, liegt satt auf dem Boden",
"values": {
"base_shape": 'BOX', "box_proportions": (2.2, 1.6, 0.5),
"box_variation": 0.25, "subsurf_levels": 6,
# Hoeher als bei den runden Sorten: eine flache Grundform hat
# weniger Oberflaeche, mit 0.40/0.80 landete sie bei 930 Tris.
"decimate_ratio_1": 0.60, "decimate_ratio_2": 0.90,
# Flache Flaechen verschmelzen beim Dissolve staerker als gewoelbte:
# mit 20-28 Grad fiel die Platte auf 848 Tris. Gleicher Bereich wie
# die anderen Sorten haelt sie im Fenster.
"dissolve_angle": 17.0, "dissolve_angle_max": 25.0,
"shading_mode": 'AUTO', "add_bevel": False,
"flatten_bottom": True, "randomize_shape": True,
},
},
'KANTIG': {
"label": "Kantig",
"desc": "Gebrochener Fels mit harten Kanten (Bevel + Weighted Normal)",
"values": {
"base_shape": 'CUBE', "subsurf_levels": 4,
"decimate_ratio_1": 0.35, "decimate_ratio_2": 0.70,
"dissolve_angle": 8.0, "dissolve_angle_max": 14.0,
"shading_mode": 'FLAT', "add_bevel": True,
"add_weighted_normal": True,
"flatten_bottom": True, "randomize_shape": True,
},
},
'ERZ': {
"label": "Erzbrocken",
"desc": ("Dicht genug fuer eine saubere Erz-Maske, Slot M_Rock_Ore. "
"Fuer das komplette S+M-Set den Knopf im Vertex-Farben-Panel"),
"values": {
"base_shape": 'ICOSPHERE', "subsurf_levels": 6,
# Knapper als der Satz hinter dem S+M-SET: der einzelne Brocken hier
# ist M-gross, das Set enthaelt auch kleine S-Brocken und braucht
# dort mehr Reserve, um ORE_MIN_VERTS zu halten. Gemessen ueber
# fuenf Seeds: min 1078 Verts, max 2368 Tris.
"decimate_ratio_1": 0.45, "decimate_ratio_2": 0.82,
"dissolve_angle": 17.0, "dissolve_angle_max": 25.0,
"shading_mode": 'AUTO', "add_bevel": False,
"flatten_bottom": True, "randomize_shape": True,
"bake_vertex_cavity": True, "bake_ore": True,
},
},
}
# 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
@@ -60,10 +130,21 @@ MATERIAL_PALETTE = (
# "Smooth & dicht": kein Low-Poly-Look, und die Vertex-Masken (Cavity, Moos,
# Erz) haengen direkt an der Vertex-Dichte. Gemessen ueber drei Seeds:
# ~1350 Verts / ~2694 Tris - im Zielfenster 1500-3000 Tris.
# Der Dissolve-Winkel gehoert MIT hier hinein. Er frisst deutlich Geometrie:
# derselbe Satz liefert bei niedrigem Standardwinkel ~2700 Tris, bei 17-25 Grad
# nur noch ~1030 und faellt damit unter ORE_MIN_VERTS - das Erz-Set haette seine
# eigene Warnung ausgeloest, sobald jemand vorher eine Sorte mit hohem Winkel
# gewaehlt hatte. Gemessen ueber drei Seeds bei 17-25 Grad: 1162 Verts / 2320 Tris.
# Decimate bewusst hoch: ueber vier Seeds und beide Groessenklassen gemessen
# liefert 0.40/0.80 im schlechtesten Fall nur 708 Verts und faellt damit unter
# ORE_MIN_VERTS. 0.70/0.92 haelt den Worst Case bei 847 Verts und bleibt mit
# maximal 2806 Tris im Zielfenster.
DENSE_PRESET = {
"subsurf_levels": 5,
"decimate_ratio_1": 0.40,
"decimate_ratio_2": 0.80,
"subsurf_levels": 6,
"decimate_ratio_1": 0.70,
"decimate_ratio_2": 0.92,
"dissolve_angle": 17.0,
"dissolve_angle_max": 25.0,
"shading_mode": 'AUTO',
}
@@ -432,7 +513,12 @@ def _build_rock_mesh(context, settings, index, location, rng, name=None):
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.dissolve_limited(angle_limit=math.radians(settings.dissolve_angle))
# Der Winkel wird je Stein aus dem Bereich gezogen. Ein fester Wert passt
# nie fuer alle Steine einer Sorte - sonst stellt man ihn bei jedem Stein
# nach. Bereich zu = fester Wert wie vorher.
lo_a, hi_a = sorted((settings.dissolve_angle, settings.dissolve_angle_max))
eff_angle = rng.uniform(lo_a, hi_a) if hi_a > lo_a else lo_a
bpy.ops.mesh.dissolve_limited(angle_limit=math.radians(eff_angle))
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode='OBJECT')
@@ -957,6 +1043,19 @@ class RockGenSettings(PropertyGroup):
# --- Decimate / Cleanup ---
decimate_ratio_1: FloatProperty(name="Decimate 1", default=0.2, min=0.01, max=1.0)
decimate_ratio_2: FloatProperty(name="Decimate 2", default=0.5, min=0.01, max=1.0)
rock_kind: EnumProperty(
name="Sorte",
items=[(k, v["label"], v["desc"]) for k, v in ROCK_KINDS.items()],
default='FINDLING',
description="Schnellstart-Sorte - der Knopf daneben uebernimmt die Werte",
)
dissolve_angle_max: FloatProperty(
name="Winkel bis", default=25.0, min=0.0, max=80.0,
description=("Obergrenze fuer den Limited-Dissolve-Winkel. Je Stein wird "
"ein Wert aus dem Bereich gezogen - gleich gross wie der "
"untere Wert heisst fester Winkel"),
)
dissolve_angle: FloatProperty(
name="Dissolve Winkel (Grad)", default=5.0, min=0.0, max=45.0,
description="Groesserer Winkel = mehr Faces werden zusammengefasst",
@@ -1583,8 +1682,13 @@ def _prepare_export_copies(context, top, lay_flat=True, want_ucx=True,
top_copy.matrix_parent_inverse = Matrix.Identity(4)
context.view_layer.update()
# 1) Ursprung auf unten-mittig der gemeinsamen Bbox.
lo, hi = _bbox_of(meshes)
# 1) Ursprung auf unten-mittig - berechnet aus LOD0, NICHT aus der
# gemeinsamen Bbox aller Stufen. Eine stark dezimierte Stufe kann tiefer
# reichen als LOD0; nimmt man die gemeinsame Box, sitzt LOD0 danach ueber
# dem Pivot (gemessen 7 cm) und der Stein schwebt. Unreal platziert nach
# LOD0, also gibt LOD0 den Ursprung vor.
referenz = max(meshes, key=lambda o: len(o.data.vertices)) if meshes else None
lo, hi = _bbox_of([referenz] if referenz is not None else [])
if lo is not None:
shift = Vector((-(lo.x + hi.x) * 0.5, -(lo.y + hi.y) * 0.5, -lo.z))
off = Matrix.Translation(shift)
@@ -1798,6 +1902,27 @@ class ROCKGEN_OT_fracture(Operator):
return {'FINISHED'}
class ROCKGEN_OT_apply_kind(Operator):
"""Uebernimmt die Werte einer Schnellstart-Sorte."""
bl_idname = "rockgen.apply_kind"
bl_label = "Sorte uebernehmen"
bl_description = "Setzt Form, Dichte, Dissolve-Bereich und Shading der gewaehlten Sorte"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.rock_gen_settings
eintrag = ROCK_KINDS.get(s.rock_kind)
if eintrag is None:
self.report({'ERROR'}, "Unbekannte Sorte: %s" % s.rock_kind)
return {'CANCELLED'}
for k, v in eintrag["values"].items():
setattr(s, k, v)
self.report({'INFO'}, "Sorte '%s' uebernommen - Dissolve %.0f-%.0f Grad."
% (eintrag["label"], s.dissolve_angle,
s.dissolve_angle_max))
return {'FINISHED'}
class ROCKGEN_OT_dense_preset(Operator):
"""Stellt auf smooth und dicht um (~2700 Tris statt ~620)."""
bl_idname = "rockgen.dense_preset"
@@ -2477,6 +2602,13 @@ class VIEW3D_PT_rock_generator(Panel):
layout = self.layout
s = context.scene.rock_gen_settings
# Schnellstart zuerst: deckt den Alltag ab, ohne durch neun
# Unter-Panels zu klicken.
row = layout.row(align=True)
row.prop(s, "rock_kind", text="")
row.operator("rockgen.apply_kind", text="", icon='CHECKMARK')
layout.separator()
row = layout.row(align=True)
row.prop(s, "active_preset", text="")
row.operator("rockgen.preset_load", text="", icon='IMPORT')
@@ -2565,7 +2697,9 @@ class VIEW3D_PT_rock_detail(_RockSub):
col = layout.column(align=True)
col.prop(s, "decimate_ratio_1")
col.prop(s, "decimate_ratio_2")
layout.prop(s, "dissolve_angle")
row = layout.row(align=True)
row.prop(s, "dissolve_angle", text="Dissolve von")
row.prop(s, "dissolve_angle_max", text="bis")
layout.separator()
layout.prop(s, "add_bevel")
sub = layout.column(align=True)
@@ -2748,6 +2882,7 @@ classes = (
OBJECT_OT_export_rocks_fbx,
OBJECT_OT_bake_rock_textures,
ROCKGEN_OT_bake_cavity_selection,
ROCKGEN_OT_apply_kind,
ROCKGEN_OT_dense_preset,
ROCKGEN_OT_fracture,
ROCKGEN_OT_ore_set,