Rock Generator 2.12.0: Vertex-Cavity auf beliebige Meshes + Teil-Maske

Der User baut Pilze von Hand (Hut und Stiel als getrennte Objekte, mit Strg+J
verbunden) und fragte, was er wegen Vertex-Farben beachten muss.

Zwei Ergaenzungen:

1) Button "Vertex-Cavity auf Auswahl": backt R=Cavity / G / B=Hoehe auf JEDES
   markierte Mesh - nicht mehr nur auf generierte Steine. Damit lassen sich
   handgebaute Assets (Pilze, Baumstumpf, Erzfels) auf dasselbe Layout bringen.

2) Option "G = Teil-Maske (Shells)": statt der Moos-Maske werden getrennte
   Mesh-Teile markiert. Der oberste/breiteste Teil bekommt G=1, der Rest 0.
   Bei Strg+J-verbundenen Assets ist die Trennung dadurch EXAKT statt ueber
   Normalen geschaetzt - beim Pilz also Hut vs Stiel. Ein Material toent beide
   unterschiedlich, ein zweiter Material-Slot entfaellt.
   Einteilige Meshes bleiben bewusst unmarkiert (G=0), damit die Maske nichts
   Falsches behauptet.

Am nachgebauten Pilz geprueft: 2 Shells erkannt, G im obersten Teil ausschliess-
lich 1.0, im Rest ausschliesslich 0.0, Maske binaer, Einzelteil-Mesh -> nur 0.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:43:30 +02:00
co-authored by Claude Opus 4.8
parent 2882c099b4
commit 512d61e370
7 changed files with 195 additions and 8 deletions
+8
View File
@@ -54,6 +54,14 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
**Moosfarbe** werden pro Material-Instanz geregelt, nicht im Mesh festgelegt.
Regler: `Cavity-Staerke`, `Weichzeichnen`, `Moos-Menge`, `Moos-Aufbruch`,
`Moos-Seed`.
**G alternativ als Teil-Maske:** Haken `G = Teil-Maske (Shells)` markiert
getrennte Mesh-Teile statt Moos — der oberste/breiteste Teil bekommt 1, der
Rest 0. Assets, die aus mehreren Objekten mit **Strg+J** verbunden wurden
(Pilz = Hut + Stiel), lassen sich damit EXAKT trennen statt ueber Normalen
geschaetzt. Ein Material toent dann beide Teile unterschiedlich — **kein
zweiter Material-Slot noetig**. Einteilige Meshes bleiben unmarkiert (G=0).
**Der Button `Vertex-Cavity auf Auswahl` backt das Layout auf JEDES markierte
Mesh** — also auch auf handgebaute Assets, nicht nur auf generierte Steine.
Verifiziert: R ohne Hoehenabhaengigkeit (unten 0.62 / oben 0.59), B steigt
sauber (0.08 -> 0.92), G oben staerker (0.20 -> 0.57), A konstant 1,
Seed aendert die Verteilung, und alle Kanaele ueberleben den FBX-Roundtrip.
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_rock_generator"
version = "2.11.0"
version = "2.12.0"
name = "Stylized Rock Generator"
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
maintainer = "D4rkst3r"
+4 -4
View File
@@ -32,7 +32,7 @@
"id": "stylized_rock_generator",
"name": "Stylized Rock Generator",
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
"version": "2.11.0",
"version": "2.12.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -48,9 +48,9 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_rock_generator-2.11.0.zip",
"archive_size": 20382,
"archive_hash": "sha256:50eeed1d105ad68487e25020fdf28f5e3b8d81e83f9a21b9d33968eaa06aff2d"
"archive_url": "./stylized_rock_generator-2.12.0.zip",
"archive_size": 21754,
"archive_hash": "sha256:6e18e32b841d627edfa6530587c99080c50d02ac4b0a532ed6079796bfa2aaf4"
},
{
"schema_version": "1.0.0",
Binary file not shown.
Binary file not shown.
+112 -3
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Rock Generator",
"author": "D4rkst3r",
"version": (2, 11, 0),
"version": (2, 12, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Rock Gen",
"description": (
@@ -61,7 +61,7 @@ PRESET_KEYS = (
"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",
"moss_amount", "moss_noise", "moss_seed", "part_mask_from_shells",
"use_class_size", "base_size",
)
@@ -865,6 +865,13 @@ class RockGenSettings(PropertyGroup):
name="Weichzeichnen", default=1, min=0, max=5,
description="Glaettet den Cavity-Verlauf ueber benachbarte Vertices",
)
part_mask_from_shells: BoolProperty(
name="G = Teil-Maske (Shells)", default=False,
description=("Statt der Moos-Maske: getrennte Mesh-Teile markieren. "
"Der oberste/breiteste Teil bekommt G=1 (z.B. Pilzhut), "
"der Rest 0 (Stiel). Nutzt Strg+J-verbundene Objekte aus - "
"damit tont EIN Material beide Teile unterschiedlich"),
)
moss_amount: FloatProperty(
name="Moos-Menge", default=0.55, min=0.0, max=1.0,
description=("Wie viel der Oberflaeche die Moos-/Schmutzmaske (Kanal G) "
@@ -1505,6 +1512,102 @@ class OBJECT_OT_bake_rock_textures(Operator):
# Operatoren: Presets
# ---------------------------------------------------------------------------
def _mesh_shells(me):
"""Zusammenhaengende Teile (Shells) als Listen von Polygon-Indizes.
Bei Assets, die aus mehreren Objekten mit Strg+J verbunden wurden (z.B.
Pilz = Hut + Stiel), bleiben die Teile geometrisch getrennt. Damit laesst
sich die Teil-Maske EXAKT bilden statt sie ueber Normalen zu schaetzen.
"""
adj = {}
for poly in me.polygons:
for ek in poly.edge_keys:
adj.setdefault(ek, []).append(poly.index)
seen = set()
shells = []
for poly in me.polygons:
if poly.index in seen:
continue
stack = [poly.index]
comp = set()
while stack:
pi = stack.pop()
if pi in comp:
continue
comp.add(pi)
for ek in me.polygons[pi].edge_keys:
for nb in adj.get(ek, ()):
if nb not in comp:
stack.append(nb)
seen |= comp
shells.append(comp)
return shells
def _shell_part_mask(me):
"""G-Wert je Polygon: 1 fuer den OBEREN/breiteren Teil (z.B. Hut), 0 sonst.
Bei nur einer Shell gibt es nichts zu trennen -> alles 0, damit die Maske
nicht faelschlich etwas markiert.
"""
shells = _mesh_shells(me)
if len(shells) < 2:
return {p.index: 0.0 for p in me.polygons}, len(shells)
infos = []
for sh in shells:
vs = {vi for pi in sh for vi in me.polygons[pi].vertices}
zs = [me.vertices[i].co.z for i in vs]
xs = [me.vertices[i].co.x for i in vs]
ys = [me.vertices[i].co.y for i in vs]
infos.append((sh, max(zs), max(max(xs) - min(xs), max(ys) - min(ys))))
# Oberster Teil, bei Gleichstand der breitere -> das ist der Hut/Deckel
top = max(infos, key=lambda t: (t[1], t[2]))
out = {}
for sh, _, _ in infos:
val = 1.0 if sh is top[0] else 0.0
for pi in sh:
out[pi] = val
return out, len(shells)
class ROCKGEN_OT_bake_cavity_selection(Operator):
bl_idname = "rockgen.bake_cavity_selection"
bl_label = "Vertex-Cavity auf Auswahl"
bl_description = ("Backt das Layout R=Cavity / G=Teil-Maske / B=Hoehe auf ALLE "
"ausgewaehlten Meshes - auch auf handgebaute Assets")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.rock_gen_settings
targets = [o for o in context.selected_objects if o.type == 'MESH']
if not targets:
self.report({'ERROR'}, "Kein Mesh ausgewaehlt.")
return {'CANCELLED'}
_ensure_object_mode(context)
done, notes = 0, []
for ob in targets:
try:
_bake_vertex_cavity(context, ob, s)
if s.part_mask_from_shells:
mask, n = _shell_part_mask(ob.data)
layer = ob.data.color_attributes.get("Cavity")
if layer is not None:
for poly in ob.data.polygons:
g = mask.get(poly.index, 0.0)
for li in poly.loop_indices:
c = layer.data[li].color
layer.data[li].color = (c[0], g, c[2], 1.0)
notes.append("%s: %d Teil(e)" % (ob.name, n))
done += 1
except Exception as exc: # noqa: BLE001
self.report({'WARNING'}, "%s: %s" % (ob.name, exc))
msg = "Cavity auf %d Mesh(es) gebacken." % done
if notes:
msg += " " + ", ".join(notes[:4])
self.report({'INFO'}, msg)
return {'FINISHED'}
class ROCKGEN_OT_randomize(Operator):
bl_idname = "rockgen.randomize"
bl_label = "Zufaellig"
@@ -1732,7 +1835,12 @@ class VIEW3D_PT_rock_generator(Panel):
sub.prop(settings, "moss_amount")
sub.prop(settings, "moss_noise")
sub.prop(settings, "moss_seed")
sub.label(text="R=Cavity G=Moos B=Hoehe", icon='INFO')
sub.prop(settings, "part_mask_from_shells")
sub.label(text="R=Cavity G=%s B=Hoehe"
% ("Teil-Maske" if settings.part_mask_from_shells else "Moos"),
icon='INFO')
box.separator()
box.operator("rockgen.bake_cavity_selection", icon='BRUSH_DATA')
box = layout.box()
box.label(text="Material")
@@ -1803,6 +1911,7 @@ classes = (
OBJECT_OT_generate_rocks,
OBJECT_OT_export_rocks_fbx,
OBJECT_OT_bake_rock_textures,
ROCKGEN_OT_bake_cavity_selection,
ROCKGEN_OT_randomize,
ROCKGEN_OT_preset_save,
ROCKGEN_OT_preset_load,
+70
View File
@@ -0,0 +1,70 @@
import bpy, sys, importlib.util, bmesh
spec=importlib.util.spec_from_file_location("srg", r"A:\eco\stylized_rock_generator\stylized_rock_generator.py")
r=importlib.util.module_from_spec(spec); sys.modules["srg"]=r; spec.loader.exec_module(r)
try: r.unregister()
except Exception: pass
r.register()
fails=[]
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
# Pilz nachbauen: Stiel (Zylinder) + Hut (UV-Kugel oben), dann Strg+J
bpy.ops.mesh.primitive_cylinder_add(vertices=10, radius=0.028, depth=0.126, location=(0,0,0.063))
stiel=bpy.context.active_object; stiel.name="Stiel"
bpy.ops.mesh.primitive_uv_sphere_add(segments=14, ring_count=7, radius=0.042, location=(0,0,0.13))
hut=bpy.context.active_object; hut.name="Hut"
hut.scale=(1,1,0.5); bpy.ops.object.transform_apply(scale=True)
for o in bpy.context.selected_objects: o.select_set(False)
stiel.select_set(True); hut.select_set(True)
bpy.context.view_layer.objects.active=stiel
bpy.ops.object.join()
pilz=bpy.context.active_object; pilz.name="Mushroom_Test"
print("Pilz: %d Tris" % sum(len(p.vertices)-2 for p in pilz.data.polygons))
s=bpy.context.scene.rock_gen_settings
s.bake_vertex_cavity=True; s.cavity_strength=0.7
s.part_mask_from_shells=True
pilz.select_set(True); bpy.context.view_layer.objects.active=pilz
res=bpy.ops.rockgen.bake_cavity_selection()
print("Operator:", list(res))
me=pilz.data
ca=me.color_attributes.get("Cavity")
if ca is None: fails.append("kein_attribut")
else:
# Hut liegt oben -> dort muss G=1 sein, am Stiel G=0
# Direkt gegen die Shell-Zerlegung pruefen statt ueber z-Schwellen:
# der oberste Teil (Hut) muss ueberall G=1 haben, alle anderen Teile G=0.
shells = r._mesh_shells(me)
infos = []
for sh in shells:
vs = {vi for pi in sh for vi in me.polygons[pi].vertices}
infos.append((sh, max(me.vertices[v].co.z for v in vs)))
infos.sort(key=lambda t: -t[1])
top_sh = infos[0][0]
g_top = {round(ca.data[li].color[1],3)
for pi in top_sh for li in me.polygons[pi].loop_indices}
g_rest = {round(ca.data[li].color[1],3)
for sh,_ in infos[1:] for pi in sh for li in me.polygons[pi].loop_indices}
print("Shells: %d | G im obersten Teil: %s | G im Rest: %s"
% (len(shells), sorted(g_top), sorted(g_rest)))
if g_top != {1.0}: fails.append("hut_nicht_1")
if g_rest != {0.0}: fails.append("stiel_nicht_0")
# G darf NUR 0 oder 1 sein (harte Maske)
vals={round(d.color[1],3) for d in ca.data}
print("G Werte insgesamt: %s" % sorted(vals))
if not vals <= {0.0,1.0}: fails.append("G_nicht_binaer")
R=[d.color[0] for d in ca.data]; B=[d.color[2] for d in ca.data]
print("R %.2f..%.2f B %.2f..%.2f" % (min(R),max(R),min(B),max(B)))
if max(B)-min(B) < 0.8: fails.append("B_kein_verlauf")
# Einzel-Shell-Fall: darf nichts faelschlich markieren
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.1, location=(1,0,0.1))
solo=bpy.context.active_object
for o in bpy.context.selected_objects: o.select_set(False)
solo.select_set(True); bpy.context.view_layer.objects.active=solo
bpy.ops.rockgen.bake_cavity_selection()
g2={round(d.color[1],3) for d in solo.data.color_attributes["Cavity"].data}
print("Einzelteil-Mesh -> G Werte: %s" % sorted(g2))
if g2 != {0.0}: fails.append("einzelshell_markiert")
r.unregister()
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))