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
+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,