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:
+112
-3
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user