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:
@@ -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,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"
|
||||
|
||||
Vendored
+4
-4
@@ -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",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+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,
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user