From 46fc05eec4ded588acd8baedbdbc6bfa923a0734 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Wed, 29 Jul 2026 07:01:10 +0200 Subject: [PATCH] v2.1.0: FBX-Export, Shading-Toggle, Pro-Rock-Varianz, Boden abflachen + Origin - FBX-Export-Button (UE-ready): je Rock/LOD-Gruppe eine FBX, use_custom_props fuer LodGroup, Triangulate, EDGE/FACE-Smoothing je nach Shading - Shading-Modus: Faceted / Smooth / Auto-Smooth-nach-Winkel - Pro-Rock-Varianz: Noise/Displace Min-Max + nicht-uniforme Skalierung - Boden abflachen (Bisect) + Origin unten-mittig - Live gegen Blender 5.1.2 getestet, Headless-Test gruen Co-Authored-By: Claude Opus 4.8 --- README.md | 11 + blender_manifest.toml | 2 +- dist/index.json | 8 +- dist/stylized_rock_generator-2.0.0.zip | Bin 9183 -> 0 bytes dist/stylized_rock_generator-2.1.0.zip | Bin 0 -> 11477 bytes stylized_rock_generator.py | 265 ++++++++++++++++++++++++- 6 files changed, 273 insertions(+), 13 deletions(-) delete mode 100644 dist/stylized_rock_generator-2.0.0.zip create mode 100644 dist/stylized_rock_generator-2.1.0.zip diff --git a/README.md b/README.md index 7d19af3..1e51f1d 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,17 @@ aussen (signed volume > 0), UV0 + UV1 vorhanden, und gibt eine Zusammenfassung --- +## Changelog v2.1.0 + +- **FBX-Export-Button (UE-ready):** exportiert jeden Rock / jede LOD-Gruppe als + eigene FBX mit `use_custom_props=True` (fuer `fbx_type=LodGroup`), Triangulate + und passendem Smoothing (`EDGE` bei Auto-Smooth, sonst `FACE`). + *UE-Import-Tipp:* Normal Import Method auf „Import Normals" stellen, „Import LODs" an. +- **Shading-Toggle:** Faceted / Smooth / Auto-Smooth-nach-Winkel. +- **Pro-Rock-Varianz:** Noise-Scale & Displace-Staerke zwischen Min/Max plus + optionale nicht-uniforme Skalierung → ein Batch wirkt variantenreich. +- **Boden abflachen (Bisect) + Origin unten-mittig** → Rocks stehen in UE plan auf. + ## Changelog v2.0.0 Gehaertet & erweitert gegenueber v1.0.0: diff --git a/blender_manifest.toml b/blender_manifest.toml index d12d621..2e5414b 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.0.0" +version = "2.1.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 98b9bcf..8853a70 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.0.0", + "version": "2.1.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -23,9 +23,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.0.0.zip", - "archive_size": 9183, - "archive_hash": "sha256:7bcf0d9cc56015157dfa17264d9ea51e624e8c0013f84b59069bcd3386815ad6" + "archive_url": "./stylized_rock_generator-2.1.0.zip", + "archive_size": 11477, + "archive_hash": "sha256:76911905d9e283ce2cbe85ab341194e3fcfcb431eb7cf3816b96e6d2358214ae" } ] } \ No newline at end of file diff --git a/dist/stylized_rock_generator-2.0.0.zip b/dist/stylized_rock_generator-2.0.0.zip deleted file mode 100644 index 93781ed9aa78b76de52a02020be4597b4d3a8bd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9183 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 ymZ1M}|3{?we{b=>Dca literal 0 HcmV?d00001 diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index 994467a..f2e1a4a 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, 0, 0), + "version": (2, 1, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( @@ -49,6 +49,9 @@ PRESET_KEYS = ( "add_bevel", "bevel_width", "bevel_segments", "add_weighted_normal", "assign_material", "generate_lods", "lod_count", "lod_ratio_step", "batch_count", "spacing", + "shading_mode", "autosmooth_angle", + "randomize_shape", "noise_scale_max", "displace_strength_max", "nonuniform_scale", + "flatten_bottom", "flatten_ratio", "set_origin_bottom", ) # Eingebaute Presets (nicht ueberschreibbar/loeschbar). @@ -285,6 +288,15 @@ def _build_rock_mesh(context, settings, index, location, rng): obj = context.active_object obj.name = "Rock_%03d" % index + # Effektive Form-Parameter (optional pro Rock variiert) + eff_noise = settings.noise_scale + eff_displace = settings.displace_strength + if settings.randomize_shape: + lo_n, hi_n = sorted((settings.noise_scale, settings.noise_scale_max)) + lo_d, hi_d = sorted((settings.displace_strength, settings.displace_strength_max)) + eff_noise = rng.uniform(lo_n, hi_n) + eff_displace = rng.uniform(lo_d, hi_d) + # 2. Subdivision Surface -> Apply subsurf = obj.modifiers.new(name="Subsurf", type='SUBSURF') subsurf.levels = settings.subsurf_levels @@ -294,18 +306,18 @@ def _build_rock_mesh(context, settings, index, location, rng): # 3. Displace + Noise-Textur -> Apply tex = bpy.data.textures.new(name="RockTex_%03d" % index, type=settings.texture_type) if settings.texture_type == 'VORONOI': - tex.noise_scale = settings.noise_scale + tex.noise_scale = eff_noise elif settings.texture_type == 'CLOUDS': - tex.noise_scale = settings.noise_scale + tex.noise_scale = eff_noise tex.noise_depth = 2 elif settings.texture_type == 'DISTORTED_NOISE': - tex.noise_scale = settings.noise_scale + tex.noise_scale = eff_noise tex.distortion = 1.0 displace = obj.modifiers.new(name="Displace", type='DISPLACE') displace.texture = tex displace.texture_coords = 'GLOBAL' - displace.strength = settings.displace_strength + displace.strength = eff_displace displace.mid_level = 0.5 # Variation pro Instanz: Objekt vor dem Apply an eine zufaellige (moderate!) @@ -349,6 +361,10 @@ def _build_rock_mesh(context, settings, index, location, rng): if _face_count(obj) == 0: raise RockGenError("Nach Limited Dissolve keine Faces mehr uebrig.") + # 5c. Boden abflachen (optional) - vor den UVs, damit die Unterseite UVs bekommt + if settings.flatten_bottom: + _flatten_bottom(context, obj, settings) + # 6. UVs if settings.generate_uv0: _make_uv0(context, obj) @@ -371,7 +387,6 @@ def _apply_hardsurface(context, obj, settings): _deselect_all(context) context.view_layer.objects.active = obj obj.select_set(True) - bpy.ops.object.shade_smooth() wn = obj.modifiers.new(name="WeightedNormal", type='WEIGHTED_NORMAL') wn.keep_sharp = True _apply_modifier(context, obj, wn.name) @@ -380,8 +395,12 @@ def _apply_hardsurface(context, obj, settings): def _apply_random_transform(context, obj, settings, rng): if not settings.randomize_transform: return - scale_factor = rng.uniform(settings.scale_min, settings.scale_max) - obj.scale = (scale_factor, scale_factor, scale_factor) + lo, hi = sorted((settings.scale_min, settings.scale_max)) + if settings.nonuniform_scale: + obj.scale = (rng.uniform(lo, hi), rng.uniform(lo, hi), rng.uniform(lo, hi)) + else: + s = rng.uniform(lo, hi) + obj.scale = (s, s, s) obj.rotation_euler = ( rng.uniform(0.0, TWO_PI), rng.uniform(0.0, TWO_PI), @@ -393,6 +412,77 @@ def _apply_random_transform(context, obj, settings, rng): bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) +def _apply_shading(context, obj, settings): + """Setzt den Shading-Look: FLAT (facettiert), SMOOTH oder AUTO (nach Winkel).""" + _deselect_all(context) + context.view_layer.objects.active = obj + obj.select_set(True) + mode = settings.shading_mode + if mode == 'FLAT': + bpy.ops.object.shade_flat() + elif mode == 'SMOOTH': + bpy.ops.object.shade_smooth() + else: # AUTO + try: + bpy.ops.object.shade_auto_smooth(angle=math.radians(settings.autosmooth_angle)) + except (RuntimeError, TypeError): + bpy.ops.object.shade_smooth() + # Scharfe Kanten nach Winkel markieren -> FBX-EDGE-Export traegt sie mit. + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.mark_sharp(clear=True) + bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(settings.autosmooth_angle)) + bpy.ops.mesh.mark_sharp() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + +def _flatten_bottom(context, obj, settings): + """Schneidet eine flache Unterseite (Bisect), damit der Rock plan aufsteht.""" + zs = [v.co.z for v in obj.data.vertices] + if not zs: + return + zmin, zmax = min(zs), max(zs) + cut_z = zmin + (zmax - zmin) * settings.flatten_ratio + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + try: + bpy.ops.mesh.bisect( + plane_co=(0.0, 0.0, cut_z), + plane_no=(0.0, 0.0, 1.0), + clear_inner=True, + use_fill=True, + ) + except RuntimeError as exc: + bpy.ops.object.mode_set(mode='OBJECT') + raise RockGenError("Boden abflachen (bisect) fehlgeschlagen: %s" % exc) + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.normals_make_consistent(inside=False) + bpy.ops.object.mode_set(mode='OBJECT') + if _face_count(obj) == 0: + raise RockGenError("Nach Boden-Abflachen keine Faces mehr uebrig.") + + +def _set_origin_bottom(context, obj): + """Setzt den Origin auf die Unterseiten-Mitte (fuer sauberes Aufstellen in UE).""" + mw = obj.matrix_world + coords = [mw @ v.co for v in obj.data.vertices] + if not coords: + return + cx = sum(c.x for c in coords) / len(coords) + cy = sum(c.y for c in coords) / len(coords) + zmin = min(c.z for c in coords) + scene = context.scene + prev_cursor = scene.cursor.location.copy() + scene.cursor.location = (cx, cy, zmin) + _deselect_all(context) + context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.origin_set(type='ORIGIN_CURSOR') + scene.cursor.location = prev_cursor + + # --------------------------------------------------------------------------- # LOD-Gruppen (fbx_type = LodGroup -> von Unreal beim FBX-Import erkannt) # --------------------------------------------------------------------------- @@ -443,6 +533,10 @@ def _build_lod_group(context, settings, index, location, rng): _make_lightmap_uv(context, lod) created.append(lod) + # Shading pro LOD (nach dem Decimate, damit scharfe Kanten zur Topologie passen) + for lod in created: + _apply_shading(context, lod, settings) + # Empty als LOD-Group-Parent empty = bpy.data.objects.new(base_name + "_LODGroup", None) empty.empty_display_type = 'PLAIN_AXES' @@ -470,6 +564,9 @@ def make_one_rock(context, settings, index): obj = _build_rock_mesh(context, settings, index, location, rng) _apply_hardsurface(context, obj, settings) _apply_random_transform(context, obj, settings, rng) + _apply_shading(context, obj, settings) + if settings.set_origin_bottom: + _set_origin_bottom(context, obj) if settings.assign_material: _assign_material(obj, index) return [obj] @@ -524,6 +621,12 @@ class RockGenSettings(PropertyGroup): noise_scale: FloatProperty(name="Noise Scale", default=1.0, min=0.01, max=10.0) displace_strength: FloatProperty(name="Displace Staerke", default=0.3, min=0.0, max=2.0) seed: IntProperty(name="Seed", default=0, min=0) + randomize_shape: BoolProperty( + name="Form pro Rock variieren", default=False, + description="Noise-Scale und Displace-Staerke pro Rock zufaellig zwischen Min und Max", + ) + noise_scale_max: FloatProperty(name="Noise Scale Max", default=2.0, min=0.01, max=10.0) + displace_strength_max: FloatProperty(name="Displace Max", default=0.5, min=0.0, max=2.0) # --- Decimate / Cleanup --- decimate_ratio_1: FloatProperty(name="Decimate 1", default=0.2, min=0.01, max=1.0) @@ -555,6 +658,35 @@ class RockGenSettings(PropertyGroup): description="Weighted-Normal-Modifier fuer sauberere Shading-Uebergaenge", ) + # --- Shading --- + shading_mode: EnumProperty( + name="Shading", + items=[ + ('FLAT', "Faceted", "Harte Flaechen - klassischer stylized Low-Poly-Look"), + ('SMOOTH', "Smooth", "Voll geglaettet"), + ('AUTO', "Auto-Smooth", "Glatt, aber scharfe Kanten oberhalb des Winkels"), + ], + default='AUTO', + ) + autosmooth_angle: FloatProperty( + name="Auto-Smooth Winkel (Grad)", default=30.0, min=0.0, max=180.0, + description="Kanten steiler als dieser Winkel bleiben hart", + ) + + # --- Form / Boden --- + flatten_bottom: BoolProperty( + name="Boden abflachen", default=False, + description="Schneidet eine flache Unterseite, damit der Rock plan aufsteht", + ) + flatten_ratio: FloatProperty( + name="Schnitthoehe", default=0.1, min=0.0, max=0.45, + description="Wie weit von unten abgeschnitten wird (Anteil der Hoehe)", + ) + set_origin_bottom: BoolProperty( + name="Origin unten-mittig", default=False, + description="Setzt den Origin auf die Unterseiten-Mitte (sauberes Aufstellen in UE)", + ) + # --- Material --- assign_material: BoolProperty( name="Material zuweisen", default=False, @@ -579,6 +711,10 @@ class RockGenSettings(PropertyGroup): randomize_transform: BoolProperty(name="Skalierung/Rotation randomisieren", default=True) scale_min: FloatProperty(name="Scale Min", default=0.8, min=0.1, max=5.0) scale_max: FloatProperty(name="Scale Max", default=1.2, min=0.1, max=5.0) + nonuniform_scale: BoolProperty( + name="Nicht-uniforme Skalierung", default=False, + description="Skaliert jede Achse einzeln (flache Platten vs. runde Brocken)", + ) # --- Batch --- batch_count: IntProperty(name="Anzahl Rocks", default=5, min=1, max=500) @@ -592,6 +728,16 @@ class RockGenSettings(PropertyGroup): description="Ab dieser Rock-Anzahl wird der modale (nicht-blockierende) Modus genutzt", ) + # --- FBX-Export --- + export_dir: StringProperty( + name="Export-Ordner", default="", subtype='DIR_PATH', + description="Zielordner fuer die FBX-Dateien (je Rock/LOD-Gruppe eine Datei)", + ) + export_selected_only: BoolProperty( + name="Nur Auswahl exportieren", default=False, + description="Exportiert nur die aktuell selektierten Rocks statt aller", + ) + # --- Presets --- active_preset: EnumProperty(name="Preset", items=_preset_items) new_preset_name: StringProperty(name="Preset-Name", default="") @@ -687,6 +833,84 @@ class OBJECT_OT_generate_rocks(Operator): context.workspace.status_text_set(None) +# --------------------------------------------------------------------------- +# FBX-Export (UE-ready) +# --------------------------------------------------------------------------- + +def _collect_export_targets(context, settings): + """Top-Level-Rocks: LOD-Group-Empties + freistehende Rock-Meshes ohne Parent.""" + tops = [] + for o in context.scene.objects: + is_group = (o.type == 'EMPTY' and o.get("fbx_type") == "LodGroup") + is_standalone = (o.type == 'MESH' and o.name.startswith("Rock_") and o.parent is None) + if is_group or is_standalone: + tops.append(o) + if settings.export_selected_only: + sel = set(context.selected_objects) + tops = [t for t in tops + if t in sel or any(c in sel for c in t.children_recursive)] + return tops + + +def _safe_filename(name): + keep = "-_.() " + return "".join(c for c in name if c.isalnum() or c in keep).strip() or "Rock" + + +class OBJECT_OT_export_rocks_fbx(Operator): + bl_idname = "object.export_rocks_fbx" + bl_label = "FBX exportieren" + bl_description = "Exportiert jeden Rock / jede LOD-Gruppe als eigene FBX (UE-ready)" + + def execute(self, context): + settings = context.scene.rock_gen_settings + directory = bpy.path.abspath(settings.export_dir) if settings.export_dir else "" + if not directory or not os.path.isdir(directory): + self.report({'ERROR'}, "Bitte einen gueltigen Export-Ordner waehlen.") + return {'CANCELLED'} + + targets = _collect_export_targets(context, settings) + if not targets: + self.report({'WARNING'}, "Keine Rocks zum Exportieren gefunden.") + return {'CANCELLED'} + + # AUTO -> Edge-Smoothing (traegt scharfe Kanten), sonst Face-Smoothing. + smooth_type = 'EDGE' if settings.shading_mode == 'AUTO' else 'FACE' + + _ensure_object_mode(context) + exported, failed = 0, 0 + for top in targets: + objs = list(top.children_recursive) + [top] + _deselect_all(context) + for o in objs: + o.select_set(True) + context.view_layer.objects.active = top + filepath = os.path.join(directory, _safe_filename(top.name) + ".fbx") + try: + bpy.ops.export_scene.fbx( + filepath=filepath, + use_selection=True, + object_types={'MESH', 'EMPTY'}, + use_mesh_modifiers=True, + mesh_smooth_type=smooth_type, + use_triangles=True, + use_custom_props=True, # noetig fuer fbx_type=LodGroup + add_leaf_bones=False, + bake_anim=False, + path_mode='AUTO', + ) + exported += 1 + except RuntimeError as exc: + failed += 1 + print("[RockGen] FBX-Export '%s' fehlgeschlagen: %s" % (top.name, exc)) + + if failed: + self.report({'WARNING'}, "%d FBX exportiert, %d fehlgeschlagen (Konsole)." % (exported, failed)) + else: + self.report({'INFO'}, "%d FBX-Datei(en) nach %s exportiert." % (exported, directory)) + return {'FINISHED'} + + # --------------------------------------------------------------------------- # Operatoren: Presets # --------------------------------------------------------------------------- @@ -799,6 +1023,11 @@ class VIEW3D_PT_rock_generator(Panel): box.prop(settings, "noise_scale") box.prop(settings, "displace_strength") box.prop(settings, "seed") + box.prop(settings, "randomize_shape") + sub = box.column(align=True) + sub.enabled = settings.randomize_shape + sub.prop(settings, "noise_scale_max") + sub.prop(settings, "displace_strength_max") box = layout.box() box.label(text="Decimate / Cleanup") @@ -815,6 +1044,18 @@ class VIEW3D_PT_rock_generator(Panel): sub.prop(settings, "bevel_segments") box.prop(settings, "add_weighted_normal") + box = layout.box() + box.label(text="Shading / Form") + box.prop(settings, "shading_mode") + row = box.row() + row.enabled = settings.shading_mode == 'AUTO' + row.prop(settings, "autosmooth_angle") + box.prop(settings, "flatten_bottom") + row = box.row() + row.enabled = settings.flatten_bottom + row.prop(settings, "flatten_ratio") + box.prop(settings, "set_origin_bottom") + box = layout.box() box.label(text="UV (Unreal Export)") box.prop(settings, "generate_uv0") @@ -838,6 +1079,7 @@ class VIEW3D_PT_rock_generator(Panel): row = box.row(align=True) row.prop(settings, "scale_min") row.prop(settings, "scale_max") + box.prop(settings, "nonuniform_scale") box = layout.box() box.label(text="Batch") @@ -851,6 +1093,12 @@ class VIEW3D_PT_rock_generator(Panel): layout.separator() layout.operator("object.generate_rocks_batch", icon='MESH_ICOSPHERE') + box = layout.box() + box.label(text="FBX-Export (Unreal)") + box.prop(settings, "export_dir") + box.prop(settings, "export_selected_only") + box.operator("object.export_rocks_fbx", icon='EXPORT') + # --------------------------------------------------------------------------- # Registration @@ -859,6 +1107,7 @@ class VIEW3D_PT_rock_generator(Panel): classes = ( RockGenSettings, OBJECT_OT_generate_rocks, + OBJECT_OT_export_rocks_fbx, ROCKGEN_OT_preset_save, ROCKGEN_OT_preset_load, ROCKGEN_OT_preset_delete,