Rock Gen 2.15.0: Erzfels-Set (Punkt 5) + Smooth/dicht (Punkt 6)
Punkt 5 - EIN neutrales Erzfels-Set fuer ALLE Erzsorten A-Kanal traegt die Erz-Maske: sie sagt nur WO Erz sitzt. WELCHES Erz (Kupfer/Eisen/Gold = Tint + Gluehen) macht die Material-Instanz in UE. Knopf "Erzfels-Set erzeugen (S + M)" liefert Rock_Ore_S_NN und Rock_Ore_M_NN und setzt alle Einstellungen danach zurueck. Regler: Erz-Anteil, Nugget-Groesse (Radius in m), Erz-Seed. Nuggets statt Noise: gestreute Zentren mit weichem Abfall. Ein Noise-Feld gaebe wolkige Schlieren, Erz sitzt aber in abgegrenzten Einschluessen. Die Zentren bevorzugen Fugen - Adern folgen Rissen, und das Gluehen liest sich in einer Vertiefung besser. Ohne Erz-Bake bleibt A auf 1.0 - wie im eingefrorenen Layout v2 und wie das bestehende 25er-Set es traegt. Punkt 6 vorgezogen, weil Punkt 5 davon abhaengt Die Maskenaufloesung haengt an der Vertex-Dichte. Bei den bisherigen ~150-Vert-Steinen war jedes dritte Vertex ein Nugget-Zentrum: im Render liefen die Nuggets zu grossen Schlieren zusammen statt einzelne Einschluesse zu bleiben. Bei ~1350 Verts liest es sich richtig. Deshalb gibt es jetzt "Smooth & dicht" (Unterteilung 5, Decimate 0.40/0.80, Auto-Smooth) und das Erz-Set stellt selbst darauf um. Gemessen ueber drei Seeds: 2708/2658/2716 Tris - im Zielfenster 1500-3000. Zusaetzlich ist die Nuggetzahl auf ein Zentrum je ~10 Verts gedeckelt. Nebenbei: _asset_name schrieb die Form-Bezeichnung pauschal gross, daraus wurde "Rock_ORE_S_01" statt des vereinbarten "Rock_Ore_S_01". Einzelne Buchstaben (Form A, B, C) werden weiter gross geschrieben, laengere Bezeichner behalten ihre Schreibweise. Gemessen: Erz-Anteil 0.08 -> 4 %, 0.18 -> 9 %, 0.35 -> 15 % der Oberflaeche, A 0.00..1.00, S bis 0.80 m / M ab 1.71 m, Maske ueberlebt den FBX-Roundtrip. Neu: tests/test_erz.py. 16 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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) |
|
| **R** | Cavity pur — 1 = offene Flaeche, 0 = tiefe Fuge (**ohne** Hoehen-Verlauf) |
|
||||||
| **G** | Moos-/Schmutzmaske — 1 = hier waechst was (Oberseite + Fugennaehe + Noise) |
|
| **G** | Moos-/Schmutzmaske — 1 = hier waechst was (Oberseite + Fugennaehe + Noise) |
|
||||||
| **B** | Hoehen-Verlauf — 0 = Unterseite, 1 = oben |
|
| **B** | Hoehen-Verlauf — 0 = Unterseite, 1 = oben |
|
||||||
| **A** | 1 (Reserve) |
|
| **A** | Erz-Maske — 1 = Erz, 0 = kein Erz. Ohne Erz-Bake bleibt der Kanal auf 1.0 |
|
||||||
|
|
||||||
Getrennt statt eingebacken: der erdige **Boden-Saum** am Stein-Fuss und die
|
Getrennt statt eingebacken: der erdige **Boden-Saum** am Stein-Fuss und die
|
||||||
**Moosfarbe** werden pro Material-Instanz geregelt, nicht im Mesh festgelegt.
|
**Moosfarbe** werden pro Material-Instanz geregelt, nicht im Mesh festgelegt.
|
||||||
@@ -118,6 +118,39 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Erzfels-Set (A-Kanal)
|
||||||
|
|
||||||
|
**EIN neutrales Set fuer ALLE Erzsorten.** Die Maske im A-Kanal sagt nur, *wo*
|
||||||
|
Erz sitzt. *Welches* Erz es ist (Kupfer/Eisen/Gold = Tint + Gluehen), entscheidet
|
||||||
|
die Material-Instanz in UE. Knopf: **Erzfels-Set erzeugen (S + M)** erzeugt
|
||||||
|
`Rock_Ore_S_NN` und `Rock_Ore_M_NN` und stellt danach alle Einstellungen zurueck.
|
||||||
|
|
||||||
|
Regler: `Erz-Anteil`, `Nugget-Groesse` (Radius in m), `Erz-Seed`.
|
||||||
|
|
||||||
|
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
|
||||||
|
Gluehen liest sich in einer Vertiefung besser als auf einer flachen Platte.
|
||||||
|
|
||||||
|
> **Die Maskenaufloesung haengt an der Vertex-Dichte.** Bei den bisherigen
|
||||||
|
> ~150-Vert-Steinen war jedes dritte Vertex ein Nugget-Zentrum: die Nuggets
|
||||||
|
> liefen zu grossen Schlieren zusammen. Deshalb erzeugt das Erz-Set **dicht**
|
||||||
|
> (siehe unten) — dort liest es sich als einzelne Einschluesse. Die Nuggetzahl
|
||||||
|
> ist zusaetzlich auf ein Zentrum je ~10 Verts gedeckelt.
|
||||||
|
|
||||||
|
Gemessen (dicht, ~2700 Tris): Erz-Anteil 0.08 → 4 %, 0.18 → 9 %, 0.35 → 15 %
|
||||||
|
der Oberflaeche. A faellt auf 0.00 und erreicht 1.00, und die Maske ueberlebt
|
||||||
|
den FBX-Roundtrip.
|
||||||
|
|
||||||
|
## Smooth & dicht
|
||||||
|
|
||||||
|
Knopf im Form-Panel: setzt Unterteilung 5, Decimate 0.40/0.80 und Auto-Smooth.
|
||||||
|
Gemessen ueber drei Seeds: **2708/2658/2716 Tris** (Schnitt 2694) — im
|
||||||
|
Zielfenster 1500–3000. Kein Low-Poly-Look, und alle Vertex-Masken (Cavity, Moos,
|
||||||
|
Erz) werden feiner.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Export-Haertung (Rock Gen)
|
## Export-Haertung (Rock Gen)
|
||||||
|
|
||||||
Drei Dinge passieren beim FBX-Export jetzt automatisch. Alles laeuft auf
|
Drei Dinge passieren beim FBX-Export jetzt automatisch. Alles laeuft auf
|
||||||
@@ -451,6 +484,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`.
|
|||||||
| Datei | prueft |
|
| Datei | prueft |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
|
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
|
||||||
|
| `tests/test_erz.py` | Erz-Maske: A=1.0 ohne Erz, Anteil monoton, Set-Namen/Groessen, Einstellungen zurueckgesetzt, FBX-Roundtrip, Tri-Fenster |
|
||||||
| `tests/test_export_robust.py` | Export am **reimportierten** FBX: Ursprung erzwungen, flach = liegend, UCX geschlossen/umschliessend/im Budget, LOD+Vertex-Farben |
|
| `tests/test_export_robust.py` | Export am **reimportierten** FBX: Ursprung erzwungen, flach = liegend, UCX geschlossen/umschliessend/im Budget, LOD+Vertex-Farben |
|
||||||
| `tests/test_panels.py` | Alle vier Addons: registrieren, jedes Panel zeichnet, Unter-Panels im richtigen Tab, **jede Property irgendwo erreichbar** |
|
| `tests/test_panels.py` | Alle vier Addons: registrieren, jedes Panel zeichnet, Unter-Panels im richtigen Tab, **jede Property irgendwo erreichbar** |
|
||||||
| `tests/test_tree_addon.py` | Tree-Addon: Presets, Wachstums-Stufen, UVs, Modifier-Apply |
|
| `tests/test_tree_addon.py` | Tree-Addon: Presets, Wachstums-Stufen, UVs, Modifier-Apply |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "stylized_rock_generator"
|
id = "stylized_rock_generator"
|
||||||
version = "2.14.0"
|
version = "2.15.0"
|
||||||
name = "Stylized Rock Generator"
|
name = "Stylized Rock Generator"
|
||||||
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
|
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
|
||||||
maintainer = "D4rkst3r"
|
maintainer = "D4rkst3r"
|
||||||
|
|||||||
Vendored
+3
-3
@@ -40,14 +40,14 @@
|
|||||||
<td>4.9KB</td>
|
<td>4.9KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><tt><a href="./stylized_rock_generator-2.14.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.14.0</a></tt></td>
|
<td><tt><a href="./stylized_rock_generator-2.15.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.15.0</a></tt></td>
|
||||||
<td>Stylized Rock Generator</td>
|
<td>Stylized Rock Generator</td>
|
||||||
<td>Batch-Generator fuer stylized Rocks mit UE5-Export</td>
|
<td>Batch-Generator fuer stylized Rocks mit UE5-Export</td>
|
||||||
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
|
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
|
||||||
<td>4.2.0 - ~</td>
|
<td>4.2.0 - ~</td>
|
||||||
<td>all</td>
|
<td>all</td>
|
||||||
<td>all</td>
|
<td>all</td>
|
||||||
<td>26.1KB</td>
|
<td>28.9KB</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><tt><a href="./stylized_tree_generator-1.25.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.25.0</a></tt></td>
|
<td><tt><a href="./stylized_tree_generator-1.25.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.25.0</a></tt></td>
|
||||||
@@ -61,6 +61,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<center><p>Built 2026-08-03, 06:02</p></center>
|
<center><p>Built 2026-08-03, 06:13</p></center>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Vendored
+4
-4
@@ -57,7 +57,7 @@
|
|||||||
"id": "stylized_rock_generator",
|
"id": "stylized_rock_generator",
|
||||||
"name": "Stylized Rock Generator",
|
"name": "Stylized Rock Generator",
|
||||||
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
|
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
|
||||||
"version": "2.14.0",
|
"version": "2.15.0",
|
||||||
"type": "add-on",
|
"type": "add-on",
|
||||||
"maintainer": "D4rkst3r",
|
"maintainer": "D4rkst3r",
|
||||||
"license": [
|
"license": [
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
"Mesh",
|
"Mesh",
|
||||||
"Modeling"
|
"Modeling"
|
||||||
],
|
],
|
||||||
"archive_url": "./stylized_rock_generator-2.14.0.zip",
|
"archive_url": "./stylized_rock_generator-2.15.0.zip",
|
||||||
"archive_size": 26683,
|
"archive_size": 29580,
|
||||||
"archive_hash": "sha256:f60aa4d5c8d73cabfccbccf19020ea30d31e77ad22dca5f8a7151e46c7ec1c42"
|
"archive_hash": "sha256:f57604a8176506626352d61e79a5a449f8d1aedced3aeed3a288a1abe5efcd51"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"schema_version": "1.0.0",
|
"schema_version": "1.0.0",
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+227
-6
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Stylized Rock Generator",
|
"name": "Stylized Rock Generator",
|
||||||
"author": "D4rkst3r",
|
"author": "D4rkst3r",
|
||||||
"version": (2, 14, 0),
|
"version": (2, 15, 0),
|
||||||
"blender": (4, 2, 0),
|
"blender": (4, 2, 0),
|
||||||
"location": "View3D > Sidebar > Rock Gen",
|
"location": "View3D > Sidebar > Rock Gen",
|
||||||
"description": (
|
"description": (
|
||||||
@@ -48,6 +48,16 @@ MATERIAL_PALETTE = (
|
|||||||
# die FORM innerhalb einer Klasse und wird von "keep_size" auf konstantes
|
# die FORM innerhalb einer Klasse und wird von "keep_size" auf konstantes
|
||||||
# Volumen normiert, damit sie sich nicht mit den ScaleMin/Max der Engine
|
# Volumen normiert, damit sie sich nicht mit den ScaleMin/Max der Engine
|
||||||
# multipliziert.
|
# multipliziert.
|
||||||
|
# "Smooth & dicht": kein Low-Poly-Look, und die Vertex-Masken (Cavity, Moos,
|
||||||
|
# Erz) haengen direkt an der Vertex-Dichte. Gemessen ueber drei Seeds:
|
||||||
|
# ~1350 Verts / ~2694 Tris - im Zielfenster 1500-3000 Tris.
|
||||||
|
DENSE_PRESET = {
|
||||||
|
"subsurf_levels": 5,
|
||||||
|
"decimate_ratio_1": 0.40,
|
||||||
|
"decimate_ratio_2": 0.80,
|
||||||
|
"shading_mode": 'AUTO',
|
||||||
|
}
|
||||||
|
|
||||||
CLASS_SIZES = {'S': 0.45, 'M': 1.0, 'L': 1.9, 'XL': 3.2}
|
CLASS_SIZES = {'S': 0.45, 'M': 1.0, 'L': 1.9, 'XL': 3.2}
|
||||||
|
|
||||||
# Property-Keys, die von Presets gespeichert/geladen werden.
|
# Property-Keys, die von Presets gespeichert/geladen werden.
|
||||||
@@ -117,9 +127,13 @@ def _apply_modifier(context, obj, mod_name):
|
|||||||
|
|
||||||
def _asset_name(settings, index, batch_count):
|
def _asset_name(settings, index, batch_count):
|
||||||
"""EcoGame-Schema: Fels_<Form>_<Klasse>, bei Batches mit laufender Nummer."""
|
"""EcoGame-Schema: Fels_<Form>_<Klasse>, bei Batches mit laufender Nummer."""
|
||||||
|
# Einzelne Buchstaben werden gross geschrieben (Form A, B, C ...), laengere
|
||||||
|
# Bezeichner behalten ihre Schreibweise - sonst wuerde aus dem vereinbarten
|
||||||
|
# "Rock_Ore_S_01" ein "Rock_ORE_S_01".
|
||||||
|
form = settings.name_form.strip() or "A"
|
||||||
|
form = form.upper() if len(form) == 1 else form
|
||||||
base = "%s_%s_%s" % (settings.name_prefix.strip() or "Fels",
|
base = "%s_%s_%s" % (settings.name_prefix.strip() or "Fels",
|
||||||
(settings.name_form.strip() or "A").upper(),
|
form, settings.name_class)
|
||||||
settings.name_class)
|
|
||||||
if settings.name_numbered and batch_count > 1:
|
if settings.name_numbered and batch_count > 1:
|
||||||
return "%s_%02d" % (base, index + 1)
|
return "%s_%02d" % (base, index + 1)
|
||||||
return base
|
return base
|
||||||
@@ -480,6 +494,85 @@ def _apply_random_transform(context, obj, settings, rng):
|
|||||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _ore_mask(me, settings):
|
||||||
|
"""Erz-Maske je VERTEX: gestreute Nuggets mit weichem Abfall.
|
||||||
|
|
||||||
|
Warum Nuggets und kein Noise: ein Noise-Feld ergibt wolkige Schlieren ueber
|
||||||
|
den ganzen Stein. Erz sitzt aber in abgegrenzten Einschluessen. Deshalb
|
||||||
|
werden Zentren gestreut und der Abfall darum herum gerechnet - dadurch sind
|
||||||
|
Menge und Korngroesse unabhaengig voneinander regelbar.
|
||||||
|
|
||||||
|
Die Zentren bevorzugen Fugen (niedrige Cavity): Adern folgen Rissen, und
|
||||||
|
das Gluehen liest sich in einer Vertiefung deutlich besser als auf einer
|
||||||
|
flachen Platte.
|
||||||
|
"""
|
||||||
|
import random as _rnd
|
||||||
|
from mathutils import kdtree
|
||||||
|
|
||||||
|
n_verts = len(me.vertices)
|
||||||
|
if n_verts == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
radius = max(settings.ore_nugget_size, 1e-4)
|
||||||
|
|
||||||
|
# Anzahl aus der Oberflaeche ableiten, nicht fest: sonst haette ein
|
||||||
|
# S-Brocken dieselbe Nuggetzahl wie ein M-Brocken und waere uebersaet.
|
||||||
|
area = sum(p.area for p in me.polygons)
|
||||||
|
pro_nugget = math.pi * radius * radius
|
||||||
|
n_nuggets = int(round(settings.ore_amount * area / max(pro_nugget, 1e-6)))
|
||||||
|
|
||||||
|
# Deckel an der AUFLOESUNG, nicht nur an einer festen Zahl: die Maske haengt
|
||||||
|
# an der Vertex-Dichte. Bei einem 150-Vert-Stein waren 46 Zentren jeder
|
||||||
|
# dritte Vertex - die Nuggets liefen zu grossen Schlieren zusammen statt
|
||||||
|
# einzelne Einschluesse zu bleiben. Ein Zentrum je ~10 Verts haelt sie
|
||||||
|
# getrennt. Wer feinere Nuggets will, braucht ein dichteres Mesh.
|
||||||
|
n_nuggets = max(1, min(n_nuggets, 96, max(1, n_verts // 10)))
|
||||||
|
|
||||||
|
# Fugen-Gewicht je Vertex aus der bereits gebackenen Cavity im R-Kanal.
|
||||||
|
layer = me.color_attributes.get("Cavity")
|
||||||
|
gewicht = [1.0] * n_verts
|
||||||
|
if layer is not None:
|
||||||
|
summe = [0.0] * n_verts
|
||||||
|
zahl = [0] * n_verts
|
||||||
|
for li, loop in enumerate(me.loops):
|
||||||
|
vi = loop.vertex_index
|
||||||
|
summe[vi] += layer.data[li].color[0]
|
||||||
|
zahl[vi] += 1
|
||||||
|
for vi in range(n_verts):
|
||||||
|
if zahl[vi]:
|
||||||
|
# 1 - Cavity -> in der Fuge gross
|
||||||
|
gewicht[vi] = 0.25 + 0.75 * (1.0 - summe[vi] / zahl[vi])
|
||||||
|
|
||||||
|
rng = _rnd.Random(settings.ore_seed)
|
||||||
|
gesamt = sum(gewicht) or 1.0
|
||||||
|
zentren = []
|
||||||
|
for _ in range(n_nuggets):
|
||||||
|
# gewichtetes Ziehen ohne cumsum-Tabelle (Meshes sind klein)
|
||||||
|
ziel = rng.random() * gesamt
|
||||||
|
lauf = 0.0
|
||||||
|
pick = n_verts - 1
|
||||||
|
for vi in range(n_verts):
|
||||||
|
lauf += gewicht[vi]
|
||||||
|
if lauf >= ziel:
|
||||||
|
pick = vi
|
||||||
|
break
|
||||||
|
zentren.append(me.vertices[pick].co.copy())
|
||||||
|
|
||||||
|
baum = kdtree.KDTree(len(zentren))
|
||||||
|
for i, co in enumerate(zentren):
|
||||||
|
baum.insert(co, i)
|
||||||
|
baum.balance()
|
||||||
|
|
||||||
|
maske = [0.0] * n_verts
|
||||||
|
for vi, v in enumerate(me.vertices):
|
||||||
|
_, _, dist = baum.find(v.co)
|
||||||
|
if dist is None or dist >= radius:
|
||||||
|
continue
|
||||||
|
t = 1.0 - dist / radius
|
||||||
|
maske[vi] = t * t * (3.0 - 2.0 * t) # Smoothstep: weiche Raender
|
||||||
|
return maske
|
||||||
|
|
||||||
|
|
||||||
def _bake_vertex_cavity(context, obj, settings):
|
def _bake_vertex_cavity(context, obj, settings):
|
||||||
"""Vertex-Farb-Layout v2 - KANAL-GETRENNT im Attribut "Cavity".
|
"""Vertex-Farb-Layout v2 - KANAL-GETRENNT im Attribut "Cavity".
|
||||||
|
|
||||||
@@ -536,6 +629,17 @@ def _bake_vertex_cavity(context, obj, settings):
|
|||||||
moss = settings.moss_amount
|
moss = settings.moss_amount
|
||||||
mnoise = settings.moss_noise
|
mnoise = settings.moss_noise
|
||||||
|
|
||||||
|
# --- A: Erz-Maske (Nuggets) ---------------------------------------------
|
||||||
|
# EIN neutrales Set fuer ALLE Erzsorten: die Maske sagt nur WO Erz sitzt.
|
||||||
|
# WELCHES Erz (Kupfer/Eisen/Gold = Tint + Gluehen) entscheidet die
|
||||||
|
# Material-Instanz in UE.
|
||||||
|
#
|
||||||
|
# Nuggets statt Rauschen: gestreute Zentren mit weichem Abfall. Ein
|
||||||
|
# Noise-Feld gaebe wolkige Schlieren, keine Einschluesse.
|
||||||
|
ore_a = None
|
||||||
|
if getattr(settings, "bake_ore", False) and settings.ore_amount > 0.0:
|
||||||
|
ore_a = _ore_mask(me, settings)
|
||||||
|
|
||||||
for poly in me.polygons:
|
for poly in me.polygons:
|
||||||
nz = poly.normal.z # 1 = zeigt nach oben
|
nz = poly.normal.z # 1 = zeigt nach oben
|
||||||
for li in poly.loop_indices:
|
for li in poly.loop_indices:
|
||||||
@@ -555,7 +659,12 @@ def _bake_vertex_cavity(context, obj, settings):
|
|||||||
n = 1.0 - mnoise + mnoise * vnoise[vi]
|
n = 1.0 - mnoise + mnoise * vnoise[vi]
|
||||||
g = max(0.0, min(1.0, base * n * moss * 1.6))
|
g = max(0.0, min(1.0, base * n * moss * 1.6))
|
||||||
|
|
||||||
data[li].color = (r, g, b, 1.0)
|
# 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]
|
||||||
|
|
||||||
|
data[li].color = (r, g, b, a)
|
||||||
|
|
||||||
|
|
||||||
def _apply_shading(context, obj, settings):
|
def _apply_shading(context, obj, settings):
|
||||||
@@ -877,6 +986,27 @@ class RockGenSettings(PropertyGroup):
|
|||||||
name="Weichzeichnen", default=1, min=0, max=5,
|
name="Weichzeichnen", default=1, min=0, max=5,
|
||||||
description="Glaettet den Cavity-Verlauf ueber benachbarte Vertices",
|
description="Glaettet den Cavity-Verlauf ueber benachbarte Vertices",
|
||||||
)
|
)
|
||||||
|
# --- 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"),
|
||||||
|
)
|
||||||
|
ore_amount: FloatProperty(
|
||||||
|
name="Erz-Anteil", default=0.18, min=0.0, max=1.0,
|
||||||
|
description=("Wie viel der Oberflaeche Erz traegt. Die Nuggetzahl wird aus "
|
||||||
|
"der Flaeche abgeleitet, damit ein S-Brocken nicht uebersaet ist"),
|
||||||
|
)
|
||||||
|
ore_nugget_size: FloatProperty(
|
||||||
|
name="Nugget-Groesse", default=0.10, min=0.01, max=1.0, unit='LENGTH',
|
||||||
|
description="Radius eines einzelnen Erz-Einschlusses in Metern",
|
||||||
|
)
|
||||||
|
ore_seed: IntProperty(
|
||||||
|
name="Erz-Seed", default=0, min=0, max=9999,
|
||||||
|
description="Andere Verteilung der Nuggets bei gleicher Form",
|
||||||
|
)
|
||||||
|
|
||||||
part_mask_from_shells: BoolProperty(
|
part_mask_from_shells: BoolProperty(
|
||||||
name="G = Teil-Maske (Shells)", default=False,
|
name="G = Teil-Maske (Shells)", default=False,
|
||||||
description=("Statt der Moos-Maske: getrennte Mesh-Teile markieren. "
|
description=("Statt der Moos-Maske: getrennte Mesh-Teile markieren. "
|
||||||
@@ -1435,6 +1565,84 @@ def _safe_filename(name):
|
|||||||
return "".join(c for c in name if c.isalnum() or c in keep).strip() or "Rock"
|
return "".join(c for c in name if c.isalnum() or c in keep).strip() or "Rock"
|
||||||
|
|
||||||
|
|
||||||
|
class ROCKGEN_OT_dense_preset(Operator):
|
||||||
|
"""Stellt auf smooth und dicht um (~2700 Tris statt ~620)."""
|
||||||
|
bl_idname = "rockgen.dense_preset"
|
||||||
|
bl_label = "Smooth & dicht"
|
||||||
|
bl_description = ("Setzt Unterteilung, Decimate und Shading auf den dichten "
|
||||||
|
"Smooth-Look (~1500-3000 Tris). Feinere Cavity- und "
|
||||||
|
"Erz-Aufloesung, kein Low-Poly-Look")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.rock_gen_settings
|
||||||
|
for k, v in DENSE_PRESET.items():
|
||||||
|
setattr(s, k, v)
|
||||||
|
self.report({'INFO'}, "Smooth & dicht: Unterteilung %d, Decimate %.2f/%.2f, "
|
||||||
|
"Shading %s" % (DENSE_PRESET["subsurf_levels"],
|
||||||
|
DENSE_PRESET["decimate_ratio_1"],
|
||||||
|
DENSE_PRESET["decimate_ratio_2"],
|
||||||
|
DENSE_PRESET["shading_mode"]))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ROCKGEN_OT_ore_set(Operator):
|
||||||
|
"""Erzeugt das Erzfels-Set: EIN neutrales Set in S und M.
|
||||||
|
|
||||||
|
Bewusst kein Set je Erzsorte - die A-Kanal-Maske sagt nur, WO Erz sitzt.
|
||||||
|
Welches Erz es ist (Kupfer/Eisen/Gold = Tint + Gluehen), entscheidet die
|
||||||
|
Material-Instanz in UE. S und M, weil die Brocken in Hoehlengaenge passen
|
||||||
|
muessen.
|
||||||
|
"""
|
||||||
|
bl_idname = "rockgen.ore_set"
|
||||||
|
bl_label = "Erzfels-Set erzeugen (S + M)"
|
||||||
|
bl_description = ("Erzeugt Rock_Ore_S_NN und Rock_Ore_M_NN mit Erz-Maske im "
|
||||||
|
"A-Kanal - ein Set fuer alle Erzsorten")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.rock_gen_settings
|
||||||
|
vorher = {
|
||||||
|
"name_prefix": s.name_prefix, "name_form": s.name_form,
|
||||||
|
"name_class": s.name_class, "name_numbered": s.name_numbered,
|
||||||
|
"bake_ore": s.bake_ore, "bake_vertex_cavity": s.bake_vertex_cavity,
|
||||||
|
"use_class_size": s.use_class_size, "seed": s.seed,
|
||||||
|
}
|
||||||
|
vorher.update({k: getattr(s, k) for k in DENSE_PRESET})
|
||||||
|
# Erz MUSS dicht: die Maske haengt an der Vertex-Dichte. Bei ~150 Verts
|
||||||
|
# sind die Nugget-Zentren jeder dritte Vertex und laufen zu Schlieren
|
||||||
|
# zusammen - gemessen und im Render bestaetigt. Bei ~1350 Verts liest es
|
||||||
|
# sich als einzelne Einschluesse.
|
||||||
|
for k, v in DENSE_PRESET.items():
|
||||||
|
setattr(s, k, v)
|
||||||
|
s.name_prefix = "Rock"
|
||||||
|
s.name_form = "Ore"
|
||||||
|
s.name_numbered = True
|
||||||
|
s.bake_ore = True
|
||||||
|
s.bake_vertex_cavity = True # A haengt am selben Attribut
|
||||||
|
s.use_class_size = True # S und M sollen sich in der Groesse unterscheiden
|
||||||
|
|
||||||
|
gesamt = 0
|
||||||
|
try:
|
||||||
|
for i, klasse in enumerate(("S", "M")):
|
||||||
|
s.name_class = klasse
|
||||||
|
# Anderer Seed je Klasse, sonst ist der M-Brocken nur ein
|
||||||
|
# aufgeblasener S-Brocken.
|
||||||
|
s.seed = vorher["seed"] + i * 4177
|
||||||
|
res = bpy.ops.object.generate_rocks_batch()
|
||||||
|
if 'FINISHED' not in res:
|
||||||
|
self.report({'WARNING'}, "Klasse %s abgebrochen." % klasse)
|
||||||
|
break
|
||||||
|
gesamt += s.batch_count
|
||||||
|
finally:
|
||||||
|
for k, v in vorher.items():
|
||||||
|
setattr(s, k, v)
|
||||||
|
|
||||||
|
self.report({'INFO'}, "Erzfels-Set: %d Brocken (Rock_Ore_S/M_NN), "
|
||||||
|
"Erz im A-Kanal." % gesamt)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_export_rocks_fbx(Operator):
|
class OBJECT_OT_export_rocks_fbx(Operator):
|
||||||
bl_idname = "object.export_rocks_fbx"
|
bl_idname = "object.export_rocks_fbx"
|
||||||
bl_label = "FBX exportieren"
|
bl_label = "FBX exportieren"
|
||||||
@@ -2098,6 +2306,7 @@ class VIEW3D_PT_rock_shape(_RockSub):
|
|||||||
layout.prop(s, "box_proportions")
|
layout.prop(s, "box_proportions")
|
||||||
layout.prop(s, "box_variation")
|
layout.prop(s, "box_variation")
|
||||||
layout.prop(s, "subsurf_levels")
|
layout.prop(s, "subsurf_levels")
|
||||||
|
layout.operator("rockgen.dense_preset", icon='MOD_SMOOTH')
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.prop(s, "texture_type")
|
layout.prop(s, "texture_type")
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
@@ -2171,8 +2380,18 @@ class VIEW3D_PT_rock_vcol(_RockSub):
|
|||||||
sub.prop(s, "moss_noise")
|
sub.prop(s, "moss_noise")
|
||||||
sub.prop(s, "moss_seed")
|
sub.prop(s, "moss_seed")
|
||||||
col.prop(s, "part_mask_from_shells")
|
col.prop(s, "part_mask_from_shells")
|
||||||
col.label(text="R=Cavity G=%s B=Hoehe"
|
col.separator()
|
||||||
% ("Teil-Maske" if s.part_mask_from_shells else "Moos"),
|
col.prop(s, "bake_ore")
|
||||||
|
sub = col.column(align=True)
|
||||||
|
sub.active = s.bake_ore
|
||||||
|
sub.prop(s, "ore_amount")
|
||||||
|
sub.prop(s, "ore_nugget_size")
|
||||||
|
sub.prop(s, "ore_seed")
|
||||||
|
sub.separator()
|
||||||
|
sub.operator("rockgen.ore_set", icon='MOD_EXPLODE')
|
||||||
|
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"),
|
||||||
icon='INFO')
|
icon='INFO')
|
||||||
# Nicht in der ausgegrauten Spalte: der Knopf arbeitet auf der Auswahl
|
# Nicht in der ausgegrauten Spalte: der Knopf arbeitet auf der Auswahl
|
||||||
# und ist auch dann sinnvoll, wenn beim Erzeugen nicht gebacken wird.
|
# und ist auch dann sinnvoll, wenn beim Erzeugen nicht gebacken wird.
|
||||||
@@ -2283,6 +2502,8 @@ classes = (
|
|||||||
OBJECT_OT_export_rocks_fbx,
|
OBJECT_OT_export_rocks_fbx,
|
||||||
OBJECT_OT_bake_rock_textures,
|
OBJECT_OT_bake_rock_textures,
|
||||||
ROCKGEN_OT_bake_cavity_selection,
|
ROCKGEN_OT_bake_cavity_selection,
|
||||||
|
ROCKGEN_OT_dense_preset,
|
||||||
|
ROCKGEN_OT_ore_set,
|
||||||
ROCKGEN_OT_randomize,
|
ROCKGEN_OT_randomize,
|
||||||
ROCKGEN_OT_preset_save,
|
ROCKGEN_OT_preset_save,
|
||||||
ROCKGEN_OT_preset_load,
|
ROCKGEN_OT_preset_load,
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""Erzfels-Set (Wunschliste Punkt 5) + Smooth/dicht (Punkt 6).
|
||||||
|
|
||||||
|
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).
|
||||||
|
* 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
|
||||||
|
und liefen zu Schlieren zusammen statt einzelne Einschluesse zu bleiben.
|
||||||
|
|
||||||
|
Aufruf: blender --background --factory-startup --python tests/test_erz.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"rg", os.path.join(ROOT, "stylized_rock_generator.py"))
|
||||||
|
rg = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["rg"] = rg
|
||||||
|
spec.loader.exec_module(rg)
|
||||||
|
rg.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.use_modal = False
|
||||||
|
s.assign_material = False
|
||||||
|
s.generate_lods = False
|
||||||
|
s.bake_vertex_cavity = True
|
||||||
|
|
||||||
|
|
||||||
|
def einer(**kw):
|
||||||
|
for o in list(bpy.data.objects):
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
s.batch_count = 1
|
||||||
|
for k, v in kw.items():
|
||||||
|
setattr(s, k, v)
|
||||||
|
bpy.ops.object.generate_rocks_batch()
|
||||||
|
ob = [o for o in bpy.context.scene.objects if o.type == 'MESH'][0]
|
||||||
|
a = [c.color[3] for c in ob.data.color_attributes["Cavity"].data]
|
||||||
|
tris = sum(len(p.vertices) - 2 for p in ob.data.polygons)
|
||||||
|
return ob, a, tris
|
||||||
|
|
||||||
|
|
||||||
|
# --- 1) Ohne Erz bleibt A auf 1.0 ----------------------------------------
|
||||||
|
_, 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))
|
||||||
|
|
||||||
|
# --- 2) Mit Erz: Maske vorhanden, Anteil steigt monoton -------------------
|
||||||
|
print("%-24s %7s %7s %9s %7s" % ("Variante", "A min", "A max", "A>0.5", "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)
|
||||||
|
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 "
|
||||||
|
"waere Erz" % amt)
|
||||||
|
if not (anteile[0] < anteile[1] < anteile[2]):
|
||||||
|
fails.append("Erz-Anteil wirkt nicht monoton: %s"
|
||||||
|
% ", ".join("%.0f%%" % (100 * x) for x in anteile))
|
||||||
|
|
||||||
|
# --- 3) Smooth & dicht landet im Tri-Fenster 1500-3000 -------------------
|
||||||
|
bpy.ops.rockgen.dense_preset()
|
||||||
|
tris_liste = []
|
||||||
|
for seed in (1, 14, 27):
|
||||||
|
_, _, tris = einer(seed=seed, bake_ore=False)
|
||||||
|
tris_liste.append(tris)
|
||||||
|
schnitt = sum(tris_liste) // len(tris_liste)
|
||||||
|
print("")
|
||||||
|
print("Smooth & dicht: %s Tris (Schnitt %d)"
|
||||||
|
% ("/".join(str(t) for t in tris_liste), schnitt))
|
||||||
|
if not (1500 <= schnitt <= 3000):
|
||||||
|
fails.append("Smooth & dicht liefert %d Tris, Zielfenster 1500-3000" % schnitt)
|
||||||
|
if s.shading_mode != 'AUTO':
|
||||||
|
fails.append("Smooth & dicht setzt kein Auto-Smooth (shading_mode=%s)"
|
||||||
|
% s.shading_mode)
|
||||||
|
|
||||||
|
# --- 4) Das Set: Namen, Groessen, Einstellungen wieder wie vorher ---------
|
||||||
|
for o in list(bpy.data.objects):
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
s.batch_count = 2
|
||||||
|
s.subsurf_levels = 4 # absichtlich duenn: das Set muss selbst dicht stellen
|
||||||
|
s.name_prefix = "Fels"
|
||||||
|
s.name_form = "A"
|
||||||
|
s.name_class = 'M'
|
||||||
|
s.bake_ore = False
|
||||||
|
merker = (s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels)
|
||||||
|
|
||||||
|
OUT = tempfile.mkdtemp()
|
||||||
|
s.export_dir = OUT
|
||||||
|
bpy.ops.rockgen.ore_set()
|
||||||
|
|
||||||
|
namen = sorted(o.name for o in bpy.context.scene.objects if o.type == 'MESH')
|
||||||
|
erwartet = ["Rock_Ore_M_01", "Rock_Ore_M_02", "Rock_Ore_S_01", "Rock_Ore_S_02"]
|
||||||
|
print("")
|
||||||
|
print("Set: %s" % ", ".join(namen))
|
||||||
|
if namen != erwartet:
|
||||||
|
fails.append("Set-Namen %s, erwartet %s" % (namen, erwartet))
|
||||||
|
|
||||||
|
hoehen = {}
|
||||||
|
for ob in bpy.context.scene.objects:
|
||||||
|
if ob.type != 'MESH':
|
||||||
|
continue
|
||||||
|
co = [ob.matrix_world @ v.co for v in ob.data.vertices]
|
||||||
|
hoehen[ob.name] = max(c.z for c in co) - min(c.z for c in co)
|
||||||
|
if len(ob.data.vertices) < 800:
|
||||||
|
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)))
|
||||||
|
|
||||||
|
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)
|
||||||
|
print("Groessen: S bis %.2f m, M ab %.2f m" % (s_hoch, m_hoch))
|
||||||
|
if s_hoch >= m_hoch:
|
||||||
|
fails.append("S (%.2f m) ist nicht kleiner als M (%.2f m) - die Brocken "
|
||||||
|
"muessen in Hoehlengaenge passen" % (s_hoch, m_hoch))
|
||||||
|
|
||||||
|
if (s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels) != merker:
|
||||||
|
fails.append("Das Set hat die Einstellungen nicht zurueckgesetzt: %s"
|
||||||
|
% str((s.name_prefix, s.name_form, s.name_class, s.bake_ore,
|
||||||
|
s.subsurf_levels)))
|
||||||
|
|
||||||
|
# --- 5) A ueberlebt den FBX-Roundtrip ------------------------------------
|
||||||
|
bpy.ops.object.export_rocks_fbx()
|
||||||
|
dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx"))
|
||||||
|
if not dateien:
|
||||||
|
fails.append("Erz-Set: kein FBX exportiert")
|
||||||
|
else:
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, dateien[0]))
|
||||||
|
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)
|
||||||
|
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:
|
||||||
|
fails.append("Erz-Maske ueberlebt den FBX-Roundtrip nicht "
|
||||||
|
"(A %.2f..%.2f)" % (min(a), max(a)))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
if fails:
|
||||||
|
print("ERGEBNIS: %d FEHLER" % len(fails))
|
||||||
|
for x in fails:
|
||||||
|
print(" - " + x)
|
||||||
|
sys.exit(1)
|
||||||
|
print("ERGEBNIS: ALLE CHECKS OK")
|
||||||
Reference in New Issue
Block a user