diff --git a/README.md b/README.md index dd29698..256b8b1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ sonst findet Blender die `bl_parent_id` nicht. Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung: **Benennung** -- Felsen: `Fels_
_` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`. +- Felsen: `Rock__` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`. + Das Praefix ist **`Rock`** (Standard) — die eingefrorene EcoGame-Konvention. Die Klasse setzt eine **Basis-Groesse** (S=0.45, M=1.0, L=1.9, XL=3.2) und ist damit ein bewusst anderes Asset — gemessen: S 0.85 m, M 1.9 m, L 3.5 m, XL 6.0 m. Davon strikt getrennt ist die **Zufalls-Streuung** (`scale_min/max`): die variiert @@ -108,6 +109,14 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung: - 1024 px reicht fuer stylized Formen. - Tileable (4D-Torus) passt zum triplanar mappenden Master-Material. +**Material-Slots (Teil des Vertrags, keine Kosmetik)** +- Die UE-Skripte mappen ueber den **Slot-Namen**. Ohne Slot legt Unreal nur + einen Default an und die Konvention greift nicht mehr — deshalb ist + `Material zuweisen` **Standard an**. +- Normale Steine rotieren durch `M_Rock_Granite/Basalt/Sandstone/Mossy`. +- Das Erzfels-Set bekommt **einen neutralen Slot `M_Rock_Ore`** — ein Set fuer + alle Erzsorten, welche Sorte es ist macht die Material-Instanz. + **Mesh** - Echte Meter, Ursprung unten-mittig, FBX pro Asset bzw. LOD-Gruppe. - **UV1 Lightmap ist Default aus** — EcoGame beleuchtet dynamisch. diff --git a/blender_manifest.toml b/blender_manifest.toml index c32a869..d75e940 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.17.0" +version = "2.18.0" name = "Stylized Rock Generator" tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export" maintainer = "D4rkst3r" diff --git a/dist/index.html b/dist/index.html index 929d951..ddfa83f 100644 --- a/dist/index.html +++ b/dist/index.html @@ -40,14 +40,14 @@ 4.9KB - stylized_rock_generator-2.17.0 + stylized_rock_generator-2.18.0 Stylized Rock Generator Batch-Generator fuer stylized Rocks mit UE5-Export link 4.2.0 - ~ all all - 31.5KB + 32.2KB stylized_tree_generator-1.25.0 @@ -61,6 +61,6 @@ -

Built 2026-08-03, 06:38

+

Built 2026-08-03, 12:08

