diff --git a/README.md b/README.md index 69e1eb1..dd29698 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung: | **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** | Erz-Maske — 1 = Erz, 0 = kein Erz. Ohne Erz-Bake bleibt der Kanal auf 1.0 | + | **A** | Erz-Maske, **invertiert**: 1.0 = kein Erz, 0.0 = Erz. Ohne Erz-Bake bleibt der Kanal durchgehend auf 1.0 | Getrennt statt eingebacken: der erdige **Boden-Saum** am Stein-Fuss und die **Moosfarbe** werden pro Material-Instanz geregelt, nicht im Mesh festgelegt. @@ -127,6 +127,27 @@ die Material-Instanz in UE. Knopf: **Erzfels-Set erzeugen (S + M)** erzeugt Regler: `Erz-Anteil`, `Nugget-Groesse` (Radius in m), `Erz-Seed`. +**Richtung des A-Kanals — wichtig.** Die Nuggets laufen auf **A = 0** zu, nicht +auf 1. Der Master rechnet: + +``` +Erz = (1 - A) * ErzMenge +``` + +`ErzMenge` ist ein MI-Regler mit **Standard 0**. Damit ignorieren normale +Stein-MIs den A-Kanal vollstaendig, und nur die Erz-MIs drehen ihn auf. Ein +Stein ohne Erz-Bake behaelt seine durchgehende 1.0 und liefert `(1-A) = 0` — +deshalb musste das bestehende 25er-Set **nicht** neu exportiert werden. + +Andersherum gebacken waere das Erz unsichtbar und der ganze *restliche* Stein +wuerde als Erz gelesen. `tests/test_erz.py` prueft deshalb direkt gegen diese +Formel und nicht gegen den Rohwert. + +Die **Nugget-Groesse bleibt absolut** (Radius in Metern): ein kleiner Brocken +bekommt weniger, dafuer proportional groessere Einschluesse. Das ist physikalisch +richtig, und das Spiel skaliert die Meshes per ScaleMin/Max ohnehin zufaellig - +eine groessenrelative Optik waere dadurch sowieso wieder zerlegt. + Nuggets statt Rauschen: gestreute Zentren mit weichem Abfall. Ein Noise-Feld gaebe wolkige Schlieren ueber den ganzen Stein, Erz sitzt aber in abgegrenzten Einschluessen. Die Zentren bevorzugen Fugen — Adern folgen Rissen, und das @@ -155,8 +176,8 @@ Material-Slots** wie der Mutterstein (die UE-Skripte mappen darueber) und beim Export eine eigene `UCX_`-Huelle. **Punkt 4a:** mit `Ein Stueck mit Erz` heisst ein zufaelliges Teil -`_Piece_Erz` und traegt Erz-Adern im A-Kanal. Alle anderen Teile behalten -A = 1.0. Ob das Erz-Stueck gespawnt wird, entscheidet die Loot-Logik im Spiel. +`_Piece_Erz` und traegt Erz-Adern im A-Kanal (Richtung 0). Alle anderen +Teile behalten durchgehend A = 1.0 und sind damit fuer den Master erzfrei. Ob das Erz-Stueck gespawnt wird, entscheidet die Loot-Logik im Spiel. **Wie die Bruchkanten zusammenpassen:** Voronoi ueber Halbraeume. Fuer jedes andere Zentrum wird an der Mittelsenkrechten geschnitten und die abgewandte diff --git a/blender_manifest.toml b/blender_manifest.toml index 4602900..c32a869 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.16.0" +version = "2.17.0" name = "Stylized Rock Generator" tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export" maintainer = "D4rkst3r" diff --git a/dist/index.html b/dist/index.html index f514219..929d951 100644 --- a/dist/index.html +++ b/dist/index.html @@ -40,14 +40,14 @@ 4.9KB - stylized_rock_generator-2.16.0 + stylized_rock_generator-2.17.0 Stylized Rock Generator Batch-Generator fuer stylized Rocks mit UE5-Export link 4.2.0 - ~ all all - 31.3KB + 31.5KB stylized_tree_generator-1.25.0 @@ -61,6 +61,6 @@ -

Built 2026-08-03, 06:23

+

Built 2026-08-03, 06:38

