Rock Generator 2.8.0: Cavity in Vertex-Farben (texturfreier Weg)

Idee kam vom User: die Steine ueber "smart materials" in UE loesen statt ueber
gebackene Texturen. Prozedurales Noise/Voronoi im Shader waere zu teuer (laeuft
pro Pixel), aber der Kern der Idee traegt - deshalb dieser Weg:

Neue Option "Cavity in Vertex-Farben": schreibt Cavity/AO plus optionalen
Hoehen-Verlauf in das Farb-Attribut "Cavity" (vertex_color_dirt + eigene
Nachbearbeitung fuer Staerke und Verlauf). Das Material braucht damit KEINE
Textur:  Base Color = Farbton x VertexColor, dazu Kantenlicht und Schattentint.

Gegenueber Triplanar sitzen die dunklen Fugen dadurch auf den ECHTEN Kanten des
Meshes statt darueberzuschweben - genau das passiert in der Genshin-Referenz.

Verifiziert: Attribut wird angelegt, Wertebereich 0.250..0.947 (echter Kontrast),
und es ueberlebt den FBX-Roundtrip UNVERAENDERT (gleicher Bereich nach Import).
Regler: Cavity-Staerke, Hoehen-Verlauf, Weichzeichnen.
Neuer Test tests/test_vertex_cavity.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 09:10:54 +02:00
co-authored by Claude Opus 4.8
parent 1df70c9e50
commit de2acdd381
7 changed files with 152 additions and 6 deletions
+9
View File
@@ -40,6 +40,15 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
Platten), `Fugenbreite` (klein = harte Fuge, flache Platte), `Platten flach`
(1 = Flaeche nahezu einfarbig). Ein dichtes Zellmuster ueber die ganze
Oberflaeche erschlaegt die Form und passt NICHT zu diesem Look.
- **Texturfreie Alternative — Vertex-Cavity:** Statt zu baken laesst sich die
Cavity in die **Vertex-Farben** schreiben (Attribut `Cavity`, ueberlebt den
FBX-Roundtrip verlustfrei — gemessen 0.250..0.947 vor und nach dem Import).
Das Material braucht dann **keine Textur**:
`Base Color = Farbton x VertexColor`, dazu Kantenlicht (Fresnel) und
Schattentint. Vorteil gegenueber Triplanar: die dunklen Fugen sitzen auf den
ECHTEN Kanten des Meshes statt darueberzuschweben. Grenze: die Aufloesung
haengt an der Vertex-Dichte — fuer flaechige Platten reicht sie, fuer feine
Struktur nicht.
- 1024 px reicht fuer stylized Formen.
- Tileable (4D-Torus) passt zum triplanar mappenden Master-Material.
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "stylized_rock_generator"
version = "2.7.0"
version = "2.8.0"
name = "Stylized Rock Generator"
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
maintainer = "D4rkst3r"
+4 -4
View File
@@ -7,7 +7,7 @@
"id": "stylized_rock_generator",
"name": "Stylized Rock Generator",
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
"version": "2.7.0",
"version": "2.8.0",
"type": "add-on",
"maintainer": "D4rkst3r",
"license": [
@@ -23,9 +23,9 @@
"Mesh",
"Modeling"
],
"archive_url": "./stylized_rock_generator-2.7.0.zip",
"archive_size": 18091,
"archive_hash": "sha256:2a3b20b6a915271e10b5246b77fc89361cab3dab2ead032aff4e4cd3526ccf33"
"archive_url": "./stylized_rock_generator-2.8.0.zip",
"archive_size": 19166,
"archive_hash": "sha256:4c05d56bd4962caf7ce2bddd086ecbf0f6e34eb7fc045f35770f8e6d5267b7ad"
},
{
"schema_version": "1.0.0",
Binary file not shown.
Binary file not shown.
+93 -1
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Rock Generator",
"author": "D4rkst3r",
"version": (2, 7, 0),
"version": (2, 8, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Rock Gen",
"description": (
@@ -60,6 +60,7 @@ PRESET_KEYS = (
"shading_mode", "autosmooth_angle",
"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", "cavity_gradient",
"use_class_size", "base_size",
)
@@ -453,6 +454,65 @@ def _apply_random_transform(context, obj, settings, rng):
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
def _bake_vertex_cavity(context, obj, settings):
"""Cavity + Hoehen-Verlauf in die VERTEX-FARBEN backen.
Warum: das Material braucht damit KEINE Textur. Base Color = Farbton x
Vertex-Farbe, dazu Kantenlicht und Schattentint im Shader - genau der
Genshin-Aufbau. Ausserdem sitzen die dunklen Fugen auf den ECHTEN Kanten
des Meshes, statt per Triplanar darueberzuschweben.
Aufloesung haengt an der Vertex-Dichte; fuer flaechige Platten reicht das,
fuer feine Struktur nicht (dann Textur nehmen).
"""
me = obj.data
layer = me.color_attributes.get("Cavity")
if layer is None:
layer = me.color_attributes.new(name="Cavity", type='BYTE_COLOR', domain='CORNER')
me.color_attributes.active_color = layer
me.color_attributes.render_color_index = me.color_attributes.find("Cavity")
_deselect_all(context)
context.view_layer.objects.active = obj
obj.select_set(True)
try:
bpy.ops.paint.vertex_color_set(color=(1.0, 1.0, 1.0, 1.0))
except RuntimeError:
pass
try:
bpy.ops.paint.vertex_color_dirt(
blur_strength=1.0,
blur_iterations=settings.cavity_blur,
clean_angle=3.14159,
dirt_angle=0.0,
dirt_only=False,
normalize=True,
)
except RuntimeError as exc:
raise RockGenError("Vertex-Cavity fehlgeschlagen: %s" % exc)
# Staerke regeln + optionaler Hoehen-Verlauf (unten dunkler, wie in der
# Referenz). Beides direkt auf den Farbwerten, das ist schnell genug.
zs = [v.co.z for v in me.vertices]
if not zs:
return
zmin, zmax = min(zs), max(zs)
span = max(zmax - zmin, 1e-6)
amt = settings.cavity_strength
grad = settings.cavity_gradient
data = layer.data
for poly in me.polygons:
for li in poly.loop_indices:
vi = me.loops[li].vertex_index
c = data[li].color
val = 1.0 - (1.0 - c[0]) * amt # Cavity abschwaechen
if grad > 0.0:
t = (me.vertices[vi].co.z - zmin) / span
val *= (1.0 - grad) + grad * (0.35 + 0.65 * t)
val = max(0.0, min(1.0, val))
data[li].color = (val, val, val, 1.0)
def _apply_shading(context, obj, settings):
"""Setzt den Shading-Look: FLAT (facettiert), SMOOTH oder AUTO (nach Winkel)."""
_deselect_all(context)
@@ -607,6 +667,8 @@ def make_one_rock(context, settings, index, batch_count=1):
_apply_hardsurface(context, obj, settings)
_apply_random_transform(context, obj, settings, rng)
_apply_shading(context, obj, settings)
if settings.bake_vertex_cavity:
_bake_vertex_cavity(context, obj, settings)
if settings.set_origin_bottom:
_set_origin_bottom(context, obj)
if settings.assign_material:
@@ -730,6 +792,27 @@ class RockGenSettings(PropertyGroup):
description="Setzt den Origin auf die Unterseiten-Mitte (sauberes Aufstellen in UE)",
)
# --- Vertex-Cavity (texturfreier Weg) ---
bake_vertex_cavity: BoolProperty(
name="Cavity in Vertex-Farben", default=False,
description=("Backt Cavity/AO in die Vertex-Farben (Attribut 'Cavity'). "
"Das Material braucht dann KEINE Textur: Base Color = Farbton "
"x Vertex-Farbe, Rest ueber Kantenlicht und Schattentint"),
)
cavity_strength: FloatProperty(
name="Cavity-Staerke", default=0.7, min=0.0, max=1.0,
description="0 = alles weiss, 1 = volle Abdunklung in den Vertiefungen",
)
cavity_blur: IntProperty(
name="Weichzeichnen", default=1, min=0, max=5,
description="Glaettet den Cavity-Verlauf ueber benachbarte Vertices",
)
cavity_gradient: FloatProperty(
name="Hoehen-Verlauf", default=0.35, min=0.0, max=1.0,
description=("Dunkelt den Stein nach unten hin ab (wie in der Referenz). "
"0 = aus"),
)
# --- Material ---
assign_material: BoolProperty(
name="Material zuweisen", default=False,
@@ -1567,6 +1650,15 @@ class VIEW3D_PT_rock_generator(Panel):
sub.prop(settings, "lod_count")
sub.prop(settings, "lod_ratio_step")
box = layout.box()
box.label(text="Vertex-Cavity (ohne Textur)")
box.prop(settings, "bake_vertex_cavity")
sub = box.column(align=True)
sub.enabled = settings.bake_vertex_cavity
sub.prop(settings, "cavity_strength")
sub.prop(settings, "cavity_gradient")
sub.prop(settings, "cavity_blur")
box = layout.box()
box.label(text="Material")
box.prop(settings, "assign_material")
+45
View File
@@ -0,0 +1,45 @@
import bpy, sys, importlib.util, os, tempfile
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)
s=bpy.context.scene.rock_gen_settings
s.batch_count=1; s.subsurf_levels=4; s.use_modal=False
s.shading_mode='FLAT'; s.bake_vertex_cavity=True
s.name_prefix="Fels"; s.name_form="A"; s.name_class='M'
res=bpy.ops.object.generate_rocks_batch()
obs=[o for o in bpy.context.scene.objects if o.type=='MESH']
ob=obs[0]
me=ob.data
names=[a.name for a in me.color_attributes]
print("FARB-ATTRIBUTE:", names)
if "Cavity" not in names: fails.append("kein_attribut")
else:
vals=[d.color[0] for d in me.color_attributes["Cavity"].data]
vals.sort()
print("CAVITY min=%.3f p10=%.3f median=%.3f max=%.3f (n=%d)"
% (vals[0], vals[len(vals)//10], vals[len(vals)//2], vals[-1], len(vals)))
if vals[-1]-vals[0] < 0.05: fails.append("kein_kontrast")
# Faceted?
sm=sum(1 for p in me.polygons if p.use_smooth)
print("Shading: smooth=%d / flat=%d" % (sm, len(me.polygons)-sm))
# FBX-Roundtrip: ueberleben die Vertex-Farben?
tmp=tempfile.mkdtemp(); fp=os.path.join(tmp,"cav.fbx")
for o in bpy.context.selected_objects: o.select_set(False)
ob.select_set(True); bpy.context.view_layer.objects.active=ob
bpy.ops.export_scene.fbx(filepath=fp, use_selection=True, colors_type='SRGB',
mesh_smooth_type='FACE', use_triangles=True)
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
bpy.ops.import_scene.fbx(filepath=fp)
imp=[o for o in bpy.data.objects if o.type=='MESH'][0]
inames=[a.name for a in imp.data.color_attributes]
print("NACH FBX-ROUNDTRIP:", inames, "Tris=%d" % sum(len(p.vertices)-2 for p in imp.data.polygons))
if not inames: fails.append("fbx_verliert_farben")
else:
iv=[d.color[0] for d in imp.data.color_attributes[0].data]
print(" Wertebereich nach Import: %.3f .. %.3f" % (min(iv), max(iv)))
r.unregister()
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))