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
+26
View File
@@ -127,6 +127,31 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
--- ---
## Schnellstart-Sorten
Ganz oben im Panel: Sorte waehlen, Haken druecken, erzeugen. Deckt den Alltag
ab — die neun Unter-Panels bleiben fuer den Feinschliff.
| Sorte | Form | Dissolve | gemessen |
|---|---|---|---|
| **Findling** | rund, weiche Kanten | 1725° | ~1160 V / ~2320 Tris |
| **Platte** | flach, liegt satt auf | 1725° | ~670 V / ~1340 Tris |
| **Kantig** | Bevel + Weighted Normal, Flat | 814° | ~350 V / ~690 Tris |
| **Erzbrocken** | dicht genug fuer die Nugget-Maske, Slot `M_Rock_Ore` | 1725° | ~1280 V / ~2560 Tris |
**Der Dissolve-Winkel ist ein Bereich, kein Wert.** Ein fester Winkel passt nie
fuer alle Steine einer Sorte — man stellt ihn sonst bei jedem Stein nach. Je
Stein wird einer aus dem Bereich gezogen; gemessen an sechs Findlingen bei
1725°: 19362928 Tris. Beide Regler gleich setzen = fester Winkel wie frueher.
> **Warum die Sorten unterschiedlich dicht sind.** Die Erz-Maske haengt an der
> Vertex-Dichte, ein normaler Stein braucht das nicht. Deshalb hat der
> Erzbrocken ein hoeheres Decimate als der Findling — sonst waere entweder das
> Erz verschmiert oder der normale Stein ueber dem Tri-Fenster. Der Satz hinter
> dem **S+M-Set** ist noch etwas dichter (0.70/0.92), weil dort auch kleine
> S-Brocken vorkommen: mit dem Findling-Satz fielen die im schlechtesten Fall
> auf 708 Verts und damit unter die Schwelle fuer eine saubere Maske.
## Erzfels-Set (A-Kanal) ## Erzfels-Set (A-Kanal)
**EIN neutrales Set fuer ALLE Erzsorten.** Die Maske im A-Kanal sagt nur, *wo* **EIN neutrales Set fuer ALLE Erzsorten.** Die Maske im A-Kanal sagt nur, *wo*
@@ -549,6 +574,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`.
|---|---| |---|---|
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 | | `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
| `tests/test_bruchstuecke.py` | Bruchstuecke: Volumen 100 % des Muttersteins, dichte Topologie, genau ein Erz-Stueck, Material-Slots, Ursprung, UCX je Teil | | `tests/test_bruchstuecke.py` | Bruchstuecke: Volumen 100 % des Muttersteins, dichte Topologie, genau ein Erz-Stueck, Material-Slots, Ursprung, UCX je Teil |
| `tests/test_schnellstart.py` | Schnellstart: jede Sorte liefert ohne Nacharbeit Geometrie + Slot, Erz-Sorte ueber der Dichte-Schwelle, Dissolve-Bereich streut je Stein |
| `tests/test_erz.py` | Erz-Maske: A=1.0 ohne Erz, Anteil monoton, Set-Namen/Groessen, Einstellungen zurueckgesetzt, FBX-Roundtrip, Tri-Fenster | | `tests/test_erz.py` | Erz-Maske: A=1.0 ohne Erz, Anteil monoton, Set-Namen/Groessen, Einstellungen zurueckgesetzt, FBX-Roundtrip, Tri-Fenster |
| `tests/test_export_robust.py` | Export am **reimportierten** FBX: Ursprung erzwungen, flach = liegend, UCX geschlossen/umschliessend/im Budget, LOD+Vertex-Farben | | `tests/test_export_robust.py` | Export am **reimportierten** FBX: Ursprung erzwungen, flach = liegend, UCX geschlossen/umschliessend/im Budget, LOD+Vertex-Farben |
| `tests/test_panels.py` | Alle vier Addons: registrieren, jedes Panel zeichnet, Unter-Panels im richtigen Tab, **jede Property irgendwo erreichbar** | | `tests/test_panels.py` | Alle vier Addons: registrieren, jedes Panel zeichnet, Unter-Panels im richtigen Tab, **jede Property irgendwo erreichbar** |
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "stylized_rock_generator" id = "stylized_rock_generator"
version = "2.18.0" version = "2.19.0"
name = "Stylized Rock Generator" name = "Stylized Rock Generator"
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export" tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
maintainer = "D4rkst3r" maintainer = "D4rkst3r"
+3 -3
View File
@@ -40,14 +40,14 @@
<td>4.9KB</td> <td>4.9KB</td>
</tr> </tr>
<tr> <tr>
<td><tt><a href="./stylized_rock_generator-2.18.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.18.0</a></tt></td> <td><tt><a href="./stylized_rock_generator-2.19.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.19.0</a></tt></td>
<td>Stylized Rock Generator</td> <td>Stylized Rock Generator</td>
<td>Batch-Generator fuer stylized Rocks mit UE5-Export</td> <td>Batch-Generator fuer stylized Rocks mit UE5-Export</td>
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td> <td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
<td>4.2.0 - ~</td> <td>4.2.0 - ~</td>
<td>all</td> <td>all</td>
<td>all</td> <td>all</td>
<td>32.2KB</td> <td>34.1KB</td>
</tr> </tr>
<tr> <tr>
<td><tt><a href="./stylized_tree_generator-1.25.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.25.0</a></tt></td> <td><tt><a href="./stylized_tree_generator-1.25.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.25.0</a></tt></td>
@@ -61,6 +61,6 @@
</tr> </tr>
</table> </table>
<center><p>Built 2026-08-03, 12:08</p></center> <center><p>Built 2026-08-03, 12:20</p></center>
</body> </body>
</html> </html>
+4 -4
View File
@@ -57,7 +57,7 @@
"id": "stylized_rock_generator", "id": "stylized_rock_generator",
"name": "Stylized Rock Generator", "name": "Stylized Rock Generator",
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export", "tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
"version": "2.18.0", "version": "2.19.0",
"type": "add-on", "type": "add-on",
"maintainer": "D4rkst3r", "maintainer": "D4rkst3r",
"license": [ "license": [
@@ -73,9 +73,9 @@
"Mesh", "Mesh",
"Modeling" "Modeling"
], ],
"archive_url": "./stylized_rock_generator-2.18.0.zip", "archive_url": "./stylized_rock_generator-2.19.0.zip",
"archive_size": 33014, "archive_size": 34968,
"archive_hash": "sha256:c690c130cff1bfd69f91c4d9685ba9fcb56b017cfc1b1211cbd2df91d15ba001" "archive_hash": "sha256:2b412f9324263a3951b1b519648ab46025e7511b09f54b6017edf6d834a17bc6"
}, },
{ {
"schema_version": "1.0.0", "schema_version": "1.0.0",
Binary file not shown.
Binary file not shown.
+143 -8
View File
@@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "Stylized Rock Generator", "name": "Stylized Rock Generator",
"author": "D4rkst3r", "author": "D4rkst3r",
"version": (2, 18, 0), "version": (2, 19, 0),
"blender": (4, 2, 0), "blender": (4, 2, 0),
"location": "View3D > Sidebar > Rock Gen", "location": "View3D > Sidebar > Rock Gen",
"description": ( "description": (
@@ -51,6 +51,76 @@ MATERIAL_PALETTE = (
("M_Rock_Mossy", (0.24, 0.30, 0.18, 1.0)), ("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). # Basis-Groesse je Groessen-Klasse (Meter-Faktor auf das Grundmesh).
# WICHTIG - Unterschied zur Zufalls-Streuung: DIESE Groesse ist gewollt und # WICHTIG - Unterschied zur Zufalls-Streuung: DIESE Groesse ist gewollt und
# unterscheidet die Klassen. Die Streuung (scale_min/max) variiert dagegen nur # 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, # "Smooth & dicht": kein Low-Poly-Look, und die Vertex-Masken (Cavity, Moos,
# Erz) haengen direkt an der Vertex-Dichte. Gemessen ueber drei Seeds: # Erz) haengen direkt an der Vertex-Dichte. Gemessen ueber drei Seeds:
# ~1350 Verts / ~2694 Tris - im Zielfenster 1500-3000 Tris. # ~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 = { DENSE_PRESET = {
"subsurf_levels": 5, "subsurf_levels": 6,
"decimate_ratio_1": 0.40, "decimate_ratio_1": 0.70,
"decimate_ratio_2": 0.80, "decimate_ratio_2": 0.92,
"dissolve_angle": 17.0,
"dissolve_angle_max": 25.0,
"shading_mode": 'AUTO', "shading_mode": 'AUTO',
} }
@@ -432,7 +513,12 @@ def _build_rock_mesh(context, settings, index, location, rng, name=None):
context.view_layer.objects.active = obj context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT') 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.select_all(action='SELECT')
bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
@@ -957,6 +1043,19 @@ class RockGenSettings(PropertyGroup):
# --- Decimate / Cleanup --- # --- Decimate / Cleanup ---
decimate_ratio_1: FloatProperty(name="Decimate 1", default=0.2, min=0.01, max=1.0) 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) 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( dissolve_angle: FloatProperty(
name="Dissolve Winkel (Grad)", default=5.0, min=0.0, max=45.0, name="Dissolve Winkel (Grad)", default=5.0, min=0.0, max=45.0,
description="Groesserer Winkel = mehr Faces werden zusammengefasst", 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) top_copy.matrix_parent_inverse = Matrix.Identity(4)
context.view_layer.update() context.view_layer.update()
# 1) Ursprung auf unten-mittig der gemeinsamen Bbox. # 1) Ursprung auf unten-mittig - berechnet aus LOD0, NICHT aus der
lo, hi = _bbox_of(meshes) # 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: if lo is not None:
shift = Vector((-(lo.x + hi.x) * 0.5, -(lo.y + hi.y) * 0.5, -lo.z)) shift = Vector((-(lo.x + hi.x) * 0.5, -(lo.y + hi.y) * 0.5, -lo.z))
off = Matrix.Translation(shift) off = Matrix.Translation(shift)
@@ -1798,6 +1902,27 @@ class ROCKGEN_OT_fracture(Operator):
return {'FINISHED'} 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): class ROCKGEN_OT_dense_preset(Operator):
"""Stellt auf smooth und dicht um (~2700 Tris statt ~620).""" """Stellt auf smooth und dicht um (~2700 Tris statt ~620)."""
bl_idname = "rockgen.dense_preset" bl_idname = "rockgen.dense_preset"
@@ -2477,6 +2602,13 @@ class VIEW3D_PT_rock_generator(Panel):
layout = self.layout layout = self.layout
s = context.scene.rock_gen_settings 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 = layout.row(align=True)
row.prop(s, "active_preset", text="") row.prop(s, "active_preset", text="")
row.operator("rockgen.preset_load", text="", icon='IMPORT') row.operator("rockgen.preset_load", text="", icon='IMPORT')
@@ -2565,7 +2697,9 @@ class VIEW3D_PT_rock_detail(_RockSub):
col = layout.column(align=True) col = layout.column(align=True)
col.prop(s, "decimate_ratio_1") col.prop(s, "decimate_ratio_1")
col.prop(s, "decimate_ratio_2") 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.separator()
layout.prop(s, "add_bevel") layout.prop(s, "add_bevel")
sub = layout.column(align=True) sub = layout.column(align=True)
@@ -2748,6 +2882,7 @@ classes = (
OBJECT_OT_export_rocks_fbx, OBJECT_OT_export_rocks_fbx,
OBJECT_OT_bake_rock_textures, OBJECT_OT_bake_rock_textures,
ROCKGEN_OT_bake_cavity_selection, ROCKGEN_OT_bake_cavity_selection,
ROCKGEN_OT_apply_kind,
ROCKGEN_OT_dense_preset, ROCKGEN_OT_dense_preset,
ROCKGEN_OT_fracture, ROCKGEN_OT_fracture,
ROCKGEN_OT_ore_set, ROCKGEN_OT_ore_set,
+118
View File
@@ -0,0 +1,118 @@
"""Schnellstart-Sorten + Dissolve-Bereich.
Der Schnellstart soll den Alltag abdecken, ohne dass man sich durch neun
Unter-Panels klickt. Damit das etwas wert ist, muss jede Sorte OHNE
Nacharbeit ein brauchbares Ergebnis liefern - genau das prueft diese Datei.
Der Dissolve-Winkel ist ein BEREICH, weil ein fester Wert nie fuer alle Steine
einer Sorte passt. Geprueft wird deshalb auch, dass der Bereich tatsaechlich je
Stein streut und nicht nur als Zahl im Panel steht.
Aufruf: blender --background --factory-startup --python tests/test_schnellstart.py
"""
import os
import sys
importlib_util = __import__("importlib.util", fromlist=["util"])
import bpy
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
spec = importlib_util.spec_from_file_location(
"rg", os.path.join(ROOT, "stylized_rock_generator.py"))
rg = importlib_util.module_from_spec(spec)
sys.modules["rg"] = rg
spec.loader.exec_module(rg)
rg.register()
fails = []
s = bpy.context.scene.rock_gen_settings
s.use_modal = False
def bauen(anzahl=3, seed=0):
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
s.batch_count = anzahl
s.seed = seed
bpy.ops.object.generate_rocks_batch()
return sorted((o for o in bpy.context.scene.objects if o.type == 'MESH'),
key=lambda o: o.name)
print("%-12s %7s %7s %10s %s" % ("Sorte", "Verts", "Tris", "Dissolve", "Slot"))
for kind, eintrag in rg.ROCK_KINDS.items():
s.rock_kind = kind
res = bpy.ops.rockgen.apply_kind()
if 'FINISHED' not in res:
fails.append("Sorte %s: apply_kind lieferte %s" % (kind, res))
continue
obs = bauen()
if not obs:
fails.append("Sorte %s erzeugt keine Geometrie" % kind)
continue
verts = sum(len(o.data.vertices) for o in obs) // len(obs)
tris = sum(sum(len(p.vertices) - 2 for p in o.data.polygons)
for o in obs) // len(obs)
slots = [m.name for m in obs[0].data.materials]
print("%-12s %7d %7d %5.0f-%-4.0f %s"
% (eintrag["label"], verts, tris, s.dissolve_angle,
s.dissolve_angle_max, slots or "KEINE"))
if not slots:
fails.append("Sorte %s: kein Material-Slot" % eintrag["label"])
elif not slots[0].startswith("M_Rock_"):
fails.append("Sorte %s: Slot '%s' folgt nicht M_Rock_<Sorte>"
% (eintrag["label"], slots[0]))
# Der Bereich muss sinnvoll sein - untere Grenze nicht ueber der oberen.
if s.dissolve_angle > s.dissolve_angle_max:
fails.append("Sorte %s: Dissolve-Bereich verdreht (%.0f > %.0f)"
% (eintrag["label"], s.dissolve_angle, s.dissolve_angle_max))
# Die Erz-Sorte muss dicht genug fuer eine saubere Nugget-Maske sein -
# sonst loest die eigene Vorlage die eigene Warnung aus.
if eintrag["values"].get("bake_ore"):
if slots != [rg.ORE_MATERIAL[0]]:
fails.append("Sorte %s: Slot %s, erwartet ['%s']"
% (eintrag["label"], slots, rg.ORE_MATERIAL[0]))
duenn = [o for o in obs if len(o.data.vertices) < rg.ORE_MIN_VERTS]
if duenn:
fails.append("Sorte %s: %d von %d Steinen unter %d Verts - die "
"eigene Vorlage wuerde die eigene Dichte-Warnung "
"ausloesen" % (eintrag["label"], len(duenn), len(obs),
rg.ORE_MIN_VERTS))
else:
if slots == [rg.ORE_MATERIAL[0]]:
fails.append("Sorte %s traegt den Erz-Slot ohne Erz"
% eintrag["label"])
# --- Der Dissolve-Bereich muss je Stein streuen --------------------------
s.rock_kind = 'FINDLING'
bpy.ops.rockgen.apply_kind()
obs = bauen(anzahl=6, seed=3)
tris = [sum(len(p.vertices) - 2 for p in o.data.polygons) for o in obs]
print("")
print("Findling x6 bei %.0f-%.0f Grad: %s"
% (s.dissolve_angle, s.dissolve_angle_max, tris))
if len(set(tris)) < 3:
fails.append("Dissolve-Bereich streut nicht je Stein: %s" % tris)
# Bereich zu = fester Winkel wie frueher. Muss weiter funktionieren.
s.dissolve_angle_max = s.dissolve_angle
obs = bauen(anzahl=2, seed=3)
if not obs:
fails.append("Geschlossener Dissolve-Bereich erzeugt keine Geometrie")
else:
print("Bereich zu (%.0f Grad): %d Steine, %d Tris"
% (s.dissolve_angle, len(obs),
sum(len(p.vertices) - 2 for p in obs[0].data.polygons)))
print("")
if fails:
print("ERGEBNIS: %d FEHLER" % len(fails))
for x in fails:
print(" - " + x)
sys.exit(1)
print("ERGEBNIS: ALLE CHECKS OK")