diff --git a/dist/index.json b/dist/index.json index 0514d69..8083681 100644 --- a/dist/index.json +++ b/dist/index.json @@ -57,7 +57,7 @@ "id": "stylized_rock_generator", "name": "Stylized Rock Generator", "tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export", - "version": "2.16.0", + "version": "2.17.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -73,9 +73,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.16.0.zip", - "archive_size": 32002, - "archive_hash": "sha256:cb9cb1d36073da86d67d5997b9266a7d3192587591995a0fd59f6fa12f57b3e1" + "archive_url": "./stylized_rock_generator-2.17.0.zip", + "archive_size": 32287, + "archive_hash": "sha256:5d6a1616822ac74059ca82a07c3b42976efb37b211b523cafdbb1864de3118f6" }, { "schema_version": "1.0.0", diff --git a/dist/stylized_rock_generator-2.16.0.zip b/dist/stylized_rock_generator-2.16.0.zip deleted file mode 100644 index fb72402..0000000 Binary files a/dist/stylized_rock_generator-2.16.0.zip and /dev/null differ diff --git a/dist/stylized_rock_generator-2.17.0.zip b/dist/stylized_rock_generator-2.17.0.zip new file mode 100644 index 0000000..f3bf76d Binary files /dev/null and b/dist/stylized_rock_generator-2.17.0.zip differ diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index 810f587..6541c6e 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, 16, 0), + "version": (2, 17, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( @@ -659,10 +659,18 @@ def _bake_vertex_cavity(context, obj, settings): n = 1.0 - mnoise + mnoise * vnoise[vi] g = max(0.0, min(1.0, base * n * moss * 1.6)) - # A: Erz-Maske. Ohne Erz bleibt der Kanal auf 1.0 - so wie im - # eingefrorenen Layout v2 festgelegt und wie das bestehende - # 25er-Set es bereits traegt. - a = 1.0 if ore_a is None else ore_a[vi] + # A: Erz-Maske, INVERTIERT geschrieben - 1.0 = kein Erz, 0.0 = Erz. + # + # Der Master rechnet `(1 - A) * ErzMenge`, wobei ErzMenge ein + # MI-Regler mit Standard 0 ist. Damit ignorieren normale Stein-MIs + # den Kanal vollstaendig, und nur die Erz-MIs drehen ihn auf. Ein + # Stein ohne Erz-Bake behaelt seine 1.0 und bleibt unveraendert - + # deshalb muss das bestehende 25er-Set nicht neu exportiert werden. + # + # Wichtig: die Nuggets muessen deshalb Richtung 0 laufen, nicht + # Richtung 1. Andersherum waere das Erz unsichtbar und der ganze + # RESTLICHE Stein wuerde als Erz gelesen. + a = 1.0 if ore_a is None else 1.0 - ore_a[vi] data[li].color = (r, g, b, a) @@ -989,9 +997,11 @@ class RockGenSettings(PropertyGroup): # --- Erz (A-Kanal) --- bake_ore: BoolProperty( name="Erz-Maske (A-Kanal)", default=False, - description=("Schreibt Erz-Nuggets in den A-Kanal. EIN neutrales Set fuer " - "ALLE Erzsorten - welches Erz es ist (Tint + Gluehen), " - "entscheidet die Material-Instanz in UE. Aus = A bleibt 1.0"), + description=("Schreibt Erz-Nuggets in den A-Kanal, und zwar Richtung 0: " + "1.0 = kein Erz, 0.0 = Erz. Der Master rechnet " + "(1 - A) x ErzMenge, ErzMenge ist ein MI-Regler mit " + "Standard 0 - normale Stein-MIs ignorieren den Kanal damit " + "komplett. EIN neutrales Set fuer ALLE Erzsorten"), ) ore_amount: FloatProperty( name="Erz-Anteil", default=0.18, min=0.0, max=1.0, @@ -2583,8 +2593,10 @@ class VIEW3D_PT_rock_vcol(_RockSub): box.operator("rockgen.fracture", icon='MOD_BUILD') col.label(text="R=Cavity G=%s B=Hoehe A=%s" % ("Teil-Maske" if s.part_mask_from_shells else "Moos", - "Erz" if s.bake_ore else "1.0"), + "Erz (0=Erz)" if s.bake_ore else "1.0"), icon='INFO') + if s.bake_ore: + col.label(text="Master rechnet (1-A) x ErzMenge", icon='NODE_MATERIAL') # Nicht in der ausgegrauten Spalte: der Knopf arbeitet auf der Auswahl # und ist auch dann sinnvoll, wenn beim Erzeugen nicht gebacken wird. layout.separator() diff --git a/tests/test_bruchstuecke.py b/tests/test_bruchstuecke.py index 5000a63..d47605d 100644 --- a/tests/test_bruchstuecke.py +++ b/tests/test_bruchstuecke.py @@ -110,9 +110,11 @@ for anzahl in (2, 3, 4): if lay is None: fails.append("%s: kein Vertex-Layout v2" % ob.name) elif ob.name.endswith("_Piece_Erz"): - if max(a) < 0.9 or min(a) > 0.01: - fails.append("%s: traegt keine Erz-Maske (A %.2f..%.2f)" - % (ob.name, min(a), max(a))) + # Der Master rechnet (1 - A): Nuggets muessen auf A = 0 zulaufen. + erz = [1.0 - x for x in a] + if max(erz) < 0.9 or min(erz) > 0.01: + fails.append("%s: traegt keine Erz-Maske ((1-A) %.2f..%.2f)" + % (ob.name, min(erz), max(erz))) else: if min(a) < 0.999: fails.append("%s: A ist nicht 1.0 (min %.2f) - nur das " diff --git a/tests/test_erz.py b/tests/test_erz.py index 11132a6..15e02f9 100644 --- a/tests/test_erz.py +++ b/tests/test_erz.py @@ -3,6 +3,10 @@ Kernaussagen, die hier festgenagelt werden: * EIN neutrales Set - die A-Maske sagt nur WO Erz sitzt, nicht welches. * Ohne Erz bleibt A auf 1.0 (Layout v2, wie das bestehende 25er-Set). +* A wird INVERTIERT gebacken: 1.0 = kein Erz, 0.0 = Erz. Der Master rechnet + `(1 - A) * ErzMenge` mit ErzMenge als MI-Regler (Standard 0) - normale + Stein-MIs ignorieren den Kanal damit komplett. Geprueft wird deshalb direkt + gegen diese Formel und nicht gegen den Rohwert. * Die Maske ueberlebt den FBX-Roundtrip. * Die Aufloesung der Maske haengt an der Vertex-Dichte - deshalb erzeugt das Erz-Set dicht. Bei ~150 Verts waren die Nugget-Zentren jeder dritte Vertex @@ -49,28 +53,37 @@ def einer(**kw): return ob, a, tris -# --- 1) Ohne Erz bleibt A auf 1.0 ---------------------------------------- +def erz_aus_a(a): + """Was der Master sieht: (1 - A). 0 = kein Erz, 1 = volles Erz.""" + return [1.0 - x for x in a] + + +# --- 1) Ohne Erz sieht der Master NICHTS --------------------------------- _, a, _ = einer(bake_ore=False, subsurf_levels=4) if min(a) < 0.999: fails.append("Ohne Erz ist A nicht 1.0 (min %.3f) - das bricht das " "eingefrorene Layout v2 und das bestehende 25er-Set" % min(a)) +if max(erz_aus_a(a)) > 0.001: + fails.append("Ohne Erz liefert (1-A) bis %.3f - ein normaler Stein wuerde " + "im Master Erz zeigen" % max(erz_aus_a(a))) # --- 2) Mit Erz: Maske vorhanden, Anteil steigt monoton ------------------- -print("%-24s %7s %7s %9s %7s" % ("Variante", "A min", "A max", "A>0.5", "Tris")) +print("%-24s %7s %7s %9s %7s" % ("Variante", "A min", "A max", "Erz-Anteil", "Tris")) anteile = [] for amt in (0.08, 0.18, 0.35): ob, a, tris = einer(bake_ore=True, ore_amount=amt, ore_nugget_size=0.10, subsurf_levels=5, decimate_ratio_1=0.40, decimate_ratio_2=0.80) - anteil = sum(1 for x in a if x > 0.5) / len(a) + erz = erz_aus_a(a) + anteil = sum(1 for x in erz if x > 0.5) / len(erz) anteile.append(anteil) print("%-24s %7.2f %7.2f %8.0f%% %7d" % ("Erz-Anteil %.2f" % amt, min(a), max(a), 100 * anteil, tris)) - if max(a) < 0.9: - fails.append("Erz-Anteil %.2f: A erreicht nur %.2f - keine Nuggets" - % (amt, max(a))) - if min(a) > 0.01: - fails.append("Erz-Anteil %.2f: A faellt nie auf 0 - der ganze Stein " + if max(erz) < 0.9: + fails.append("Erz-Anteil %.2f: (1-A) erreicht nur %.2f - die Nuggets " + "laufen nicht auf A=0 zu" % (amt, max(erz))) + if min(erz) > 0.01: + fails.append("Erz-Anteil %.2f: (1-A) faellt nie auf 0 - der ganze Stein " "waere Erz" % amt) if not (anteile[0] < anteile[1] < anteile[2]): fails.append("Erz-Anteil wirkt nicht monoton: %s" @@ -124,8 +137,9 @@ for ob in bpy.context.scene.objects: fails.append("%s hat nur %d Verts - zu grob fuer eine Nugget-Maske" % (ob.name, len(ob.data.vertices))) a = [c.color[3] for c in ob.data.color_attributes["Cavity"].data] - if max(a) < 0.9: - fails.append("%s traegt keine Erz-Maske (A max %.2f)" % (ob.name, max(a))) + if max(erz_aus_a(a)) < 0.9: + fails.append("%s traegt keine Erz-Maske ((1-A) max %.2f)" + % (ob.name, max(erz_aus_a(a)))) s_hoch = max(v for k, v in hoehen.items() if "_S_" in k) m_hoch = min(v for k, v in hoehen.items() if "_M_" in k) @@ -150,12 +164,13 @@ else: ob = [o for o in bpy.context.scene.objects if o.type == 'MESH' and not o.name.startswith("UCX_")][0] a = [c.color[3] for c in ob.data.color_attributes[0].data] - anteil = sum(1 for x in a if x > 0.5) / len(a) + erz = erz_aus_a(a) + anteil = sum(1 for x in erz if x > 0.5) / len(erz) print("Nach FBX (%s): A %.2f..%.2f, Erz auf %.0f%%" % (dateien[0], min(a), max(a), 100 * anteil)) - if max(a) < 0.9 or min(a) > 0.01: + if max(erz) < 0.9 or min(erz) > 0.01: fails.append("Erz-Maske ueberlebt den FBX-Roundtrip nicht " - "(A %.2f..%.2f)" % (min(a), max(a))) + "((1-A) %.2f..%.2f)" % (min(erz), max(erz))) print("") if fails: