diff --git a/README.md b/README.md index 1e771ee..992efb7 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,23 @@ 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. +- **Vertex-Farb-Layout v2 (kanal-getrennt), Attribut `Cavity`:** + + | Kanal | Inhalt | + |---|---| + | **R** | Cavity pur — 1 = offene Flaeche, 0 = tiefe Fuge (**ohne** Hoehen-Verlauf) | + | **G** | Moos-/Schmutzmaske — 1 = hier waechst was (Oberseite + Fugennaehe + Noise) | + | **B** | Hoehen-Verlauf — 0 = Unterseite, 1 = oben | + | **A** | 1 (Reserve) | + + Getrennt statt eingebacken: der erdige **Boden-Saum** am Stein-Fuss und die + **Moosfarbe** werden pro Material-Instanz geregelt, nicht im Mesh festgelegt. + Regler: `Cavity-Staerke`, `Weichzeichnen`, `Moos-Menge`, `Moos-Aufbruch`, + `Moos-Seed`. + 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. + - **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). diff --git a/blender_manifest.toml b/blender_manifest.toml index f7be3bd..81c5c10 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.8.1" +version = "2.9.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 187393f..a9eee63 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.8.1", + "version": "2.9.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -23,9 +23,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.8.1.zip", - "archive_size": 19215, - "archive_hash": "sha256:c548846fa03033b4aa2cf792ecd046118527926c5e09246c07095f77617dded1" + "archive_url": "./stylized_rock_generator-2.9.0.zip", + "archive_size": 19688, + "archive_hash": "sha256:78452434a3f175ae1a415b4d4b2abdb5cb803415499c114c0452fb0a4524959e" }, { "schema_version": "1.0.0", diff --git a/dist/stylized_rock_generator-2.8.1.zip b/dist/stylized_rock_generator-2.8.1.zip deleted file mode 100644 index 2ad8f45..0000000 Binary files a/dist/stylized_rock_generator-2.8.1.zip and /dev/null differ diff --git a/dist/stylized_rock_generator-2.9.0.zip b/dist/stylized_rock_generator-2.9.0.zip new file mode 100644 index 0000000..732217f Binary files /dev/null and b/dist/stylized_rock_generator-2.9.0.zip differ diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index fd1b5dd..2cedf4c 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, 8, 1), + "version": (2, 9, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( @@ -60,7 +60,8 @@ 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", + "bake_vertex_cavity", "cavity_strength", "cavity_blur", + "moss_amount", "moss_noise", "moss_seed", "use_class_size", "base_size", ) @@ -455,23 +456,27 @@ def _apply_random_transform(context, obj, settings, rng): def _bake_vertex_cavity(context, obj, settings): - """Cavity + Hoehen-Verlauf in die VERTEX-FARBEN backen. + """Vertex-Farb-Layout v2 - KANAL-GETRENNT im Attribut "Cavity". - 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. + R = Cavity pur 1 = offene Flaeche, 0 = tiefe Fuge (ohne Verlauf!) + G = Moos/Schmutz 1 = hier waechst was (Oberseite + Fugennaehe + Noise) + B = Hoehen-Verlauf 0 = Unterseite, 1 = oben + A = 1 (Reserve) - Aufloesung haengt an der Vertex-Dichte; fuer flaechige Platten reicht das, - fuer feine Struktur nicht (dann Textur nehmen). + Getrennt statt eingebacken, damit das Material selbst entscheidet: Boden-Saum + und Moosfarbe werden pro Material-Instanz geregelt, nicht im Mesh festgelegt. """ + import random as _rnd 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") + idx = me.color_attributes.find("Cavity") + if idx >= 0: + me.color_attributes.render_color_index = idx + # --- R: Cavity ueber Blenders Dirty-Vertex-Colors --- _deselect_all(context) context.view_layer.objects.active = obj obj.select_set(True) @@ -481,36 +486,51 @@ def _bake_vertex_cavity(context, obj, settings): 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, + 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. + data = layer.data + cav_raw = [data[i].color[0] for i in range(len(data))] + 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) + + # Normalen je Loop fuer den Oberseiten-Anteil + me.calc_normals_split() if hasattr(me, "calc_normals_split") else None + rng = _rnd.Random(settings.moss_seed) + # Noise-Wert je VERTEX (nicht je Loop), damit er nicht flackert + vnoise = [rng.random() for _ in range(len(me.vertices))] + amt = settings.cavity_strength - grad = settings.cavity_gradient - data = layer.data + moss = settings.moss_amount + mnoise = settings.moss_noise + for poly in me.polygons: + nz = poly.normal.z # 1 = zeigt nach oben 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) + c_raw = cav_raw[li] + + # R: reine Cavity, Staerke geregelt, KEIN Hoehen-Verlauf + r = max(0.0, min(1.0, 1.0 - (1.0 - c_raw) * amt)) + + # B: Hoehen-Verlauf 0..1 + b = max(0.0, min(1.0, (me.vertices[vi].co.z - zmin) / span)) + + # G: Moos - Oberseite UND Fugennaehe, mit Noise aufgebrochen + up = max(0.0, nz) # 0 unten, 1 oben + crev = 1.0 - c_raw # 1 in der Fuge + base = up * 0.7 + crev * 0.5 + n = 1.0 - mnoise + mnoise * vnoise[vi] + g = max(0.0, min(1.0, base * n * moss * 1.6)) + + data[li].color = (r, g, b, 1.0) def _apply_shading(context, obj, settings): @@ -807,10 +827,18 @@ class RockGenSettings(PropertyGroup): 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"), + 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) " + "markiert. Die FARBE bestimmt das Material, nicht das Mesh"), + ) + moss_noise: FloatProperty( + name="Moos-Aufbruch", default=0.55, min=0.0, max=1.0, + description="Bricht die Maske mit Rauschen auf, damit sie nicht flaechig wirkt", + ) + moss_seed: IntProperty( + name="Moos-Seed", default=0, min=0, max=9999, + description="Anderer Wert = andere Moos-Verteilung bei gleicher Form", ) # --- Material --- @@ -1658,8 +1686,12 @@ class VIEW3D_PT_rock_generator(Panel): 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") + sub.separator() + 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') box = layout.box() box.label(text="Material") diff --git a/tests/test_vertex_cavity.py b/tests/test_vertex_cavity.py index 91f0a94..a0e43e8 100644 --- a/tests/test_vertex_cavity.py +++ b/tests/test_vertex_cavity.py @@ -8,38 +8,62 @@ 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] +s.shading_mode='FLAT'; s.bake_vertex_cavity=True; s.set_origin_bottom=True +bpy.ops.object.generate_rocks_batch() +ob=[o for o in bpy.context.scene.objects if o.type=='MESH'][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") +d=me.color_attributes["Cavity"].data +R=[x.color[0] for x in d]; G=[x.color[1] for x in d] +B=[x.color[2] for x in d]; A=[x.color[3] for x in d] +def rng_(v): return (min(v),max(v)) +print("R (Cavity) %.2f..%.2f | G (Moos) %.2f..%.2f | B (Hoehe) %.2f..%.2f | A %.2f..%.2f" + % (*rng_(R),*rng_(G),*rng_(B),*rng_(A))) +if max(R)-min(R)<0.05: fails.append("R_ohne_kontrast") +if max(G)-min(G)<0.05: fails.append("G_ohne_kontrast") +if max(A)!=1.0 or min(A)!=1.0: fails.append("A_nicht_1") + +# B muss mit der Hoehe korrelieren +zs=[me.vertices[me.loops[i].vertex_index].co.z for i in range(len(d))] +zmin,zmax=min(zs),max(zs) +lo=[B[i] for i in range(len(d)) if zs[i] < zmin+(zmax-zmin)*0.15] +hi=[B[i] for i in range(len(d)) if zs[i] > zmin+(zmax-zmin)*0.85] +print("B unten=%.2f oben=%.2f (muss steigen)" % (sum(lo)/len(lo), sum(hi)/len(hi))) +if sum(lo)/len(lo) >= sum(hi)/len(hi): fails.append("B_falsch_herum") + +# R darf KEINEN Hoehen-Verlauf mehr haben (das war v1) +rlo=sum(R[i] for i in range(len(d)) if zs[i]zmin+(zmax-zmin)*0.85)/max(1,len(hi)) +print("R unten=%.2f oben=%.2f (Differenz soll klein sein)" % (rlo,rhi)) +if abs(rlo-rhi) > 0.25: fails.append("R_hat_noch_verlauf") + +# G soll oben mehr sein als unten +glo=sum(G[i] for i in range(len(d)) if zs[i]zmin+(zmax-zmin)*0.85)/max(1,len(hi)) +print("G unten=%.2f oben=%.2f (oben soll mehr sein)" % (glo,ghi)) +if ghi <= glo: fails.append("G_nicht_oben") + +# Moos-Seed aendert die Verteilung? +s.moss_seed=42 +for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True) +bpy.ops.object.generate_rocks_batch() +ob2=[o for o in bpy.context.scene.objects if o.type=='MESH'][0] +G2=[x.color[1] for x in ob2.data.color_attributes["Cavity"].data] +same = len(G)==len(G2) and all(abs(a-b)<1e-4 for a,b in zip(G,G2)) +print("Moos-Seed 0 vs 42 identisch:", same) +if same: fails.append("seed_wirkungslos") + +# FBX-Roundtrip +tmp=tempfile.mkdtemp(); fp=os.path.join(tmp,"v2.fbx") for o in bpy.context.selected_objects: o.select_set(False) -ob.select_set(True); bpy.context.view_layer.objects.active=ob +ob2.select_set(True); bpy.context.view_layer.objects.active=ob2 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))) +id_=imp.data.color_attributes[0].data +iR=[x.color[0] for x in id_]; iG=[x.color[1] for x in id_]; iB=[x.color[2] for x in id_] +print("NACH FBX: R %.2f..%.2f G %.2f..%.2f B %.2f..%.2f" % (*rng_(iR),*rng_(iG),*rng_(iB))) +if max(iG)-min(iG)<0.05 or max(iB)-min(iB)<0.05: fails.append("fbx_kanaele_verloren") r.unregister() print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))