diff --git a/dist/index.json b/dist/index.json index 8083681..d057c54 100644 --- a/dist/index.json +++ b/dist/index.json @@ -57,7 +57,7 @@ "id": "stylized_rock_generator", "name": "Stylized Rock Generator", "tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export", - "version": "2.17.0", + "version": "2.18.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -73,9 +73,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.17.0.zip", - "archive_size": 32287, - "archive_hash": "sha256:5d6a1616822ac74059ca82a07c3b42976efb37b211b523cafdbb1864de3118f6" + "archive_url": "./stylized_rock_generator-2.18.0.zip", + "archive_size": 33014, + "archive_hash": "sha256:c690c130cff1bfd69f91c4d9685ba9fcb56b017cfc1b1211cbd2df91d15ba001" }, { "schema_version": "1.0.0", diff --git a/dist/stylized_rock_generator-2.17.0.zip b/dist/stylized_rock_generator-2.17.0.zip deleted file mode 100644 index f3bf76d..0000000 Binary files a/dist/stylized_rock_generator-2.17.0.zip and /dev/null differ diff --git a/dist/stylized_rock_generator-2.18.0.zip b/dist/stylized_rock_generator-2.18.0.zip new file mode 100644 index 0000000..8176e8b Binary files /dev/null and b/dist/stylized_rock_generator-2.18.0.zip differ diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index 6541c6e..640b07d 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, 17, 0), + "version": (2, 18, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( @@ -35,6 +35,15 @@ from mathutils import Matrix, Vector 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)), @@ -251,8 +260,14 @@ def _get_or_create_material(name, color): return mat -def _assign_material(obj, index): - name, color = MATERIAL_PALETTE[index % len(MATERIAL_PALETTE)] +def _assign_material(obj, index, fixed=None): + """Setzt den Material-Slot. Die UE-Skripte mappen ueber den SLOT-NAMEN, + deshalb ist die Benennung Teil des Vertrags und kein Kosmetik-Thema. + + `fixed` erzwingt einen bestimmten Slot - das Erzfels-Set nutzt das fuer + seinen neutralen `M_Rock_Ore`, statt durch die Sorten zu rotieren. + """ + name, color = fixed if fixed is not None else MATERIAL_PALETTE[index % len(MATERIAL_PALETTE)] mat = _get_or_create_material(name, color) obj.data.materials.clear() obj.data.materials.append(mat) @@ -781,7 +796,8 @@ def _build_lod_group(context, settings, index, location, rng, base_name=None): _bake_vertex_cavity(context, lod0, settings) if settings.assign_material: - _assign_material(lod0, index) + _assign_material(lod0, index, + fixed=ORE_MATERIAL if settings.bake_ore else None) lod0.name = base_name + "_LOD0" lod0.data.name = base_name + "_LOD0" @@ -844,7 +860,8 @@ def make_one_rock(context, settings, index, batch_count=1): if settings.set_origin_bottom: _set_origin_bottom(context, obj) if settings.assign_material: - _assign_material(obj, index) + _assign_material(obj, index, + fixed=ORE_MATERIAL if settings.bake_ore else None) return [obj] @@ -858,8 +875,26 @@ def _cleanup_partial(context, names_before): bpy.data.objects.remove(obj, do_unlink=True) -def _report_result(op, created, failed, cancelled=False): +def _report_result(op, created, failed, cancelled=False, settings=None): n_mesh = sum(1 for o in created if getattr(o, "type", None) == 'MESH') + + # Erz-Haken an, aber das Mesh ist zu grob? Dann verschmieren die Nuggets. + # Das faellt sonst erst in UE auf - und dort sieht es nach einem + # Material-Fehler aus, obwohl es an der Vertex-Dichte liegt. + if settings is not None and settings.bake_ore and not failed: + duenn = [o for o in created + if getattr(o, "type", None) == 'MESH' + and len(o.data.vertices) < ORE_MIN_VERTS] + if duenn: + op.report( + {'WARNING'}, + "%d Rock(s) erzeugt - aber %d davon haben unter %d Verts. Die " + "Erz-Nuggets verschmieren dabei zu Schlieren. Knopf " + "'Smooth & dicht' druecken und neu erzeugen." + % (n_mesh, len(duenn), ORE_MIN_VERTS), + ) + return + if failed: op.report( {'WARNING'}, @@ -1060,8 +1095,11 @@ class RockGenSettings(PropertyGroup): # --- Material --- assign_material: BoolProperty( - name="Material zuweisen", default=False, - description="Weist pro Rock ein Platzhalter-Material zu (rotiert durch eine Liste)", + name="Material zuweisen", default=True, + description=("Legt den Material-Slot an (M_Rock_, beim Erz-Set " + "neutral M_Rock_Ore). Die UE-Skripte mappen ueber den " + "Slot-NAMEN - ohne Slot erzeugt Unreal nur einen Default " + "und die Konvention greift nicht mehr"), ) # --- LOD --- @@ -1113,7 +1151,7 @@ class RockGenSettings(PropertyGroup): ) # --- Benennung (EcoGame-Schema: Fels__) --- name_prefix: StringProperty( - name="Praefix", default="Fels", + name="Praefix", default="Rock", description="Basisname, z.B. 'Fels' oder 'Felsblock'", ) name_form: StringProperty( @@ -1268,7 +1306,7 @@ class OBJECT_OT_generate_rocks(Operator): print("[RockGen] Rock %d fehlgeschlagen: %s" % (i, exc)) traceback.print_exc() _cleanup_partial(context, names_before) - _report_result(self, created, failed) + _report_result(self, created, failed, settings=settings) return {'FINISHED'} # --- Einstiegspunkt: entscheidet synchron vs. modal --- @@ -1294,13 +1332,15 @@ class OBJECT_OT_generate_rocks(Operator): if event.type == 'ESC': self._finish(context) - _report_result(self, self._created, self._failed, cancelled=True) + _report_result(self, self._created, self._failed, cancelled=True, + settings=context.scene.rock_gen_settings) return {'CANCELLED'} if event.type == 'TIMER': if self._index >= settings.batch_count: self._finish(context) - _report_result(self, self._created, self._failed) + _report_result(self, self._created, self._failed, + settings=context.scene.rock_gen_settings) return {'FINISHED'} names_before = set(bpy.data.objects.keys()) @@ -1800,6 +1840,7 @@ class ROCKGEN_OT_ore_set(Operator): "name_class": s.name_class, "name_numbered": s.name_numbered, "bake_ore": s.bake_ore, "bake_vertex_cavity": s.bake_vertex_cavity, "use_class_size": s.use_class_size, "seed": s.seed, + "assign_material": s.assign_material, } vorher.update({k: getattr(s, k) for k in DENSE_PRESET}) # Erz MUSS dicht: die Maske haengt an der Vertex-Dichte. Bei ~150 Verts @@ -1813,6 +1854,7 @@ class ROCKGEN_OT_ore_set(Operator): s.name_numbered = True s.bake_ore = True s.bake_vertex_cavity = True # A haengt am selben Attribut + s.assign_material = True # Slot M_Rock_Ore - die UE-Skripte mappen darueber s.use_class_size = True # S und M sollen sich in der Groesse unterscheiden gesamt = 0 diff --git a/tests/test_ecogame_rock.py b/tests/test_ecogame_rock.py index e22ae5c..f152076 100644 --- a/tests/test_ecogame_rock.py +++ b/tests/test_ecogame_rock.py @@ -11,11 +11,12 @@ for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) # --- Namensschema --- s.batch_count=3; s.subsurf_levels=3; s.use_modal=False; s.generate_lods=False -s.name_prefix="Fels"; s.name_form="a"; s.name_class='L' +# Eingefrorene EcoGame-Konvention: Rock___. +s.name_prefix="Rock"; s.name_form="a"; s.name_class='L' bpy.ops.object.generate_rocks_batch() names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH') print("NAMEN:", names) -if not all(n.startswith("Fels_A_L") for n in names): fails.append("naming") +if not all(n.startswith("Rock_A_L") for n in names): fails.append("naming") # Einzeln (ohne Nummer) for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True) @@ -23,7 +24,7 @@ s.batch_count=1 bpy.ops.object.generate_rocks_batch() single=[o.name for o in bpy.context.scene.objects if o.type=='MESH'] print("EINZELN:", single) -if single and single[0]!="Fels_A_L": fails.append("naming_single") +if single and single[0]!="Rock_A_L": fails.append("naming_single") print("LIGHTMAP-DEFAULT aus:", not m.RockGenSettings.bl_rna.properties['generate_lightmap_uv'].default) # --- Bake: nur _albedo + _normal, AO drin --- diff --git a/tests/test_erz.py b/tests/test_erz.py index 15e02f9..b0c10cd 100644 --- a/tests/test_erz.py +++ b/tests/test_erz.py @@ -34,6 +34,16 @@ for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) s = bpy.context.scene.rock_gen_settings +# Auslieferungszustand festhalten, BEVOR der Test etwas verstellt: die +# Material-Zuweisung muss standardmaessig AN sein, sonst kommt in UE nur ein +# Default-Slot an und die Konvention M_Rock_ greift nicht. +if not s.assign_material: + fails.append("assign_material ist im Auslieferungszustand aus - dann fehlt " + "der Material-Slot und die UE-Skripte koennen nicht mappen") +if s.name_prefix != "Rock": + fails.append("Standard-Praefix ist '%s', die eingefrorene Konvention " + "verlangt 'Rock'" % s.name_prefix) + s.use_modal = False s.assign_material = False s.generate_lods = False @@ -110,7 +120,7 @@ for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) s.batch_count = 2 s.subsurf_levels = 4 # absichtlich duenn: das Set muss selbst dicht stellen -s.name_prefix = "Fels" +s.name_prefix = "Fels" # absichtlich abweichend: das Set muss selbst umstellen s.name_form = "A" s.name_class = 'M' s.bake_ore = False @@ -140,6 +150,12 @@ for ob in bpy.context.scene.objects: if max(erz_aus_a(a)) < 0.9: fails.append("%s traegt keine Erz-Maske ((1-A) max %.2f)" % (ob.name, max(erz_aus_a(a)))) + # EIN neutraler Slot fuer ALLE Erzsorten. Die UE-Skripte mappen ueber den + # Slot-NAMEN - ohne Slot legt Unreal nur einen Default an. + slots = [m.name for m in ob.data.materials] + if slots != [rg.ORE_MATERIAL[0]]: + fails.append("%s: Slots %s, erwartet ['%s']" + % (ob.name, slots, rg.ORE_MATERIAL[0])) s_hoch = max(v for k, v in hoehen.items() if "_S_" in k) m_hoch = min(v for k, v in hoehen.items() if "_M_" in k) @@ -153,7 +169,37 @@ if (s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels) != m % str((s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels))) +# --- 4b) Normale Steine tragen die Sorten-Slots -------------------------- +for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) +s.bake_ore = False +s.name_prefix = "Rock" +s.batch_count = 4 +s.assign_material = True +bpy.ops.object.generate_rocks_batch() +normale = sorted((o for o in bpy.context.scene.objects if o.type == 'MESH'), + key=lambda o: o.name) +slots_normal = [[m.name for m in o.data.materials] for o in normale] +print("Normale Steine: %s" % ", ".join(x[0] if x else "KEINE" for x in slots_normal)) +for o, sl in zip(normale, slots_normal): + if not sl: + fails.append("%s hat keinen Material-Slot - Unreal legt dann nur einen " + "Default an und die Konvention M_Rock_ greift nicht" + % o.name) + elif not sl[0].startswith("M_Rock_"): + fails.append("%s: Slot '%s' folgt nicht M_Rock_" % (o.name, sl[0])) + elif sl[0] == rg.ORE_MATERIAL[0]: + fails.append("%s: traegt den Erz-Slot, obwohl kein Erz gebacken wurde" + % o.name) +if not any(n.startswith("Rock_") for n in (o.name for o in normale)): + fails.append("Standard-Praefix ist nicht 'Rock' - das bricht die " + "eingefrorene Konvention und erzeugt Asset-Zwillinge in UE") + # --- 5) A ueberlebt den FBX-Roundtrip ------------------------------------ +for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) +s.batch_count = 1 +bpy.ops.rockgen.ore_set() bpy.ops.object.export_rocks_fbx() dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx")) if not dateien: