bl_info = { "name": "Stylized Rock Generator", "author": "D4rkst3r", "version": (2, 19, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( "Batch-Generator fuer stylized Rocks " "(Subsurf -> Displace -> Decimate -> Limited Dissolve) " "mit Presets, LOD-Gruppen, Material- und UV-Setup fuer Unreal Engine." ), "category": "Object", } import bpy import bmesh import os import json import math import random import traceback from bpy.props import ( IntProperty, FloatProperty, FloatVectorProperty, EnumProperty, BoolProperty, StringProperty, PointerProperty, ) from bpy.types import Operator, Panel, PropertyGroup from mathutils import Matrix, Vector # --------------------------------------------------------------------------- # Konstanten # --------------------------------------------------------------------------- TWO_PI = math.pi * 2.0 # Platzhalter-Materialien: (Name, Base-Color RGBA). Werden pro Rock rotiert. # Neutraler Slot fuer das Erzfels-Set: EIN Set fuer alle Erzsorten, welche # Sorte es ist (Kupfer/Eisen/Gold) macht die Material-Instanz in UE. ORE_MATERIAL = ("M_Rock_Ore", (0.30, 0.28, 0.26, 1.0)) # Unterhalb dieser Vertex-Zahl verschmieren die Erz-Nuggets zu Schlieren - die # Maske haengt an der Vertex-Dichte. Gemessen: bei ~150 Verts ist jedes dritte # ein Nugget-Zentrum, bei ~1300 liest es sich als einzelne Einschluesse. ORE_MIN_VERTS = 800 MATERIAL_PALETTE = ( ("M_Rock_Granite", (0.32, 0.30, 0.28, 1.0)), ("M_Rock_Basalt", (0.11, 0.11, 0.13, 1.0)), ("M_Rock_Sandstone", (0.55, 0.42, 0.30, 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). # 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. # "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": 6, "decimate_ratio_1": 0.70, "decimate_ratio_2": 0.92, "dissolve_angle": 17.0, "dissolve_angle_max": 25.0, "shading_mode": 'AUTO', } 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", "box_proportions", "box_variation", "subsurf_levels", "texture_type", "noise_scale", "displace_strength", "seed", "decimate_ratio_1", "decimate_ratio_2", "dissolve_angle", "generate_uv0", "generate_lightmap_uv", "randomize_transform", "scale_min", "scale_max", "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", "keep_size", "bake_vertex_cavity", "cavity_strength", "cavity_blur", "moss_amount", "moss_noise", "moss_seed", "part_mask_from_shells", "use_class_size", "base_size", ) # Eingebaute Presets (nicht ueberschreibbar/loeschbar). BUILTIN_PRESETS = { "Kleiner Kiesel": { "base_shape": "ICOSPHERE", "subsurf_levels": 3, "texture_type": "CLOUDS", "noise_scale": 0.6, "displace_strength": 0.18, "decimate_ratio_1": 0.3, "decimate_ratio_2": 0.6, "dissolve_angle": 6.0, "scale_min": 0.4, "scale_max": 0.7, "spacing": 1.5, }, "Grosser Bruchfels": { "base_shape": "CUBE", "subsurf_levels": 5, "texture_type": "VORONOI", "noise_scale": 1.4, "displace_strength": 0.5, "decimate_ratio_1": 0.2, "decimate_ratio_2": 0.45, "dissolve_angle": 4.0, "scale_min": 1.5, "scale_max": 2.5, "spacing": 5.0, }, "Lava-Rock": { "base_shape": "ICOSPHERE", "subsurf_levels": 4, "texture_type": "DISTORTED_NOISE", "noise_scale": 1.0, "displace_strength": 0.35, "decimate_ratio_1": 0.25, "decimate_ratio_2": 0.5, "dissolve_angle": 5.0, "scale_min": 0.9, "scale_max": 1.4, "spacing": 3.0, }, } class RockGenError(Exception): """Kontrollierter Fehler innerhalb der Rock-Generierung. Wird pro Rock gefangen, damit der Batch weiterlaeuft statt komplett abzustuerzen. """ pass # --------------------------------------------------------------------------- # Kleine Helfer (Fehlerbehandlung / Kontext) # --------------------------------------------------------------------------- def _apply_modifier(context, obj, mod_name): """Wendet einen Modifier an und wirft bei Fehlern RockGenError.""" context.view_layer.objects.active = obj try: res = bpy.ops.object.modifier_apply(modifier=mod_name) except RuntimeError as exc: raise RockGenError("Modifier '%s' Apply fehlgeschlagen: %s" % (mod_name, exc)) if 'CANCELLED' in res: raise RockGenError("Modifier '%s' Apply wurde abgebrochen." % mod_name) def _asset_name(settings, index, batch_count): """EcoGame-Schema: Fels_