Tree Generator v1.1.0: Aeste folgen der Elternkurve

Kern aus EcoGame tools/blender_tree_gen.py regeneriert. Ast-Richtung jetzt ueber
Tangent/Normal von 'Curve to Points' (Goldener Winkel) statt Weltkoordinaten ->
Sub-Aeste wachsen entlang ihres Elternastes statt quer dazu. 'Tip Blunt' neu.

Headless getestet: alle 4 Presets, Stufen aufsteigend, Modifier-Apply ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 10:33:09 +02:00
co-authored by Claude Opus 4.8
parent 9240cc6d80
commit f9e3fe4223
6 changed files with 42 additions and 29 deletions
+5
View File
@@ -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.
+1 -1
View File
@@ -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"
+4 -4
View File
@@ -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"
}
]
}
Binary file not shown.
Binary file not shown.
+32 -24
View File
@@ -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: