diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 8d72878..04f4798 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.7.0" +version = "1.8.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 ecb43ac..2d79fbf 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.7.0", + "version": "1.8.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -48,9 +48,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.7.0.zip", - "archive_size": 13398, - "archive_hash": "sha256:3817ce7433ef63918ddf75a827591b3bb712c8659141ae85f73f4f1fd19e20fb" + "archive_url": "./stylized_tree_generator-1.8.0.zip", + "archive_size": 13440, + "archive_hash": "sha256:e96e6ec6f8009708ce0611dea6ea50e3271b6603d60bce84ebeab2e365bba983" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.7.0.zip b/dist/stylized_tree_generator-1.7.0.zip deleted file mode 100644 index 197f40d..0000000 Binary files a/dist/stylized_tree_generator-1.7.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.8.0.zip b/dist/stylized_tree_generator-1.8.0.zip new file mode 100644 index 0000000..9090f97 Binary files /dev/null and b/dist/stylized_tree_generator-1.8.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index a913bbf..fce99f8 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, 7, 0), + "version": (1, 8, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -43,7 +43,9 @@ DEFAULTS = { "Sub Count": 2, # Sub-Aeste je Hauptast (0 = aus) "Sub Length": 0.55, "Sub Up": 1.4, - "Sides": 5, + "Sides": 8, # Stamm rund (Referenz TreeIt: ~9) + "Branch Sides": 4, # Aeste sparsamer + "Sub Sides": 3, # Zweige nur Dreiecke - sieht man nicht "UV Scale": 1.0, # Rinden-Dichte; UVs sind world-space (m) "Tip Blunt": 0.18, # >0 verhindert Nadelspitzen (0 = spitz, 0.45 = Kaktus) "Root Flare": 1.1, # Wurzelanlauf: Stamm verbreitert sich am Fuss @@ -86,7 +88,7 @@ PRESETS = { "Branch Taper": 0.12, # Arme bleiben dick statt spitz zuzulaufen "Sub Count": 0, "Tip Blunt": 0.45, - "Sides": 18, "Ribs": 9.0, "Rib Depth": 0.09, + "Sides": 16, "Branch Sides": 12, "Ribs": 9.0, "Rib Depth": 0.09, }, } # =========================================================================== @@ -166,6 +168,8 @@ def build_group(): add_in("Sub Length", 'NodeSocketFloat', DEFAULTS["Sub Length"], 0.05, 5.0) add_in("Sub Up", 'NodeSocketFloat', DEFAULTS["Sub Up"], -3.0, 3.0) add_in("Sides", 'NodeSocketInt', DEFAULTS["Sides"], 3, 32) + add_in("Branch Sides", 'NodeSocketInt', DEFAULTS["Branch Sides"], 3, 16) + add_in("Sub Sides", 'NodeSocketInt', DEFAULTS["Sub Sides"], 3, 12) 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("Detail", 'NodeSocketFloat', DEFAULTS["Detail"], 0.2, 2.0) @@ -573,108 +577,102 @@ def build_group(): L(_out(sreal, "Geometry"), _sock(sbset, "Geometry")) L(sboff.outputs[0], _sock(sbset, "Offset")) - join = N("GeometryNodeJoinGeometry"); join.location = (3150, 200) - L(_out(sbset, "Geometry"), join.inputs[0]) - L(_out(rsetp, "Geometry"), join.inputs[0]) - L(_out(setrad, "Curve"), join.inputs[0]) + # ---------- Curve -> Mesh, PRO EBENE mit eigener Profilaufloesung ---------- + # Referenz (TreeIt): Stamm ~9 Segmente, duenne Zweige nur Dreiecke. Ein + # "Curve to Mesh" kennt aber nur EIN Profil -> deshalb wird jede Ebene + # einzeln gewandelt und erst danach zusammengefuegt. Spart massiv Tris, + # weil die vielen duennen Zweige nicht die Rundung des Stammes brauchen. + def _profile(sides_socket, x, y): + c = N("GeometryNodeCurvePrimitiveCircle"); c.location = (x, y) + L(sides_socket, _sock(c, "Resolution")) + _sock(c, "Radius").default_value = 1.0 + # Rippen (Kaktus-Kanneluren): radiale Welle auf dem Profil + sp = N("GeometryNodeSplineParameter"); sp.location = (x, y - 200) + ang = N("ShaderNodeMath"); ang.location = (x + 150, y - 200); ang.operation = 'MULTIPLY' + ang.inputs[1].default_value = 6.283185 + L(sp.outputs["Factor"], ang.inputs[0]) + rb = N("ShaderNodeMath"); rb.location = (x + 300, y - 200); rb.operation = 'MULTIPLY' + L(ang.outputs[0], rb.inputs[0]); L(V["Ribs"], rb.inputs[1]) + co = N("ShaderNodeMath"); co.location = (x + 450, y - 200); co.operation = 'COSINE' + L(rb.outputs[0], co.inputs[0]) + am = N("ShaderNodeMath"); am.location = (x + 600, y - 200); am.operation = 'MULTIPLY' + L(co.outputs[0], am.inputs[0]); L(V["Rib Depth"], am.inputs[1]) + po = N("GeometryNodeInputPosition"); po.location = (x + 300, y - 340) + dn = N("ShaderNodeVectorMath"); dn.location = (x + 450, y - 340); dn.operation = 'NORMALIZE' + L(po.outputs[0], dn.inputs[0]) + of = N("ShaderNodeVectorMath"); of.location = (x + 750, y - 300); of.operation = 'SCALE' + L(dn.outputs[0], of.inputs[0]); L(am.outputs[0], _sock(of, "Scale")) + st = N("GeometryNodeSetPosition"); st.location = (x + 900, y) + L(_out(c, "Curve"), _sock(st, "Geometry")); L(of.outputs[0], _sock(st, "Offset")) + # uv_u = Bogenlaenge um das Profil + sp2 = N("GeometryNodeSplineParameter"); sp2.location = (x + 900, y - 200) + su = N("GeometryNodeStoreNamedAttribute"); su.location = (x + 1050, y) + su.domain = 'POINT'; su.data_type = 'FLOAT' + L(_out(st, "Geometry"), _sock(su, "Geometry")) + _sock(su, "Name").default_value = "uv_u" + L(sp2.outputs["Length"], _sock(su, "Value")) + return su - # ---------- Curve -> Mesh ---------- - # Falle 6: Der Profil-Radius wird mit dem Curve-Radius MULTIPLIZIERT. - # Profil deshalb auf 1.0 lassen waere richtig -- aber nur, wenn der - # Curve-Radius bereits die echte Staerke ist. Hier ist er das, also 1.0. - circ = N("GeometryNodeCurvePrimitiveCircle"); circ.location = (1350, -100) - L(V["Sides"], _sock(circ, "Resolution")) - _sock(circ, "Radius").default_value = 1.0 + def _to_mesh(curve_node, curve_out_name, sides_socket, x, y): + """Kurven -> Mesh inkl. world-space UVs. Pro Ast-Ebene einmal.""" + cs = N("GeometryNodeSplineParameter"); cs.location = (x, y - 140) + sv = N("GeometryNodeStoreNamedAttribute"); sv.location = (x + 150, y) + sv.domain = 'POINT'; sv.data_type = 'FLOAT' + L(_out(curve_node, curve_out_name), _sock(sv, "Geometry")) + _sock(sv, "Name").default_value = "uv_v" + L(cs.outputs["Length"], _sock(sv, "Value")) + rd = N("GeometryNodeInputRadius"); rd.location = (x, y - 280) + sw = N("GeometryNodeStoreNamedAttribute"); sw.location = (x + 300, y) + sw.domain = 'POINT'; sw.data_type = 'FLOAT' + L(_out(sv, "Geometry"), _sock(sw, "Geometry")) + _sock(sw, "Name").default_value = "uv_r" + L(rd.outputs[0], _sock(sw, "Value")) - # ---------- Rippen (senkrechte Kanneluren, Kaktus-Signatur) ---------- - # Das Profil bekommt eine radiale Welle: Offset entlang der Punktrichtung, - # moduliert mit cos(Rippen * Winkel). Rib Depth = 0 -> glatter Kreis. - pspar = N("GeometryNodeSplineParameter"); pspar.location = (1350, -420) - pang = N("ShaderNodeMath"); pang.location = (1500, -420); pang.operation = 'MULTIPLY' - pang.inputs[1].default_value = 6.283185 - L(pspar.outputs["Factor"], pang.inputs[0]) - pribs = N("ShaderNodeMath"); pribs.location = (1650, -420); pribs.operation = 'MULTIPLY' - L(pang.outputs[0], pribs.inputs[0]) - L(V["Ribs"], pribs.inputs[1]) - pcos = N("ShaderNodeMath"); pcos.location = (1800, -420); pcos.operation = 'COSINE' - L(pribs.outputs[0], pcos.inputs[0]) - pamp = N("ShaderNodeMath"); pamp.location = (1950, -420); pamp.operation = 'MULTIPLY' - L(pcos.outputs[0], pamp.inputs[0]) - L(V["Rib Depth"], pamp.inputs[1]) - ppos = N("GeometryNodeInputPosition"); ppos.location = (1650, -560) - pdir = N("ShaderNodeVectorMath"); pdir.location = (1800, -560) - pdir.operation = 'NORMALIZE' - L(ppos.outputs[0], pdir.inputs[0]) - poff = N("ShaderNodeVectorMath"); poff.location = (2100, -520) - poff.operation = 'SCALE' - L(pdir.outputs[0], poff.inputs[0]) - L(pamp.outputs[0], _sock(poff, "Scale")) - pset = N("GeometryNodeSetPosition"); pset.location = (2250, -100) - L(_out(circ, "Curve"), _sock(pset, "Geometry")) - L(poff.outputs[0], _sock(pset, "Offset")) + prof = _profile(sides_socket, x - 1100, y - 500) - # ---------- 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")) + cm = N("GeometryNodeCurveToMesh"); cm.location = (x + 500, y) + L(_out(sw, "Geometry"), _sock(cm, "Curve")) + L(_out(prof, "Geometry"), _sock(cm, "Profile Curve")) + # Falle 7 (Blender 5.x): Radius-Attribut wird NICHT mehr implizit + # ausgewertet -> muss an den "Scale"-Eingang. + if "Scale" in cm.inputs: + ra = N("GeometryNodeInputRadius"); ra.location = (x + 350, y - 420) + L(ra.outputs[0], _sock(cm, "Scale")) + try: + _sock(cm, "Fill Caps").default_value = True + except KeyError: + pass - 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")) + nu = N("GeometryNodeInputNamedAttribute"); nu.location = (x + 500, y - 200) + nu.data_type = 'FLOAT'; _sock(nu, "Name").default_value = "uv_u" + nv = N("GeometryNodeInputNamedAttribute"); nv.location = (x + 500, y - 320) + nv.data_type = 'FLOAT'; _sock(nv, "Name").default_value = "uv_v" + nr = N("GeometryNodeInputNamedAttribute"); nr.location = (x + 500, y - 440) + nr.data_type = 'FLOAT'; _sock(nr, "Name").default_value = "uv_r" + um = N("ShaderNodeMath"); um.location = (x + 650, y - 200); um.operation = 'MULTIPLY' + L(_out(nu, "Attribute"), um.inputs[0]); L(_out(nr, "Attribute"), um.inputs[1]) + us_ = N("ShaderNodeMath"); us_.location = (x + 800, y - 200); us_.operation = 'MULTIPLY' + L(um.outputs[0], us_.inputs[0]); L(V["UV Scale"], us_.inputs[1]) + vs_ = N("ShaderNodeMath"); vs_.location = (x + 800, y - 320); vs_.operation = 'MULTIPLY' + L(_out(nv, "Attribute"), vs_.inputs[0]); L(V["UV Scale"], vs_.inputs[1]) + uv = N("ShaderNodeCombineXYZ"); uv.location = (x + 950, y - 260) + L(us_.outputs[0], uv.inputs["X"]); L(vs_.outputs[0], uv.inputs["Y"]) + sm = N("GeometryNodeStoreNamedAttribute"); sm.location = (x + 1100, y) + sm.domain = 'CORNER'; sm.data_type = 'FLOAT2' + L(_out(cm, "Mesh", "Geometry"), _sock(sm, "Geometry")) + _sock(sm, "Name").default_value = "UVMap" + L(uv.outputs[0], _sock(sm, "Value")) + return sm - 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")) + mesh_trunk = _to_mesh(setrad, "Curve", V["Sides"], 3400, 900) + mesh_branch = _to_mesh(rsetp, "Geometry", V["Branch Sides"], 3400, 200) + mesh_sub = _to_mesh(sbset, "Geometry", V["Sub Sides"], 3400, -500) - c2m = N("GeometryNodeCurveToMesh"); c2m.location = (1500, 200) - 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), - # egal was "Trunk Radius" sagt. Gemessen: Stammbreite konstant 1.995. - if "Scale" in c2m.inputs: - radattr = N("GeometryNodeInputRadius"); radattr.location = (1350, -260) - L(radattr.outputs[0], _sock(c2m, "Scale")) - try: - _sock(c2m, "Fill Caps").default_value = True - 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")) + join = N("GeometryNodeJoinGeometry"); join.location = (4700, 300) + L(_out(mesh_sub, "Geometry"), join.inputs[0]) + L(_out(mesh_branch, "Geometry"), join.inputs[0]) + L(_out(mesh_trunk, "Geometry"), join.inputs[0]) + stuv = join # Name beibehalten fuer den nachfolgenden Merge-Zweig # ---------- optional: Aeste mit dem Stamm VERSCHMELZEN ---------- # Voxel-Remesh (Mesh to Volume -> Volume to Mesh) macht aus den sich