diff --git a/README.md b/README.md index 467ae08..2708624 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,11 @@ Gruppe. Blaetter/Krone macht man selbst — das Addon liefert die Struktur. im Viewport ziehen, der Baum aktualisiert sich sofort. - **Export:** „Modifier anwenden" friert das Ergebnis als normales Mesh ein. +**v1.1.0:** Aeste folgen jetzt der Elternkurve (Tangent/Normal aus *Curve to +Points*, Goldener Winkel = Phyllotaxis) statt einer aus Weltkoordinaten +gerechneten Richtung — Sub-Aeste standen vorher wie Nadeln quer zum Ast. +Neu: `Tip Blunt` (Baum laeuft spitz aus, Kaktus endet stumpf). + ⚠️ **Bekannte Grenze:** Das `kaktus`-Preset ist noch nicht ueberzeugend — die Arme spreizen im V, statt nach dem Abgang senkrecht hochzulaufen (die Droop-Naeherung kann nur einen gleichmaessigen Bogen, keinen Ellbogen). Als Platzhalter brauchbar. diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index cf5e935..1d0df9f 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.0.0" +version = "1.1.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 1f3679c..6171214 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.0.0", + "version": "1.1.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -48,9 +48,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.0.0.zip", - "archive_size": 9292, - "archive_hash": "sha256:23693ed0476102b52d76e67d1d711ce6c55925b47c9323a06d535e51a2508c01" + "archive_url": "./stylized_tree_generator-1.1.0.zip", + "archive_size": 9492, + "archive_hash": "sha256:e563fd7c056b20c3d20c779713b5891baf7843f5158e4276629b8886f1efbd9f" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.0.0.zip b/dist/stylized_tree_generator-1.0.0.zip deleted file mode 100644 index 527ac12..0000000 Binary files a/dist/stylized_tree_generator-1.0.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.1.0.zip b/dist/stylized_tree_generator-1.1.0.zip new file mode 100644 index 0000000..f6eda9d Binary files /dev/null and b/dist/stylized_tree_generator-1.1.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 226775a..000d0c5 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, 0, 0), + "version": (1, 1, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -44,6 +44,7 @@ DEFAULTS = { "Sub Length": 0.55, "Sub Up": 1.4, "Sides": 6, + "Tip Blunt": 0.0, # 0 = laeuft spitz aus (Baum), 0.45 = stumpfes Ende (Kaktus) "Ribs": 0.0, # senkrechte Kanneluren (0 = glatt); Kaktus ~9 "Rib Depth": 0.0, } @@ -77,6 +78,7 @@ PRESETS = { "Branch Thickness": 0.72, # Arme fast so dick wie der Stamm "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, }, } @@ -145,6 +147,7 @@ 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("Tip Blunt", 'NodeSocketFloat', DEFAULTS["Tip Blunt"], 0.0, 0.9) add_in("Ribs", 'NodeSocketFloat', DEFAULTS["Ribs"], 0.0, 20.0) add_in("Rib Depth", 'NodeSocketFloat', DEFAULTS["Rib Depth"], 0.0, 0.5) @@ -222,8 +225,8 @@ def build_group(): # egal wie fein resampled wird. Mit Mindestradius + Fill Caps endet # der Arm stumpf/gerundet statt als Dorn. mx = N("ShaderNodeMath"); mx.location = (x + 450, y); mx.operation = 'MAXIMUM' - mx.inputs[1].default_value = 0.45 L(rt.outputs[0], mx.inputs[0]) + L(V["Tip Blunt"], mx.inputs[1]) return mx tdome = _dome(spar, -900, 220) @@ -285,19 +288,33 @@ def build_group(): L(_out(bres, "Curve"), _sock(bsetr, "Curve")) L(bscl.outputs[0], _sock(bsetr, "Radius")) + # ---------- Ausrichtung relativ zur ELTERNKURVE ---------- + # Curve to Points liefert Tangent + Normal des Ansatzpunktes gratis mit. + # Die Normale um die Tangente zu drehen (Index * Goldener Winkel) verteilt + # die Aeste spiralig um den Elternast (Phyllotaxis) UND folgt automatisch + # dessen Biegung. Vorher wurde die Richtung aus Weltkoordinaten gerechnet -> + # Aeste standen wie angeklebte Nadeln quer zum Ast. + def _child_dir(points_node, up_socket, x, y): + i = N("GeometryNodeInputIndex"); i.location = (x, y + 220) + a = N("ShaderNodeMath"); a.location = (x + 150, y + 220); a.operation = 'MULTIPLY' + a.inputs[1].default_value = 2.399963 # Goldener Winkel + L(i.outputs[0], a.inputs[0]) + vr = N("ShaderNodeVectorRotate"); vr.location = (x + 320, y + 120) + vr.rotation_type = 'AXIS_ANGLE' + L(_out(points_node, "Normal"), vr.inputs["Vector"]) + L(_out(points_node, "Tangent"), vr.inputs["Axis"]) + L(a.outputs[0], vr.inputs["Angle"]) + lean = N("ShaderNodeVectorMath"); lean.location = (x + 320, y - 60) + lean.operation = 'SCALE' + L(_out(points_node, "Tangent"), lean.inputs[0]) + L(up_socket, _sock(lean, "Scale")) + add = N("ShaderNodeVectorMath"); add.location = (x + 500, y + 40) + add.operation = 'ADD' + L(vr.outputs[0], add.inputs[0]); L(lean.outputs[0], add.inputs[1]) + return add + # ---------- radiale Ausrichtung je Ast ---------- - idx = N("GeometryNodeInputIndex"); idx.location = (100, 560) - frac = N("ShaderNodeMath"); frac.location = (260, 560); frac.operation = 'DIVIDE' - L(idx.outputs[0], frac.inputs[0]); L(V["Branch Count"], frac.inputs[1]) - ang = N("ShaderNodeMath"); ang.location = (420, 560); ang.operation = 'MULTIPLY' - ang.inputs[1].default_value = 6.283185 - L(frac.outputs[0], ang.inputs[0]) - cs = N("ShaderNodeMath"); cs.location = (580, 620); cs.operation = 'COSINE' - sn = N("ShaderNodeMath"); sn.location = (580, 500); sn.operation = 'SINE' - L(ang.outputs[0], cs.inputs[0]); L(ang.outputs[0], sn.inputs[0]) - dirv = N("ShaderNodeCombineXYZ"); dirv.location = (740, 560) - L(cs.outputs[0], dirv.inputs["X"]); L(sn.outputs[0], dirv.inputs["Y"]) - L(V["Branch Up"], dirv.inputs["Z"]) + dirv = _child_dir(c2p, V["Branch Up"], 100, 560) align = _new_any("FunctionNodeAlignRotationToVector", "FunctionNodeAlignEulerToVector") if align is not None: @@ -402,16 +419,7 @@ def build_group(): L(srad2.outputs[0], _sock(ssetr, "Radius")) # Richtung: radial gestreut (Index) + "Sub Up" - sidx = N("GeometryNodeInputIndex"); sidx.location = (2100, 1060) - sfr = N("ShaderNodeMath"); sfr.location = (2250, 1060); sfr.operation = 'MULTIPLY' - sfr.inputs[1].default_value = 2.399963 # Goldener Winkel -> gute Streuung - L(sidx.outputs[0], sfr.inputs[0]) - scs = N("ShaderNodeMath"); scs.location = (2400, 1120); scs.operation = 'COSINE' - ssn = N("ShaderNodeMath"); ssn.location = (2400, 1000); ssn.operation = 'SINE' - L(sfr.outputs[0], scs.inputs[0]); L(sfr.outputs[0], ssn.inputs[0]) - sdir = N("ShaderNodeCombineXYZ"); sdir.location = (2550, 1060) - L(scs.outputs[0], sdir.inputs["X"]); L(ssn.outputs[0], sdir.inputs["Y"]) - L(V["Sub Up"], sdir.inputs["Z"]) + sdir = _child_dir(sp, V["Sub Up"], 2100, 1060) salign = _new_any("FunctionNodeAlignRotationToVector", "FunctionNodeAlignEulerToVector") if salign is not None: