Tree Generator v1.22.0: Phototropismus (Attraction Up)

Aeste kruemmen sich zur Spitze hin zur Senkrechten - als echte Rotation um den
Astansatz, nicht als Z-Versatz. Die Astlaenge bleibt dabei erhalten (gemessen:
+0.08 m Hoehe statt +0.93 m beim alten Droop-Hack). Pro Preset abgestimmt.

Idee aus UE 5.8 Procedural Vegetation / Weber-Penn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 09:52:45 +02:00
co-authored by Claude Opus 4.8
parent 2eb0131a0e
commit 1112797138
6 changed files with 67 additions and 10 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "stylized_tree_generator" id = "stylized_tree_generator"
version = "1.21.0" version = "1.22.0"
name = "Stylized Tree Generator" name = "Stylized Tree Generator"
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen" tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
maintainer = "D4rkst3r" maintainer = "D4rkst3r"
+4 -4
View File
@@ -32,7 +32,7 @@
"id": "stylized_tree_generator", "id": "stylized_tree_generator",
"name": "Stylized Tree Generator", "name": "Stylized Tree Generator",
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
"version": "1.21.0", "version": "1.22.0",
"type": "add-on", "type": "add-on",
"maintainer": "D4rkst3r", "maintainer": "D4rkst3r",
"license": [ "license": [
@@ -48,9 +48,9 @@
"Mesh", "Mesh",
"Modeling" "Modeling"
], ],
"archive_url": "./stylized_tree_generator-1.21.0.zip", "archive_url": "./stylized_tree_generator-1.22.0.zip",
"archive_size": 18435, "archive_size": 19331,
"archive_hash": "sha256:eb093c862dc7192728e86ef07d45fc1a9f4381044c2f8560b399f2a85911ec94" "archive_hash": "sha256:5e5e3b5e0b73aabe416f93c27bbef4772b561122762dd52daf836f10c0e45b35"
} }
] ]
} }
Binary file not shown.
Binary file not shown.
+61 -4
View File
@@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "Stylized Tree Generator", "name": "Stylized Tree Generator",
"author": "D4rkst3r", "author": "D4rkst3r",
"version": (1, 21, 0), "version": (1, 22, 0),
"blender": (4, 2, 0), "blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen", "location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -40,6 +40,9 @@ DEFAULTS = {
"Branch Bend": 0.75, # Krummheit je Ast/Zweig (0 = gerade Staebe) "Branch Bend": 0.75, # Krummheit je Ast/Zweig (0 = gerade Staebe)
"Branch Droop": 0.0, # >0 haengt nach unten (Palme), <0 kruemmt nach oben (Kaktus) "Branch Droop": 0.0, # >0 haengt nach unten (Palme), <0 kruemmt nach oben (Kaktus)
"Droop Curve": 2.0, # 2 = weicher Bogen, 4+ = scharfer Ellbogen "Droop Curve": 2.0, # 2 = weicher Bogen, 4+ = scharfer Ellbogen
"Attraction Up": 0.45, # Phototropismus in Radiant: Ast dreht sich zur
# Spitze hin zur Senkrechten. Echte Rotation um
# den Ansatzpunkt -> Astlaenge bleibt erhalten
"Trunk Tip": 0.3, # Anteil von Tip Blunt fuer die Stammspitze; "Trunk Tip": 0.3, # Anteil von Tip Blunt fuer die Stammspitze;
# klein = laeuft duenn aus, 1.0 = stumpfe Kuppe # klein = laeuft duenn aus, 1.0 = stumpfe Kuppe
"Branch Thickness": 0.17, # Ast-Dicke relativ zum Stamm. Am TreeIt-Baum "Branch Thickness": 0.17, # Ast-Dicke relativ zum Stamm. Am TreeIt-Baum
@@ -96,6 +99,7 @@ PRESETS = {
"Branch Start": 0.9, "Branch End": 1.0, "Branch Start": 0.9, "Branch End": 1.0,
"Branch Bend": 0.3, "Branch Bend": 0.3,
"Branch Droop": 0.5, "Droop Curve": 2.2, "Branch Droop": 0.5, "Droop Curve": 2.2,
"Attraction Up": 0.2, # Wedel schwingen leicht hoch, dann Bogen
"Branch Thickness": 0.16, "Branch Thickness": 0.16,
"Branch Taper": 0.45, "Branch Taper": 0.45,
"Root Flare": 0.5, # leichte Verdickung ... "Root Flare": 0.5, # leichte Verdickung ...
@@ -119,6 +123,7 @@ PRESETS = {
"Branch Count": 22, "Branch Length": 1.9, "Branch Up": -0.25, "Branch Count": 22, "Branch Length": 1.9, "Branch Up": -0.25,
"Branch Start": 0.12, "Branch End": 0.97, "Branch Start": 0.12, "Branch End": 0.97,
"Crown Taper": 0.9, "Crown Bulge": 0.0, "Crown Taper": 0.9, "Crown Bulge": 0.0,
"Attraction Up": 0.0, # Tannenaeste haengen, streben nicht hoch
"Branch Bend": 0.3, "Branch Thickness": 0.12, "Sub Count": 1, "Branch Bend": 0.3, "Branch Thickness": 0.12, "Sub Count": 1,
"Sub Length": 0.4, "Root Flare": 0.7, "Sub Length": 0.4, "Root Flare": 0.7,
}, },
@@ -127,6 +132,7 @@ PRESETS = {
"Branch Count": 10, "Branch Length": 2.2, "Branch Up": 0.9, "Branch Count": 10, "Branch Length": 2.2, "Branch Up": 0.9,
"Branch Start": 0.3, "Branch End": 0.95, "Branch Start": 0.3, "Branch End": 0.95,
"Crown Taper": -0.15, "Crown Bulge": 0.5, "Crown Taper": -0.15, "Crown Bulge": 0.5,
"Attraction Up": 0.5, # Eiche: Aeste schwingen deutlich hoch
"Branch Bend": 0.9, "Branch Thickness": 0.24, "Sub Count": 3, "Branch Bend": 0.9, "Branch Thickness": 0.24, "Sub Count": 3,
"Sub Length": 0.8, "Root Flare": 1.5, "Root Lobe Depth": 0.4, "Sub Length": 0.8, "Root Flare": 1.5, "Root Lobe Depth": 0.4,
}, },
@@ -143,6 +149,7 @@ PRESETS = {
"Branch Count": 12, "Branch Length": 2.0, "Branch Up": 0.5, "Branch Count": 12, "Branch Length": 2.0, "Branch Up": 0.5,
"Branch Start": 0.35, "Branch End": 0.95, "Branch Start": 0.35, "Branch End": 0.95,
"Crown Taper": -0.2, "Crown Bulge": 0.4, "Crown Taper": -0.2, "Crown Bulge": 0.4,
"Attraction Up": -0.25, # Weide: Zweige haengen
"Branch Droop": 1.2, "Branch Bend": 0.7, "Branch Thickness": 0.16, "Branch Droop": 1.2, "Branch Bend": 0.7, "Branch Thickness": 0.16,
"Sub Count": 3, "Sub Length": 0.9, "Root Flare": 1.2, "Sub Count": 3, "Sub Length": 0.9, "Root Flare": 1.2,
}, },
@@ -151,6 +158,7 @@ PRESETS = {
"Branch Count": 6, "Branch Length": 1.5, "Branch Up": 0.9, "Branch Count": 6, "Branch Length": 1.5, "Branch Up": 0.9,
"Branch Start": 0.3, "Branch End": 0.9, "Branch Start": 0.3, "Branch End": 0.9,
"Crown Taper": 0.3, "Crown Bulge": 0.0, "Crown Taper": 0.3, "Crown Bulge": 0.0,
"Attraction Up": 0.35,
"Branch Bend": 1.4, "Branch Thickness": 0.18, "Sub Count": 1, "Branch Bend": 1.4, "Branch Thickness": 0.18, "Sub Count": 1,
"Sub Length": 0.5, "Root Flare": 1.4, "Tip Blunt": 0.08, "Sub Length": 0.5, "Root Flare": 1.4, "Tip Blunt": 0.08,
}, },
@@ -161,6 +169,7 @@ PRESETS = {
"Branch Count": 2, "Branch Length": 1.9, "Branch Up": 0.12, "Branch Count": 2, "Branch Length": 1.9, "Branch Up": 0.12,
"Branch Start": 0.30, "Branch End": 0.46, "Branch Start": 0.30, "Branch End": 0.46,
"Branch Bend": 0.0, "Branch Bend": 0.0,
"Attraction Up": 0.0, # Kaktus nutzt den Droop-Ellbogen
"Branch Droop": -1.35, # negativ = Arme kruemmen nach OBEN (Saguaro); "Branch Droop": -1.35, # negativ = Arme kruemmen nach OBEN (Saguaro);
# zu gross und die Arme ueberragen den Stamm # zu gross und die Arme ueberragen den Stamm
"Droop Curve": 4.5, # spaeter, dafuer scharfer Ellbogen "Droop Curve": 4.5, # spaeter, dafuer scharfer Ellbogen
@@ -244,6 +253,7 @@ def build_group():
# (= Kaktus-Arme statt Palmwedel). # (= Kaktus-Arme statt Palmwedel).
add_in("Branch Droop", 'NodeSocketFloat', DEFAULTS["Branch Droop"], -3.0, 3.0) add_in("Branch Droop", 'NodeSocketFloat', DEFAULTS["Branch Droop"], -3.0, 3.0)
add_in("Droop Curve", 'NodeSocketFloat', DEFAULTS["Droop Curve"], 1.0, 8.0) add_in("Droop Curve", 'NodeSocketFloat', DEFAULTS["Droop Curve"], 1.0, 8.0)
add_in("Attraction Up", 'NodeSocketFloat', DEFAULTS["Attraction Up"], -1.5, 1.5)
add_in("Trunk Tip", 'NodeSocketFloat', DEFAULTS["Trunk Tip"], 0.05, 1.0) add_in("Trunk Tip", 'NodeSocketFloat', DEFAULTS["Trunk Tip"], 0.05, 1.0)
add_in("Branch Thickness", 'NodeSocketFloat', DEFAULTS["Branch Thickness"], 0.05, 1.0) add_in("Branch Thickness", 'NodeSocketFloat', DEFAULTS["Branch Thickness"], 0.05, 1.0)
add_in("Branch Taper", 'NodeSocketFloat', DEFAULTS["Branch Taper"], 0.0, 1.0) add_in("Branch Taper", 'NodeSocketFloat', DEFAULTS["Branch Taper"], 0.0, 1.0)
@@ -546,8 +556,19 @@ def build_group():
pfin = N("ShaderNodeMath"); pfin.location = (2100, 120); pfin.operation = 'MULTIPLY' pfin = N("ShaderNodeMath"); pfin.location = (2100, 120); pfin.operation = 'MULTIPLY'
L(pscale.outputs[0], pfin.inputs[0]); L(ccl.outputs[0], pfin.inputs[1]) L(pscale.outputs[0], pfin.inputs[0]); L(ccl.outputs[0], pfin.inputs[1])
# Ansatzpunkt als Attribut mitgeben. Punkt-Attribute ueberleben
# "Instance on Points" + "Realize" (geprueft) - dadurch kennt spaeter JEDER
# Punkt eines Astes seinen eigenen Ursprung. Das ist die Voraussetzung fuer
# echtes Biegen (Rotation) statt blossem Verschieben.
apos = N("GeometryNodeInputPosition"); apos.location = (900, 480)
anch = N("GeometryNodeStoreNamedAttribute"); anch.location = (1050, 320)
anch.domain = 'POINT'; anch.data_type = 'FLOAT_VECTOR'
L(_out(c2p, "Points"), _sock(anch, "Geometry"))
_sock(anch, "Name").default_value = "anchor"
L(apos.outputs[0], _sock(anch, "Value"))
inst = N("GeometryNodeInstanceOnPoints"); inst.location = (1400, 320) inst = N("GeometryNodeInstanceOnPoints"); inst.location = (1400, 320)
L(_out(c2p, "Points"), _sock(inst, "Points")) L(_out(anch, "Geometry"), _sock(inst, "Points"))
L(_out(bsetr, "Curve"), _sock(inst, "Instance")) L(_out(bsetr, "Curve"), _sock(inst, "Instance"))
L(pfin.outputs[0], _sock(inst, "Scale")) L(pfin.outputs[0], _sock(inst, "Scale"))
if align is not None: if align is not None:
@@ -561,6 +582,42 @@ def build_group():
# des jeweiligen Astes. Dieselbe Noise liefert damit pro Ast einen anderen # des jeweiligen Astes. Dieselbe Noise liefert damit pro Ast einen anderen
# Wert — vor dem Realize haetten alle Instanzen identische lokale Coords # Wert — vor dem Realize haetten alle Instanzen identische lokale Coords
# (und wuerden exakt gleich gebogen). # (und wuerden exakt gleich gebogen).
# ---------- Phototropismus (Lichtsuche) ----------
# Idee aus UE 5.8 Procedural Vegetation ("light detection") bzw. Weber-Penn
# ("attraction up"): Aeste kruemmen sich zur Spitze hin zur Senkrechten.
# Umgesetzt als echte ROTATION des Punktes um seinen Astansatz - im
# Gegensatz zum Z-Versatz (Droop) bleibt die Astlaenge dabei erhalten.
ph_spar = N("GeometryNodeSplineParameter"); ph_spar.location = (1200, 780)
ph_pos = N("GeometryNodeInputPosition"); ph_pos.location = (1200, 640)
ph_anch = N("GeometryNodeInputNamedAttribute"); ph_anch.location = (1200, 500)
ph_anch.data_type = 'FLOAT_VECTOR'
_sock(ph_anch, "Name").default_value = "anchor"
ph_rel = N("ShaderNodeVectorMath"); ph_rel.location = (1400, 640)
ph_rel.operation = 'SUBTRACT'
L(ph_pos.outputs[0], ph_rel.inputs[0])
L(_out(ph_anch, "Attribute"), ph_rel.inputs[1])
ph_up = N("ShaderNodeCombineXYZ"); ph_up.location = (1400, 500)
ph_up.inputs["Z"].default_value = 1.0
ph_ax = N("ShaderNodeVectorMath"); ph_ax.location = (1550, 570)
ph_ax.operation = 'CROSS_PRODUCT'
L(ph_rel.outputs[0], ph_ax.inputs[0]); L(ph_up.outputs[0], ph_ax.inputs[1])
ph_ang = N("ShaderNodeMath"); ph_ang.location = (1400, 780)
ph_ang.operation = 'MULTIPLY'
L(V["Attraction Up"], ph_ang.inputs[0])
L(ph_spar.outputs["Factor"], ph_ang.inputs[1])
ph_rot = N("ShaderNodeVectorRotate"); ph_rot.location = (1750, 640)
ph_rot.rotation_type = 'AXIS_ANGLE'
L(ph_rel.outputs[0], ph_rot.inputs["Vector"])
L(ph_ax.outputs[0], ph_rot.inputs["Axis"])
L(ph_ang.outputs[0], ph_rot.inputs["Angle"])
ph_new = N("ShaderNodeVectorMath"); ph_new.location = (1900, 640)
ph_new.operation = 'ADD'
L(_out(ph_anch, "Attribute"), ph_new.inputs[0])
L(ph_rot.outputs[0], ph_new.inputs[1])
ph_set = N("GeometryNodeSetPosition"); ph_set.location = (2050, 780)
L(_out(real, "Geometry"), _sock(ph_set, "Geometry"))
L(ph_new.outputs[0], _sock(ph_set, "Position"))
rspar = N("GeometryNodeSplineParameter"); rspar.location = (1200, 60) rspar = N("GeometryNodeSplineParameter"); rspar.location = (1200, 60)
rpos = N("GeometryNodeInputPosition"); rpos.location = (1200, -100) rpos = N("GeometryNodeInputPosition"); rpos.location = (1200, -100)
rnoise = N("ShaderNodeTexNoise"); rnoise.location = (1350, -140) rnoise = N("ShaderNodeTexNoise"); rnoise.location = (1350, -140)
@@ -599,8 +656,8 @@ def build_group():
L(rscale.outputs[0], roffs.inputs[0]) L(rscale.outputs[0], roffs.inputs[0])
L(dvec.outputs[0], roffs.inputs[1]) L(dvec.outputs[0], roffs.inputs[1])
rsetp = N("GeometryNodeSetPosition"); rsetp.location = (1950, 320) rsetp = N("GeometryNodeSetPosition"); rsetp.location = (2200, 320)
L(_out(real, "Geometry"), _sock(rsetp, "Geometry")) L(_out(ph_set, "Geometry"), _sock(rsetp, "Geometry"))
L(roffs.outputs[0], _sock(rsetp, "Offset")) L(roffs.outputs[0], _sock(rsetp, "Offset"))
# ---------- Sub-Aeste (zweite Verzweigungsebene) ---------- # ---------- Sub-Aeste (zweite Verzweigungsebene) ----------
+1 -1
View File
@@ -1,7 +1,7 @@
"""Prueft das gebaute Tree-Addon: registrieren, Operator ausfuehren, Ergebnis messen.""" """Prueft das gebaute Tree-Addon: registrieren, Operator ausfuehren, Ergebnis messen."""
import bpy, sys, zipfile, os, tempfile, importlib.util, traceback import bpy, sys, zipfile, os, tempfile, importlib.util, traceback
ZIP = r"A:\eco\stylized_rock_generator\dist\stylized_tree_generator-1.21.0.zip" ZIP = r"A:\eco\stylized_rock_generator\dist\stylized_tree_generator-1.22.0.zip"
fails = [] fails = []
tmp = tempfile.mkdtemp() tmp = tempfile.mkdtemp()