Beide Addons auf die EcoGame-Konventionen abgestimmt

Rock Generator 2.4.0:
- Benennung Fels_<Form>_<Klasse> statt Rock_000 (Praefix/Form/Klasse im Panel,
  Klasse S/M/L/XL, optionale Durchnummerierung bei Batches)
- Bake liefert jetzt <name>_albedo und <name>_normal. AO/Cavity wird per Option
  MULTIPLIKATIV ins Albedo gebacken (Staerke regelbar) - separate Roughness- und
  AO-Paesse sind Default aus, weil Roughness in EcoGame eine Zahl im Master-
  Material ist. Gemessen: Albedo-Mittelwert 0.549 -> 0.433 mit AO, Kontrast rauf.
- UV1 Lightmap Default AUS (EcoGame beleuchtet dynamisch)

Tree Generator 1.18.0:
- Benennung <Art>_<Variante>_S<Stufe>, z.B. Birke_01_S1..S4 (statt stage0..3);
  Art-Feld und Varianten-Nummer im Panel, Art leer = Preset-Name
- UV1 Lightmap Default AUS

README: Konventionen dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:25:09 +02:00
co-authored by Claude Opus 4.8
parent ca13e9f573
commit f5bbd1bc35
10 changed files with 182 additions and 41 deletions
+113 -24
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Rock Generator",
"author": "D4rkst3r",
"version": (2, 3, 0),
"version": (2, 4, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Rock Gen",
"description": (
@@ -102,6 +102,16 @@ def _apply_modifier(context, obj, mod_name):
raise RockGenError("Modifier '%s' Apply wurde abgebrochen." % mod_name)
def _asset_name(settings, index, batch_count):
"""EcoGame-Schema: Fels_<Form>_<Klasse>, bei Batches mit laufender Nummer."""
base = "%s_%s_%s" % (settings.name_prefix.strip() or "Fels",
(settings.name_form.strip() or "A").upper(),
settings.name_class)
if settings.name_numbered and batch_count > 1:
return "%s_%02d" % (base, index + 1)
return base
def _face_count(obj):
return len(obj.data.polygons)
@@ -279,7 +289,7 @@ def _make_lightmap_uv(context, obj):
# Mesh-Aufbau
# ---------------------------------------------------------------------------
def _build_rock_mesh(context, settings, index, location, rng):
def _build_rock_mesh(context, settings, index, location, rng, name=None):
"""Baut das reine Rock-Mesh (ohne Transform/Material). Kann RockGenError werfen."""
# 1. Basis-Mesh
if settings.base_shape == 'CUBE':
@@ -289,7 +299,7 @@ def _build_rock_mesh(context, settings, index, location, rng):
subdivisions=1, radius=1.0, location=location
)
obj = context.active_object
obj.name = "Rock_%03d" % index
obj.name = name or ("Rock_%03d" % index)
# Effektive Form-Parameter (optional pro Rock variiert)
eff_noise = settings.noise_scale
@@ -504,10 +514,10 @@ def _parent_keep_transform(child, parent):
child.matrix_parent_inverse = parent.matrix_world.inverted()
def _build_lod_group(context, settings, index, location, rng):
base_name = "Rock_%03d" % index
def _build_lod_group(context, settings, index, location, rng, base_name=None):
base_name = base_name or ("Rock_%03d" % index)
lod0 = _build_rock_mesh(context, settings, index, location, rng)
lod0 = _build_rock_mesh(context, settings, index, location, rng, name=base_name)
_apply_hardsurface(context, lod0, settings)
_apply_random_transform(context, lod0, settings, rng)
if settings.assign_material:
@@ -556,15 +566,16 @@ def _build_lod_group(context, settings, index, location, rng):
# Ein Rock (bzw. eine LOD-Gruppe) erzeugen
# ---------------------------------------------------------------------------
def make_one_rock(context, settings, index):
def make_one_rock(context, settings, index, batch_count=1):
"""Erzeugt genau einen Rock / eine LOD-Gruppe. Liefert Liste der Objekte."""
rng = random.Random(settings.seed + index * 9973)
location = (index * settings.spacing, 0.0, 0.0)
name = _asset_name(settings, index, batch_count)
if settings.generate_lods:
return _build_lod_group(context, settings, index, location, rng)
return _build_lod_group(context, settings, index, location, rng, base_name=name)
obj = _build_rock_mesh(context, settings, index, location, rng)
obj = _build_rock_mesh(context, settings, index, location, rng, name=name)
_apply_hardsurface(context, obj, settings)
_apply_random_transform(context, obj, settings, rng)
_apply_shading(context, obj, settings)
@@ -645,8 +656,9 @@ class RockGenSettings(PropertyGroup):
description="Fuer Material-Maker-Texturen (Albedo/Normal/Roughness/AO)",
)
generate_lightmap_uv: BoolProperty(
name="UV1 Lightmap-UV", default=True,
description="Separates, nicht ueberlappendes UV fuer Unreal Static-Mesh-Lightmaps",
name="UV1 Lightmap-UV", default=False,
description=("Separates, nicht ueberlappendes UV fuer Static-Mesh-Lightmaps. "
"EcoGame beleuchtet dynamisch - dort nicht noetig"),
)
# --- Hard-Surface (optional) ---
@@ -736,19 +748,57 @@ class RockGenSettings(PropertyGroup):
name="Bake-Ordner", default="", subtype='DIR_PATH',
description="Zielordner fuer die gebackenen Textur-PNGs",
)
# --- Benennung (EcoGame-Schema: Fels_<Form>_<Klasse>) ---
name_prefix: StringProperty(
name="Praefix", default="Fels",
description="Basisname, z.B. 'Fels' oder 'Felsblock'",
)
name_form: StringProperty(
name="Form", default="A",
description="Form-Variante als Buchstabe (A, B, C ...)",
)
name_class: EnumProperty(
name="Groessen-Klasse",
items=[('S', "S", "klein"), ('M', "M", "mittel"),
('L', "L", "gross"), ('XL', "XL", "sehr gross")],
default='M',
description=("Groessen-KLASSE mit eigener Geometrie. Reines Groesser/Kleiner "
"macht die Engine ueber ScaleMin/Max - eine eigene Klasse nur, "
"wenn sich die Silhouette aendert"),
)
name_numbered: BoolProperty(
name="Bei Batch durchnummerieren", default=True,
description="Haengt bei mehreren Rocks _01, _02 ... an (sonst Blender-Suffix)",
)
bake_prefix: StringProperty(
name="Textur-Prefix", default="T_Rock_Stylized",
description="Dateiname-Prefix (UE-Konvention T_)",
name="Textur-Name", default="fels_a",
description="Basisname; es entstehen <name>_albedo.png und <name>_normal.png",
)
bake_resolution: EnumProperty(
name="Aufloesung",
items=[('512', "512", ""), ('1024', "1024", ""), ('2048', "2048", "")],
default='1024',
default='1024', # reicht fuer stylized/flaechige Formen
)
bake_color: BoolProperty(name="BaseColor", default=True)
bake_color: BoolProperty(name="Albedo", default=True)
bake_normal: BoolProperty(name="Normal", default=True)
bake_roughness: BoolProperty(name="Roughness", default=True)
bake_ao: BoolProperty(name="AO / Cavity", default=True)
bake_roughness: BoolProperty(
name="Roughness", default=False,
description="In EcoGame ist Roughness eine ZAHL im Master-Material, keine Textur",
)
bake_ao: BoolProperty(
name="AO separat", default=False,
description=("Eigene AO-Textur. In EcoGame nicht noetig - dort wird AO ins "
"Albedo eingerechnet (siehe unten)"),
)
bake_ao_into_albedo: BoolProperty(
name="AO ins Albedo einrechnen", default=True,
description="Cavity/AO multiplikativ ins Farbbild backen (EcoGame-Konvention)",
)
bake_ao_strength: FloatProperty(
name="AO-Staerke", default=1.0, min=0.0, max=1.0,
description="0 = kein AO im Albedo, 1 = voll",
)
bake_base_color: FloatVectorProperty(
name="Grundfarbe", subtype='COLOR', size=4,
default=(0.52, 0.47, 0.40, 1.0), min=0.0, max=1.0,
@@ -796,7 +846,7 @@ class OBJECT_OT_generate_rocks(Operator):
for i in range(settings.batch_count):
names_before = set(bpy.data.objects.keys())
try:
created.extend(make_one_rock(context, settings, i))
created.extend(make_one_rock(context, settings, i, settings.batch_count))
except Exception as exc: # noqa: BLE001 - bewusst breit, ein Rock darf nicht den Batch killen
failed.append((i, str(exc)))
print("[RockGen] Rock %d fehlgeschlagen: %s" % (i, exc))
@@ -839,7 +889,7 @@ class OBJECT_OT_generate_rocks(Operator):
names_before = set(bpy.data.objects.keys())
try:
self._created.extend(make_one_rock(context, settings, self._index))
self._created.extend(make_one_rock(context, settings, self._index, settings.batch_count))
except Exception as exc: # noqa: BLE001
self._failed.append((self._index, str(exc)))
print("[RockGen] Rock %d fehlgeschlagen: %s" % (self._index, exc))
@@ -873,7 +923,9 @@ def _collect_export_targets(context, settings):
tops = []
for o in context.scene.objects:
is_group = (o.type == 'EMPTY' and o.get("fbx_type") == "LodGroup")
is_standalone = (o.type == 'MESH' and o.name.startswith("Rock_") and o.parent is None)
is_standalone = (o.type == 'MESH' and o.parent is None
and (o.name.startswith(settings.name_prefix.strip() or "Fels")
or o.name.startswith("Rock_")))
if is_group or is_standalone:
tops.append(o)
if settings.export_selected_only:
@@ -1073,6 +1125,19 @@ def _build_bake_material(base_color=(0.52, 0.47, 0.40), cell_scale=2.5):
bump.inputs["Distance"].default_value = 0.4
links.new(cell_fac.outputs[0], bump.inputs["Height"])
# EcoGame-Konvention: AO/Cavity wird NICHT als eigene Textur exportiert,
# sondern direkt ins Albedo multipliziert - dunkle Ritzen und helle Platten
# sind im Farbbild eingebacken. Das traegt den Stylized-Look.
ao_mix = nodes.new("ShaderNodeMixRGB")
ao_mix.blend_type = 'MULTIPLY'
ao_mix.inputs["Fac"].default_value = 1.0
links.new(ramp.outputs["Color"], ao_mix.inputs["Color1"])
ao_rgb = nodes.new("ShaderNodeCombineXYZ")
links.new(ao.outputs[0], ao_rgb.inputs["X"])
links.new(ao.outputs[0], ao_rgb.inputs["Y"])
links.new(ao.outputs[0], ao_rgb.inputs["Z"])
links.new(ao_rgb.outputs[0], ao_mix.inputs["Color2"])
links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
links.new(rough.outputs[0], bsdf.inputs["Roughness"])
links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
@@ -1081,6 +1146,8 @@ def _build_bake_material(base_color=(0.52, 0.47, 0.40), cell_scale=2.5):
sockets = {
"out": out, "bsdf": bsdf, "emit": emit,
"color": ramp.outputs["Color"],
"color_ao": ao_mix.outputs["Color"], # Albedo mit eingebackenem AO
"ao_mix": ao_mix,
"roughness": rough.outputs[0],
"ao": ao.outputs[0],
}
@@ -1139,6 +1206,10 @@ class OBJECT_OT_bake_rock_textures(Operator):
base_color=tuple(settings.bake_base_color),
cell_scale=settings.bake_cell_scale,
)
# AO-Staerke ins Albedo (Fac 0 = aus, 1 = voll)
sk["ao_mix"].inputs["Fac"].default_value = (
settings.bake_ao_strength if settings.bake_ao_into_albedo else 0.0
)
plane.data.materials.clear()
plane.data.materials.append(mat)
@@ -1157,7 +1228,7 @@ class OBJECT_OT_bake_rock_textures(Operator):
if settings.bake_normal:
links.new(sk["bsdf"].outputs[0], sk["out"].inputs["Surface"])
saved.append(_bake_one(context, mat, img_node, prefix + "_N",
saved.append(_bake_one(context, mat, img_node, prefix + "_normal",
folder, res, 'NORMAL', non_color=True))
def _emit_pass(source_socket, suffix, non_color):
@@ -1167,11 +1238,14 @@ class OBJECT_OT_bake_rock_textures(Operator):
folder, res, 'EMIT', non_color))
if settings.bake_color:
_emit_pass(sk["color"], "_BC", non_color=False)
# EcoGame braucht nur _albedo und _normal. Das AO steckt bereits
# im Farbbild, sofern "AO ins Albedo einrechnen" aktiv ist.
src = sk["color_ao"] if settings.bake_ao_into_albedo else sk["color"]
_emit_pass(src, "_albedo", non_color=False)
if settings.bake_roughness:
_emit_pass(sk["roughness"], "_R", non_color=True)
_emit_pass(sk["roughness"], "_roughness", non_color=True)
if settings.bake_ao:
_emit_pass(sk["ao"], "_AO", non_color=True)
_emit_pass(sk["ao"], "_ao", non_color=True)
except Exception as exc: # noqa: BLE001
print("[RockGen] Bake fehlgeschlagen: %s" % exc)
@@ -1300,6 +1374,16 @@ class VIEW3D_PT_rock_generator(Panel):
row.prop(settings, "new_preset_name", text="")
row.operator("rockgen.preset_save", text="Speichern", icon='ADD')
box = layout.box()
box.label(text="Benennung", icon='SORTALPHA')
row = box.row(align=True)
row.prop(settings, "name_prefix", text="")
row.prop(settings, "name_form", text="")
row.prop(settings, "name_class", text="")
box.prop(settings, "name_numbered")
box.label(text="Ergebnis: %s" % _asset_name(settings, 0, settings.batch_count),
icon='INFO')
box = layout.box()
box.label(text="Basis")
box.prop(settings, "base_shape")
@@ -1397,6 +1481,11 @@ class VIEW3D_PT_rock_generator(Panel):
row = box.row(align=True)
row.prop(settings, "bake_color", toggle=True)
row.prop(settings, "bake_normal", toggle=True)
box.prop(settings, "bake_ao_into_albedo")
sub = box.row()
sub.enabled = settings.bake_ao_into_albedo
sub.prop(settings, "bake_ao_strength")
row = box.row(align=True)
row.prop(settings, "bake_roughness", toggle=True)
row.prop(settings, "bake_ao", toggle=True)
box.operator("object.bake_rock_textures", icon='TEXTURE')