Rock Gen 2.18.0: Material-Slots, Praefix Rock, Warnung bei zu grobem Erz-Mesh
Drei Rueckmeldungen von der Spielseite abgearbeitet. 1) Fehlende Material-Slots "Material zuweisen" stand auf Standard AUS, deshalb kamen alle Proben mit mats: [] an - Unreal legt dann nur einen Default-Slot an und die eingefrorene Konvention M_Rock_<Sorte> greift nicht. Jetzt Standard AN. Normale Steine rotieren durch M_Rock_Granite/Basalt/Sandstone/Mossy, das Erzfels-Set bekommt einen NEUTRALEN Slot M_Rock_Ore - ein Set fuer alle Erzsorten, die Sorte macht die Material-Instanz. 2) Namens-Drift Fels_ statt Rock_ War ein Versehen, keine Absicht: der Standard-Praefix stammte noch aus der ersten Fassung, bevor die Konvention Rock_<Set>_<Groesse>_<NN> stand. Der Erz-Set-Knopf setzte "Rock" schon selbst, der allgemeine Standard nicht. Standard ist jetzt "Rock" - keine Asset-Zwillinge, keine Migration noetig. 3) Erzfelsen mit ~870 statt ~2700 Tris Nachgestellt: aus den Werkseinstellungen liefert der Set-Knopf 2590-2690 Tris, die Automatik greift also. ~870 entstehen genau dann, wenn man den Erz-Haken setzt und den NORMALEN Erzeugen-Knopf nimmt - Werkseinstellung ist Decimate 0.20/0.50 statt 0.40/0.80. Das ist eine Bedienluecke, kein Fehler in der Automatik: die Dichte wird bewusst nur ueber den Set-Knopf gesetzt, damit niemandem seine Einstellungen unterm Hintern weggezogen werden. Deshalb warnt der Operator jetzt, wenn mit Erz-Haken ein Mesh unter 800 Verts entsteht, und nennt den Knopf "Smooth & dicht" beim Namen. Sonst faellt das erst in UE auf und sieht dort nach einem Material-Fehler aus. Tests halten den Vertrag jetzt fest: Auslieferungszustand (assign_material an, Praefix Rock), neutraler Erz-Slot, Sorten-Slots bei normalen Steinen. 17 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,8 @@ sonst findet Blender die `bl_parent_id` nicht.
|
||||
Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
|
||||
|
||||
**Benennung**
|
||||
- Felsen: `Fels_<Form>_<Klasse>` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`.
|
||||
- Felsen: `Rock_<Form>_<Klasse>` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`.
|
||||
Das Praefix ist **`Rock`** (Standard) — die eingefrorene EcoGame-Konvention.
|
||||
Die Klasse setzt eine **Basis-Groesse** (S=0.45, M=1.0, L=1.9, XL=3.2) und ist
|
||||
damit ein bewusst anderes Asset — gemessen: S 0.85 m, M 1.9 m, L 3.5 m, XL 6.0 m.
|
||||
Davon strikt getrennt ist die **Zufalls-Streuung** (`scale_min/max`): die variiert
|
||||
@@ -108,6 +109,14 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
|
||||
- 1024 px reicht fuer stylized Formen.
|
||||
- Tileable (4D-Torus) passt zum triplanar mappenden Master-Material.
|
||||
|
||||
**Material-Slots (Teil des Vertrags, keine Kosmetik)**
|
||||
- Die UE-Skripte mappen ueber den **Slot-Namen**. Ohne Slot legt Unreal nur
|
||||
einen Default an und die Konvention greift nicht mehr — deshalb ist
|
||||
`Material zuweisen` **Standard an**.
|
||||
- Normale Steine rotieren durch `M_Rock_Granite/Basalt/Sandstone/Mossy`.
|
||||
- Das Erzfels-Set bekommt **einen neutralen Slot `M_Rock_Ore`** — ein Set fuer
|
||||
alle Erzsorten, welche Sorte es ist macht die Material-Instanz.
|
||||
|
||||
**Mesh**
|
||||
- Echte Meter, Ursprung unten-mittig, FBX pro Asset bzw. LOD-Gruppe.
|
||||
- **UV1 Lightmap ist Default aus** — EcoGame beleuchtet dynamisch.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "stylized_rock_generator"
|
||||
version = "2.17.0"
|
||||
version = "2.18.0"
|
||||
name = "Stylized Rock Generator"
|
||||
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
|
||||
maintainer = "D4rkst3r"
|
||||
|
||||
Vendored
+3
-3
@@ -40,14 +40,14 @@
|
||||
<td>4.9KB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><tt><a href="./stylized_rock_generator-2.17.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.17.0</a></tt></td>
|
||||
<td><tt><a href="./stylized_rock_generator-2.18.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_rock_generator-2.18.0</a></tt></td>
|
||||
<td>Stylized Rock Generator</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>4.2.0 - ~</td>
|
||||
<td>all</td>
|
||||
<td>all</td>
|
||||
<td>31.5KB</td>
|
||||
<td>32.2KB</td>
|
||||
</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>
|
||||
@@ -61,6 +61,6 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<center><p>Built 2026-08-03, 06:38</p></center>
|
||||
<center><p>Built 2026-08-03, 12:08</p></center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Vendored
+4
-4
@@ -57,7 +57,7 @@
|
||||
"id": "stylized_rock_generator",
|
||||
"name": "Stylized Rock Generator",
|
||||
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
|
||||
"version": "2.17.0",
|
||||
"version": "2.18.0",
|
||||
"type": "add-on",
|
||||
"maintainer": "D4rkst3r",
|
||||
"license": [
|
||||
@@ -73,9 +73,9 @@
|
||||
"Mesh",
|
||||
"Modeling"
|
||||
],
|
||||
"archive_url": "./stylized_rock_generator-2.17.0.zip",
|
||||
"archive_size": 32287,
|
||||
"archive_hash": "sha256:5d6a1616822ac74059ca82a07c3b42976efb37b211b523cafdbb1864de3118f6"
|
||||
"archive_url": "./stylized_rock_generator-2.18.0.zip",
|
||||
"archive_size": 33014,
|
||||
"archive_hash": "sha256:c690c130cff1bfd69f91c4d9685ba9fcb56b017cfc1b1211cbd2df91d15ba001"
|
||||
},
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+54
-12
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "Stylized Rock Generator",
|
||||
"author": "D4rkst3r",
|
||||
"version": (2, 17, 0),
|
||||
"version": (2, 18, 0),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "View3D > Sidebar > Rock Gen",
|
||||
"description": (
|
||||
@@ -35,6 +35,15 @@ from mathutils import Matrix, Vector
|
||||
TWO_PI = math.pi * 2.0
|
||||
|
||||
# Platzhalter-Materialien: (Name, Base-Color RGBA). Werden pro Rock rotiert.
|
||||
# Neutraler Slot fuer das Erzfels-Set: EIN Set fuer alle Erzsorten, welche
|
||||
# Sorte es ist (Kupfer/Eisen/Gold) macht die Material-Instanz in UE.
|
||||
ORE_MATERIAL = ("M_Rock_Ore", (0.30, 0.28, 0.26, 1.0))
|
||||
|
||||
# Unterhalb dieser Vertex-Zahl verschmieren die Erz-Nuggets zu Schlieren - die
|
||||
# Maske haengt an der Vertex-Dichte. Gemessen: bei ~150 Verts ist jedes dritte
|
||||
# ein Nugget-Zentrum, bei ~1300 liest es sich als einzelne Einschluesse.
|
||||
ORE_MIN_VERTS = 800
|
||||
|
||||
MATERIAL_PALETTE = (
|
||||
("M_Rock_Granite", (0.32, 0.30, 0.28, 1.0)),
|
||||
("M_Rock_Basalt", (0.11, 0.11, 0.13, 1.0)),
|
||||
@@ -251,8 +260,14 @@ def _get_or_create_material(name, color):
|
||||
return mat
|
||||
|
||||
|
||||
def _assign_material(obj, index):
|
||||
name, color = MATERIAL_PALETTE[index % len(MATERIAL_PALETTE)]
|
||||
def _assign_material(obj, index, fixed=None):
|
||||
"""Setzt den Material-Slot. Die UE-Skripte mappen ueber den SLOT-NAMEN,
|
||||
deshalb ist die Benennung Teil des Vertrags und kein Kosmetik-Thema.
|
||||
|
||||
`fixed` erzwingt einen bestimmten Slot - das Erzfels-Set nutzt das fuer
|
||||
seinen neutralen `M_Rock_Ore`, statt durch die Sorten zu rotieren.
|
||||
"""
|
||||
name, color = fixed if fixed is not None else MATERIAL_PALETTE[index % len(MATERIAL_PALETTE)]
|
||||
mat = _get_or_create_material(name, color)
|
||||
obj.data.materials.clear()
|
||||
obj.data.materials.append(mat)
|
||||
@@ -781,7 +796,8 @@ def _build_lod_group(context, settings, index, location, rng, base_name=None):
|
||||
_bake_vertex_cavity(context, lod0, settings)
|
||||
|
||||
if settings.assign_material:
|
||||
_assign_material(lod0, index)
|
||||
_assign_material(lod0, index,
|
||||
fixed=ORE_MATERIAL if settings.bake_ore else None)
|
||||
lod0.name = base_name + "_LOD0"
|
||||
lod0.data.name = base_name + "_LOD0"
|
||||
|
||||
@@ -844,7 +860,8 @@ def make_one_rock(context, settings, index, batch_count=1):
|
||||
if settings.set_origin_bottom:
|
||||
_set_origin_bottom(context, obj)
|
||||
if settings.assign_material:
|
||||
_assign_material(obj, index)
|
||||
_assign_material(obj, index,
|
||||
fixed=ORE_MATERIAL if settings.bake_ore else None)
|
||||
return [obj]
|
||||
|
||||
|
||||
@@ -858,8 +875,26 @@ def _cleanup_partial(context, names_before):
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
def _report_result(op, created, failed, cancelled=False):
|
||||
def _report_result(op, created, failed, cancelled=False, settings=None):
|
||||
n_mesh = sum(1 for o in created if getattr(o, "type", None) == 'MESH')
|
||||
|
||||
# Erz-Haken an, aber das Mesh ist zu grob? Dann verschmieren die Nuggets.
|
||||
# Das faellt sonst erst in UE auf - und dort sieht es nach einem
|
||||
# Material-Fehler aus, obwohl es an der Vertex-Dichte liegt.
|
||||
if settings is not None and settings.bake_ore and not failed:
|
||||
duenn = [o for o in created
|
||||
if getattr(o, "type", None) == 'MESH'
|
||||
and len(o.data.vertices) < ORE_MIN_VERTS]
|
||||
if duenn:
|
||||
op.report(
|
||||
{'WARNING'},
|
||||
"%d Rock(s) erzeugt - aber %d davon haben unter %d Verts. Die "
|
||||
"Erz-Nuggets verschmieren dabei zu Schlieren. Knopf "
|
||||
"'Smooth & dicht' druecken und neu erzeugen."
|
||||
% (n_mesh, len(duenn), ORE_MIN_VERTS),
|
||||
)
|
||||
return
|
||||
|
||||
if failed:
|
||||
op.report(
|
||||
{'WARNING'},
|
||||
@@ -1060,8 +1095,11 @@ class RockGenSettings(PropertyGroup):
|
||||
|
||||
# --- Material ---
|
||||
assign_material: BoolProperty(
|
||||
name="Material zuweisen", default=False,
|
||||
description="Weist pro Rock ein Platzhalter-Material zu (rotiert durch eine Liste)",
|
||||
name="Material zuweisen", default=True,
|
||||
description=("Legt den Material-Slot an (M_Rock_<Sorte>, beim Erz-Set "
|
||||
"neutral M_Rock_Ore). Die UE-Skripte mappen ueber den "
|
||||
"Slot-NAMEN - ohne Slot erzeugt Unreal nur einen Default "
|
||||
"und die Konvention greift nicht mehr"),
|
||||
)
|
||||
|
||||
# --- LOD ---
|
||||
@@ -1113,7 +1151,7 @@ class RockGenSettings(PropertyGroup):
|
||||
)
|
||||
# --- Benennung (EcoGame-Schema: Fels_<Form>_<Klasse>) ---
|
||||
name_prefix: StringProperty(
|
||||
name="Praefix", default="Fels",
|
||||
name="Praefix", default="Rock",
|
||||
description="Basisname, z.B. 'Fels' oder 'Felsblock'",
|
||||
)
|
||||
name_form: StringProperty(
|
||||
@@ -1268,7 +1306,7 @@ class OBJECT_OT_generate_rocks(Operator):
|
||||
print("[RockGen] Rock %d fehlgeschlagen: %s" % (i, exc))
|
||||
traceback.print_exc()
|
||||
_cleanup_partial(context, names_before)
|
||||
_report_result(self, created, failed)
|
||||
_report_result(self, created, failed, settings=settings)
|
||||
return {'FINISHED'}
|
||||
|
||||
# --- Einstiegspunkt: entscheidet synchron vs. modal ---
|
||||
@@ -1294,13 +1332,15 @@ class OBJECT_OT_generate_rocks(Operator):
|
||||
|
||||
if event.type == 'ESC':
|
||||
self._finish(context)
|
||||
_report_result(self, self._created, self._failed, cancelled=True)
|
||||
_report_result(self, self._created, self._failed, cancelled=True,
|
||||
settings=context.scene.rock_gen_settings)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if event.type == 'TIMER':
|
||||
if self._index >= settings.batch_count:
|
||||
self._finish(context)
|
||||
_report_result(self, self._created, self._failed)
|
||||
_report_result(self, self._created, self._failed,
|
||||
settings=context.scene.rock_gen_settings)
|
||||
return {'FINISHED'}
|
||||
|
||||
names_before = set(bpy.data.objects.keys())
|
||||
@@ -1800,6 +1840,7 @@ class ROCKGEN_OT_ore_set(Operator):
|
||||
"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,
|
||||
"assign_material": s.assign_material,
|
||||
}
|
||||
vorher.update({k: getattr(s, k) for k in DENSE_PRESET})
|
||||
# Erz MUSS dicht: die Maske haengt an der Vertex-Dichte. Bei ~150 Verts
|
||||
@@ -1813,6 +1854,7 @@ class ROCKGEN_OT_ore_set(Operator):
|
||||
s.name_numbered = True
|
||||
s.bake_ore = True
|
||||
s.bake_vertex_cavity = True # A haengt am selben Attribut
|
||||
s.assign_material = True # Slot M_Rock_Ore - die UE-Skripte mappen darueber
|
||||
s.use_class_size = True # S und M sollen sich in der Groesse unterscheiden
|
||||
|
||||
gesamt = 0
|
||||
|
||||
@@ -11,11 +11,12 @@ for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True)
|
||||
|
||||
# --- Namensschema ---
|
||||
s.batch_count=3; s.subsurf_levels=3; s.use_modal=False; s.generate_lods=False
|
||||
s.name_prefix="Fels"; s.name_form="a"; s.name_class='L'
|
||||
# Eingefrorene EcoGame-Konvention: Rock_<Set>_<Groesse>_<NN>.
|
||||
s.name_prefix="Rock"; s.name_form="a"; s.name_class='L'
|
||||
bpy.ops.object.generate_rocks_batch()
|
||||
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
|
||||
print("NAMEN:", names)
|
||||
if not all(n.startswith("Fels_A_L") for n in names): fails.append("naming")
|
||||
if not all(n.startswith("Rock_A_L") for n in names): fails.append("naming")
|
||||
|
||||
# Einzeln (ohne Nummer)
|
||||
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||
@@ -23,7 +24,7 @@ s.batch_count=1
|
||||
bpy.ops.object.generate_rocks_batch()
|
||||
single=[o.name for o in bpy.context.scene.objects if o.type=='MESH']
|
||||
print("EINZELN:", single)
|
||||
if single and single[0]!="Fels_A_L": fails.append("naming_single")
|
||||
if single and single[0]!="Rock_A_L": fails.append("naming_single")
|
||||
print("LIGHTMAP-DEFAULT aus:", not m.RockGenSettings.bl_rna.properties['generate_lightmap_uv'].default)
|
||||
|
||||
# --- Bake: nur _albedo + _normal, AO drin ---
|
||||
|
||||
+47
-1
@@ -34,6 +34,16 @@ for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
|
||||
s = bpy.context.scene.rock_gen_settings
|
||||
# Auslieferungszustand festhalten, BEVOR der Test etwas verstellt: die
|
||||
# Material-Zuweisung muss standardmaessig AN sein, sonst kommt in UE nur ein
|
||||
# Default-Slot an und die Konvention M_Rock_<Sorte> greift nicht.
|
||||
if not s.assign_material:
|
||||
fails.append("assign_material ist im Auslieferungszustand aus - dann fehlt "
|
||||
"der Material-Slot und die UE-Skripte koennen nicht mappen")
|
||||
if s.name_prefix != "Rock":
|
||||
fails.append("Standard-Praefix ist '%s', die eingefrorene Konvention "
|
||||
"verlangt 'Rock'" % s.name_prefix)
|
||||
|
||||
s.use_modal = False
|
||||
s.assign_material = False
|
||||
s.generate_lods = False
|
||||
@@ -110,7 +120,7 @@ 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_prefix = "Fels" # absichtlich abweichend: das Set muss selbst umstellen
|
||||
s.name_form = "A"
|
||||
s.name_class = 'M'
|
||||
s.bake_ore = False
|
||||
@@ -140,6 +150,12 @@ for ob in bpy.context.scene.objects:
|
||||
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))))
|
||||
# EIN neutraler Slot fuer ALLE Erzsorten. Die UE-Skripte mappen ueber den
|
||||
# Slot-NAMEN - ohne Slot legt Unreal nur einen Default an.
|
||||
slots = [m.name for m in ob.data.materials]
|
||||
if slots != [rg.ORE_MATERIAL[0]]:
|
||||
fails.append("%s: Slots %s, erwartet ['%s']"
|
||||
% (ob.name, slots, rg.ORE_MATERIAL[0]))
|
||||
|
||||
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)
|
||||
@@ -153,7 +169,37 @@ if (s.name_prefix, s.name_form, s.name_class, s.bake_ore, s.subsurf_levels) != m
|
||||
% str((s.name_prefix, s.name_form, s.name_class, s.bake_ore,
|
||||
s.subsurf_levels)))
|
||||
|
||||
# --- 4b) Normale Steine tragen die Sorten-Slots --------------------------
|
||||
for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
s.bake_ore = False
|
||||
s.name_prefix = "Rock"
|
||||
s.batch_count = 4
|
||||
s.assign_material = True
|
||||
bpy.ops.object.generate_rocks_batch()
|
||||
normale = sorted((o for o in bpy.context.scene.objects if o.type == 'MESH'),
|
||||
key=lambda o: o.name)
|
||||
slots_normal = [[m.name for m in o.data.materials] for o in normale]
|
||||
print("Normale Steine: %s" % ", ".join(x[0] if x else "KEINE" for x in slots_normal))
|
||||
for o, sl in zip(normale, slots_normal):
|
||||
if not sl:
|
||||
fails.append("%s hat keinen Material-Slot - Unreal legt dann nur einen "
|
||||
"Default an und die Konvention M_Rock_<Sorte> greift nicht"
|
||||
% o.name)
|
||||
elif not sl[0].startswith("M_Rock_"):
|
||||
fails.append("%s: Slot '%s' folgt nicht M_Rock_<Sorte>" % (o.name, sl[0]))
|
||||
elif sl[0] == rg.ORE_MATERIAL[0]:
|
||||
fails.append("%s: traegt den Erz-Slot, obwohl kein Erz gebacken wurde"
|
||||
% o.name)
|
||||
if not any(n.startswith("Rock_") for n in (o.name for o in normale)):
|
||||
fails.append("Standard-Praefix ist nicht 'Rock' - das bricht die "
|
||||
"eingefrorene Konvention und erzeugt Asset-Zwillinge in UE")
|
||||
|
||||
# --- 5) A ueberlebt den FBX-Roundtrip ------------------------------------
|
||||
for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
s.batch_count = 1
|
||||
bpy.ops.rockgen.ore_set()
|
||||
bpy.ops.object.export_rocks_fbx()
|
||||
dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx"))
|
||||
if not dateien:
|
||||
|
||||
Reference in New Issue
Block a user