diff --git a/README.md b/README.md index 1213083..0d7d09a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,37 @@ --- +## EcoGame-Konventionen (beide Addons) + +Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung: + +**Benennung** +- Felsen: `Fels_
_` — Form = Buchstabe (A, B, …), Klasse = `S/M/L/XL`. + Die Klasse meint eine eigene **Silhouette**, nicht blosse Groesse: rein + groesser/kleiner macht die Engine ueber ScaleMin/Max. +- Baeume: `__S`, z. B. `Birke_01_S1` … `Birke_01_S4`. + Zusatz-Ebenen haengen hinten an: `_Leaf`, `_Frucht`. +- Texturen: `_albedo` und `_normal` — mehr nicht. + +**Textur-Bake** +- Nur **Albedo + Normal**. Roughness ist im Master-Material eine **Zahl**, + keine Textur. +- **AO/Cavity wird ins Albedo multipliziert** (Option „AO ins Albedo einrechnen", + Staerke regelbar). Dunkle Ritzen und helle Platten stecken im Farbbild — das + traegt den Stylized-Look. Separate Rough-/AO-Paesse sind Default **aus**. +- 1024 px reicht fuer stylized Formen. +- Tileable (4D-Torus) passt zum triplanar mappenden Master-Material. + +**Mesh** +- Echte Meter, Ursprung unten-mittig, FBX pro Asset bzw. LOD-Gruppe. +- **UV1 Lightmap ist Default aus** — EcoGame beleuchtet dynamisch. +- Meshes brauchen **keine eigenen Materialien**; in UE bekommt alles die + Material-Instanz vom Master. Ein Platzhalter im FBX ist okay. +- Scale-Randomisierung nur fuer die **Form** (nicht-uniform) nutzen, nicht fuer + die Gesamtgroesse — sonst multipliziert sie sich mit ScaleMin/Max der Engine. + +--- + ## Stylized Tree Generator Parametrischer **Geaest**-Generator (Stamm + Aeste + Sub-Aeste) als Geometry-Nodes- diff --git a/blender_manifest.toml b/blender_manifest.toml index d40f40f..d78c793 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.3.0" +version = "2.4.0" name = "Stylized Rock Generator" tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export" maintainer = "D4rkst3r" diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index cdb0e8e..6e64508 100644 --- a/blender_manifest_tree.toml +++ b/blender_manifest_tree.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_tree_generator" -version = "1.17.0" +version = "1.18.0" name = "Stylized Tree Generator" tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen" maintainer = "D4rkst3r" diff --git a/dist/index.json b/dist/index.json index 2596408..52c751c 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.3.0", + "version": "2.4.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -23,16 +23,16 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.3.0.zip", - "archive_size": 14694, - "archive_hash": "sha256:a6e15dd94c6b77dc1ef7c631d1d1dce275a8d3550d16369d90675a2f42fa6298" + "archive_url": "./stylized_rock_generator-2.4.0.zip", + "archive_size": 16036, + "archive_hash": "sha256:e7163f6f0c53731f897b8a1ba63db1cdab94aa182dd720ca1a5653a2ca3df9ec" }, { "schema_version": "1.0.0", "id": "stylized_tree_generator", "name": "Stylized Tree Generator", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", - "version": "1.17.0", + "version": "1.18.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -48,9 +48,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.17.0.zip", - "archive_size": 16523, - "archive_hash": "sha256:49c3e777aa602ba3c63537aacd866bb5b11ac6484dbc0f91b28d26f497f98c00" + "archive_url": "./stylized_tree_generator-1.18.0.zip", + "archive_size": 16777, + "archive_hash": "sha256:9ea5b7a9c01f3c1b382436d25539ab9ec41ffb8b051d26c17c82783c594680a0" } ] } \ No newline at end of file diff --git a/dist/stylized_rock_generator-2.3.0.zip b/dist/stylized_rock_generator-2.3.0.zip deleted file mode 100644 index 2de9afc..0000000 Binary files a/dist/stylized_rock_generator-2.3.0.zip and /dev/null differ diff --git a/dist/stylized_rock_generator-2.4.0.zip b/dist/stylized_rock_generator-2.4.0.zip new file mode 100644 index 0000000..264d821 Binary files /dev/null and b/dist/stylized_rock_generator-2.4.0.zip differ diff --git a/dist/stylized_tree_generator-1.17.0.zip b/dist/stylized_tree_generator-1.17.0.zip deleted file mode 100644 index 5a9b8b7..0000000 Binary files a/dist/stylized_tree_generator-1.17.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.18.0.zip b/dist/stylized_tree_generator-1.18.0.zip new file mode 100644 index 0000000..f8b8bd1 Binary files /dev/null and b/dist/stylized_tree_generator-1.18.0.zip differ diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index 89225be..a72060e 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, 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__, 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__) --- + 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 _albedo.png und _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') diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 55835b2..9353768 100644 --- a/stylized_tree_generator.py +++ b/stylized_tree_generator.py @@ -1,7 +1,7 @@ bl_info = { "name": "Stylized Tree Generator", "author": "D4rkst3r", - "version": (1, 17, 0), + "version": (1, 18, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -964,7 +964,8 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None): # --------------------------------------------------------------------------- from bpy.props import ( - IntProperty, FloatProperty, EnumProperty, BoolProperty, PointerProperty, + IntProperty, FloatProperty, EnumProperty, BoolProperty, StringProperty, + PointerProperty, ) from bpy.types import Operator, Panel, PropertyGroup @@ -981,18 +982,32 @@ class TreeGenSettings(PropertyGroup): description="Erzeugt mehrere Stufen (Setzling .. ausgewachsen) mit gleichem Seed", ) stages: IntProperty(name="Anzahl Stufen", default=4, min=2, max=8) + art_name: StringProperty( + name="Art", default="", + description=("Art-Name fuer den Export, z.B. 'Birke'. Leer = Preset-Name. " + "Schema: __S"), + ) + variant: IntProperty( + name="Variante", default=1, min=1, max=99, + description="Laufende Variante derselben Art (Birke_01, Birke_02 ...)", + ) spacing: FloatProperty(name="Abstand", default=4.0, min=0.0, max=20.0) apply_modifier: BoolProperty( name="Modifier anwenden", default=False, description="Ergebnis als normales Mesh einfrieren (fuer den Export)", ) generate_lightmap_uv: BoolProperty( - name="UV1 Lightmap-UV", default=True, - description=("Zweiter, nicht ueberlappender UV-Kanal fuer Unreal-Lightmaps. " - "Braucht 'Modifier anwenden' (nur echte Meshes lassen sich unwrappen)"), + name="UV1 Lightmap-UV", default=False, + description=("Zweiter, nicht ueberlappender UV-Kanal fuer Lightmaps. Braucht " + "'Modifier anwenden'. EcoGame beleuchtet dynamisch -> nicht noetig"), ) +def _art(s): + """Art-Name fuer das EcoGame-Schema __S.""" + return (s.art_name.strip() or s.preset).capitalize() + + def _place(ob, x): ob.location.x = x @@ -1049,12 +1064,12 @@ class TREEGEN_OT_create(Operator): for i in range(s.stages): t = i / float(s.stages - 1) if s.stages > 1 else 1.0 ob = make_object(ng, preset=s.preset, growth=t, seed=s.seed, - name="%s_%s_stage%d" % (OBJ_NAME, s.preset, i)) + name="%s_%02d_S%d" % (_art(s), s.variant, i + 1)) _place(ob, x); x += s.spacing created.append(ob) else: ob = make_object(ng, preset=s.preset, seed=s.seed, - name="%s_%s" % (OBJ_NAME, s.preset)) + name="%s_%02d" % (_art(s), s.variant)) created.append(ob) except Exception as exc: # noqa: BLE001 self.report({'ERROR'}, "Erzeugen fehlgeschlagen: %s" % exc) @@ -1099,6 +1114,12 @@ class VIEW3D_PT_tree_generator(Panel): box.label(text="Vorlage", icon='PRESET') box.prop(s, "preset", text="") box.prop(s, "seed") + row = box.row(align=True) + row.prop(s, "art_name", text="") + row.prop(s, "variant") + box.label(text="Name: %s_%02d%s" % (_art(s), s.variant, + "_S1..S%d" % s.stages if s.use_growth else ""), + icon='INFO') box = layout.box() box.label(text="Wachstum")