diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 1d0df9f..9b7a147 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.1.0" +version = "1.2.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 6171214..bbb73f9 100644 --- a/dist/index.json +++ b/dist/index.json @@ -32,7 +32,7 @@ "id": "stylized_tree_generator", "name": "Stylized Tree Generator", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", - "version": "1.1.0", + "version": "1.2.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -48,9 +48,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.1.0.zip", - "archive_size": 9492, - "archive_hash": "sha256:e563fd7c056b20c3d20c779713b5891baf7843f5158e4276629b8886f1efbd9f" + "archive_url": "./stylized_tree_generator-1.2.0.zip", + "archive_size": 10862, + "archive_hash": "sha256:8297d75cf7b8d73189471b480f65e98861ee73840f0ab387fb9f2141fa80a083" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.1.0.zip b/dist/stylized_tree_generator-1.1.0.zip deleted file mode 100644 index f6eda9d..0000000 Binary files a/dist/stylized_tree_generator-1.1.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.2.0.zip b/dist/stylized_tree_generator-1.2.0.zip new file mode 100644 index 0000000..1947b97 Binary files /dev/null and b/dist/stylized_tree_generator-1.2.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 000d0c5..8b81ffe 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, 1, 0), + "version": (1, 2, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -39,12 +39,13 @@ DEFAULTS = { "Branch Bend": 0.45, # Variation je Ast (0 = gerade Staebe) "Branch Droop": 0.0, # >0 haengt nach unten (Palme), <0 kruemmt nach oben (Kaktus) "Branch Thickness": 0.32, # Ast-Dicke relativ zum Stamm - "Branch Taper": 1.0, # 1 = laeuft spitz zu, 0.2 = bleibt dick (Kaktus) + "Branch Taper": 0.85, # wie der Stamm: laeuft NICHT auf 0 -> keine Nadeln "Sub Count": 3, # Sub-Aeste je Hauptast (0 = aus) "Sub Length": 0.55, "Sub Up": 1.4, "Sides": 6, - "Tip Blunt": 0.0, # 0 = laeuft spitz aus (Baum), 0.45 = stumpfes Ende (Kaktus) + "UV Scale": 1.0, # Rinden-Dichte; UVs sind world-space (m) + "Tip Blunt": 0.18, # >0 verhindert Nadelspitzen (0 = spitz, 0.45 = Kaktus) "Ribs": 0.0, # senkrechte Kanneluren (0 = glatt); Kaktus ~9 "Rib Depth": 0.0, } @@ -148,6 +149,7 @@ def build_group(): add_in("Sub Up", 'NodeSocketFloat', DEFAULTS["Sub Up"], -3.0, 3.0) add_in("Sides", 'NodeSocketInt', DEFAULTS["Sides"], 3, 32) add_in("Tip Blunt", 'NodeSocketFloat', DEFAULTS["Tip Blunt"], 0.0, 0.9) + add_in("UV Scale", 'NodeSocketFloat', DEFAULTS["UV Scale"], 0.01, 20.0) add_in("Ribs", 'NodeSocketFloat', DEFAULTS["Ribs"], 0.0, 20.0) add_in("Rib Depth", 'NodeSocketFloat', DEFAULTS["Rib Depth"], 0.0, 0.5) @@ -405,15 +407,22 @@ def build_group(): _sock(sres, "Count").default_value = 6 L(_out(sline, "Curve"), _sock(sres, "Curve")) + # Sub-Aeste: gleiche Verjuengungs-Logik wie Hauptaeste (Branch Taper + Kuppel). + # Vorher lief der Radius hier hart auf 0 -> die Sub-Aeste wurden zu Nadeln. sspar = N("GeometryNodeSplineParameter"); sspar.location = (2250, 900) + stap = N("ShaderNodeMath"); stap.location = (2330, 1000); stap.operation = 'MULTIPLY' + L(V["Branch Taper"], stap.inputs[0]); L(sspar.outputs["Factor"], stap.inputs[1]) sinv = N("ShaderNodeMath"); sinv.location = (2400, 900) sinv.operation = 'SUBTRACT'; sinv.inputs[0].default_value = 1.0 - L(sspar.outputs["Factor"], sinv.inputs[1]) + L(stap.outputs[0], sinv.inputs[1]) srad = N("ShaderNodeMath"); srad.location = (2550, 900); srad.operation = 'MULTIPLY' L(V["Trunk Radius"], srad.inputs[0]); L(sinv.outputs[0], srad.inputs[1]) - srad2 = N("ShaderNodeMath"); srad2.location = (2700, 900); srad2.operation = 'MULTIPLY' - srad2.inputs[1].default_value = 0.14 # deutlich duenner als Hauptaeste - L(srad.outputs[0], srad2.inputs[0]) + srad1 = N("ShaderNodeMath"); srad1.location = (2700, 900); srad1.operation = 'MULTIPLY' + srad1.inputs[1].default_value = 0.24 # duenner als Hauptaeste + L(srad.outputs[0], srad1.inputs[0]) + sdome = _dome(sspar, 2550, 1200) + srad2 = N("ShaderNodeMath"); srad2.location = (3050, 900); srad2.operation = 'MULTIPLY' + L(srad1.outputs[0], srad2.inputs[0]); L(sdome.outputs[0], srad2.inputs[1]) ssetr = N("GeometryNodeSetCurveRadius"); ssetr.location = (2550, 760) L(_out(sres, "Curve"), _sock(ssetr, "Curve")) L(srad2.outputs[0], _sock(ssetr, "Radius")) @@ -478,9 +487,34 @@ def build_group(): L(_out(circ, "Curve"), _sock(pset, "Geometry")) L(poff.outputs[0], _sock(pset, "Offset")) + # ---------- UVs (Curve to Mesh erzeugt KEINE - gemessen!) ---------- + # V = echte Bogenlaenge entlang Stamm/Ast, U = Bogenlaenge um das Profil mal + # Radius => world-space UVs: Rinde sitzt auf dickem Stamm und duennem Zweig + # gleich dicht. Attribute VOR Curve to Mesh ablegen, danach kombinieren. + cspar = N("GeometryNodeSplineParameter"); cspar.location = (2250, 60) + stv = N("GeometryNodeStoreNamedAttribute"); stv.location = (2400, 200) + stv.domain = 'POINT'; stv.data_type = 'FLOAT' + L(_out(join, "Geometry"), _sock(stv, "Geometry")) + _sock(stv, "Name").default_value = "uv_v" + L(cspar.outputs["Length"], _sock(stv, "Value")) + + crad = N("GeometryNodeInputRadius"); crad.location = (2250, -80) + stw = N("GeometryNodeStoreNamedAttribute"); stw.location = (2550, 200) + stw.domain = 'POINT'; stw.data_type = 'FLOAT' + L(_out(stv, "Geometry"), _sock(stw, "Geometry")) + _sock(stw, "Name").default_value = "uv_r" + L(crad.outputs[0], _sock(stw, "Value")) + + pspar2 = N("GeometryNodeSplineParameter"); pspar2.location = (2250, -700) + stu = N("GeometryNodeStoreNamedAttribute"); stu.location = (2400, -100) + stu.domain = 'POINT'; stu.data_type = 'FLOAT' + L(_out(pset, "Geometry"), _sock(stu, "Geometry")) + _sock(stu, "Name").default_value = "uv_u" + L(pspar2.outputs["Length"], _sock(stu, "Value")) + c2m = N("GeometryNodeCurveToMesh"); c2m.location = (1500, 200) - L(_out(join, "Geometry"), _sock(c2m, "Curve")) - L(_out(pset, "Geometry"), _sock(c2m, "Profile Curve")) + L(_out(stw, "Geometry"), _sock(c2m, "Curve")) + L(_out(stu, "Geometry"), _sock(c2m, "Profile Curve")) # Falle 7 (Blender 5.x!): "Curve to Mesh" wertet das Radius-Attribut NICHT # mehr implizit aus, sondern hat einen eigenen "Scale"-Eingang. Ohne diese # Verbindung bleibt der Stamm immer bei Profil-Radius 1.0 (= 2 m dick), @@ -493,8 +527,31 @@ def build_group(): except KeyError: pass + # UVMap aus den drei Attributen zusammensetzen (FACE_CORNER, sonst kein UV) + na_u = N("GeometryNodeInputNamedAttribute"); na_u.location = (2700, -300) + na_u.data_type = 'FLOAT'; _sock(na_u, "Name").default_value = "uv_u" + na_v = N("GeometryNodeInputNamedAttribute"); na_v.location = (2700, -420) + na_v.data_type = 'FLOAT'; _sock(na_v, "Name").default_value = "uv_v" + na_r = N("GeometryNodeInputNamedAttribute"); na_r.location = (2700, -540) + na_r.data_type = 'FLOAT'; _sock(na_r, "Name").default_value = "uv_r" + + umul = N("ShaderNodeMath"); umul.location = (2880, -300); umul.operation = 'MULTIPLY' + L(_out(na_u, "Attribute"), umul.inputs[0]); L(_out(na_r, "Attribute"), umul.inputs[1]) + us = N("ShaderNodeMath"); us.location = (3030, -300); us.operation = 'MULTIPLY' + L(umul.outputs[0], us.inputs[0]); L(V["UV Scale"], us.inputs[1]) + vs = N("ShaderNodeMath"); vs.location = (3030, -420); vs.operation = 'MULTIPLY' + L(_out(na_v, "Attribute"), vs.inputs[0]); L(V["UV Scale"], vs.inputs[1]) + uvvec = N("ShaderNodeCombineXYZ"); uvvec.location = (3180, -360) + L(us.outputs[0], uvvec.inputs["X"]); L(vs.outputs[0], uvvec.inputs["Y"]) + + stuv = N("GeometryNodeStoreNamedAttribute"); stuv.location = (1650, 60) + stuv.domain = 'CORNER'; stuv.data_type = 'FLOAT2' + L(_out(c2m, "Mesh", "Geometry"), _sock(stuv, "Geometry")) + _sock(stuv, "Name").default_value = "UVMap" + L(uvvec.outputs[0], _sock(stuv, "Value")) + shade = N("GeometryNodeSetShadeSmooth"); shade.location = (1650, 200) - L(_out(c2m, "Mesh", "Geometry"), _sock(shade, "Geometry")) + L(_out(stuv, "Geometry"), _sock(shade, "Geometry")) L(_out(shade, "Geometry"), gout.inputs[0]) return ng @@ -593,12 +650,43 @@ class TreeGenSettings(PropertyGroup): 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)"), + ) def _place(ob, x): ob.location.x = x +def _add_lightmap_uv(context, ob, op): + """UV1 fuer Unreal-Lightmaps. Nur auf echten Meshes moeglich (nach Apply).""" + if not ob.data.polygons: + return + lm = ob.data.uv_layers.get("Lightmap") or ob.data.uv_layers.new(name="Lightmap") + # WICHTIG: aktiven Kanal setzen, sonst ueberschreibt lightmap_pack UV0. + ob.data.uv_layers.active = lm + for o in context.view_layer.objects: + try: + o.select_set(False) + except (ReferenceError, RuntimeError): + pass + ob.select_set(True) + context.view_layer.objects.active = ob + try: + bpy.ops.uv.lightmap_pack(PREF_CONTEXT='ALL_FACES', PREF_PACK_IN_ONE=True, + PREF_NEW_UVLAYER=False, PREF_BOX_DIV=12, + PREF_MARGIN_DIV=0.2) + except RuntimeError as exc: + op.report({'WARNING'}, "Lightmap-UV: %s" % exc) + # UV0 wieder als Kanal 0 / Render-UV (Unreal nutzt die Reihenfolge) + if ob.data.uv_layers: + ob.data.uv_layers.active_index = 0 + ob.data.uv_layers[0].active_render = True + + class TREEGEN_OT_create(Operator): bl_idname = "object.treegen_create" bl_label = "Baum erzeugen" @@ -643,6 +731,10 @@ class TREEGEN_OT_create(Operator): bpy.ops.object.modifier_apply(modifier="GN_Tree") except RuntimeError as exc: self.report({'WARNING'}, "Modifier-Apply: %s" % exc) + continue + # UV0 heisst nach dem Apply "UVMap" (aus den Geometry Nodes). + if s.generate_lightmap_uv: + _add_lightmap_uv(context, ob, self) context.view_layer.update() total = 0 @@ -683,6 +775,9 @@ class VIEW3D_PT_tree_generator(Panel): box = layout.box() box.label(text="Export") box.prop(s, "apply_modifier") + row = box.row() + row.enabled = s.apply_modifier + row.prop(s, "generate_lightmap_uv") layout.separator() layout.operator("object.treegen_create", icon='OUTLINER_OB_MESH')