diff --git a/README.md b/README.md index f41c8a1..1e771ee 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/blender_manifest.toml b/blender_manifest.toml index 7cdfb78..713129d 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -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" diff --git a/dist/index.json b/dist/index.json index e45b176..d3cfd85 100644 --- a/dist/index.json +++ b/dist/index.json @@ -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", diff --git a/dist/stylized_rock_generator-2.7.0.zip b/dist/stylized_rock_generator-2.7.0.zip deleted file mode 100644 index 7007ff0..0000000 Binary files a/dist/stylized_rock_generator-2.7.0.zip and /dev/null differ diff --git a/dist/stylized_rock_generator-2.8.0.zip b/dist/stylized_rock_generator-2.8.0.zip new file mode 100644 index 0000000..62b7260 Binary files /dev/null and b/dist/stylized_rock_generator-2.8.0.zip differ diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index bea0d64..9947c56 100644 --- a/stylized_rock_generator.py +++ b/stylized_rock_generator.py @@ -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") diff --git a/tests/test_vertex_cavity.py b/tests/test_vertex_cavity.py new file mode 100644 index 0000000..91f0a94 --- /dev/null +++ b/tests/test_vertex_cavity.py @@ -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))