Tree Generator v1.8.0: Profilaufloesung je Ast-Ebene (Branch Sides / Sub Sides)

Stamm rund (8 Seiten), Aeste 4, Zweige 3 - wie im TreeIt-Referenzbaum. Runderer
Stamm bei gleichzeitig ~25% weniger Tris (baum 1368 -> 1032).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 11:17:36 +02:00
co-authored by Claude Opus 4.8
parent 57b4c12725
commit 55beb2bb2f
5 changed files with 102 additions and 104 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.7.0" version = "1.8.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.7.0", "version": "1.8.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.7.0.zip", "archive_url": "./stylized_tree_generator-1.8.0.zip",
"archive_size": 13398, "archive_size": 13440,
"archive_hash": "sha256:3817ce7433ef63918ddf75a827591b3bb712c8659141ae85f73f4f1fd19e20fb" "archive_hash": "sha256:e96e6ec6f8009708ce0611dea6ea50e3271b6603d60bce84ebeab2e365bba983"
} }
] ]
} }
Binary file not shown.
Binary file not shown.
+94 -96
View File
@@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "Stylized Tree Generator", "name": "Stylized Tree Generator",
"author": "D4rkst3r", "author": "D4rkst3r",
"version": (1, 7, 0), "version": (1, 8, 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",
@@ -43,7 +43,9 @@ DEFAULTS = {
"Sub Count": 2, # Sub-Aeste je Hauptast (0 = aus) "Sub Count": 2, # Sub-Aeste je Hauptast (0 = aus)
"Sub Length": 0.55, "Sub Length": 0.55,
"Sub Up": 1.4, "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) "UV Scale": 1.0, # Rinden-Dichte; UVs sind world-space (m)
"Tip Blunt": 0.18, # >0 verhindert Nadelspitzen (0 = spitz, 0.45 = Kaktus) "Tip Blunt": 0.18, # >0 verhindert Nadelspitzen (0 = spitz, 0.45 = Kaktus)
"Root Flare": 1.1, # Wurzelanlauf: Stamm verbreitert sich am Fuss "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 "Branch Taper": 0.12, # Arme bleiben dick statt spitz zuzulaufen
"Sub Count": 0, "Sub Count": 0,
"Tip Blunt": 0.45, "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 Length", 'NodeSocketFloat', DEFAULTS["Sub Length"], 0.05, 5.0)
add_in("Sub Up", 'NodeSocketFloat', DEFAULTS["Sub Up"], -3.0, 3.0) add_in("Sub Up", 'NodeSocketFloat', DEFAULTS["Sub Up"], -3.0, 3.0)
add_in("Sides", 'NodeSocketInt', DEFAULTS["Sides"], 3, 32) 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("Tip Blunt", 'NodeSocketFloat', DEFAULTS["Tip Blunt"], 0.0, 0.9)
add_in("UV Scale", 'NodeSocketFloat', DEFAULTS["UV Scale"], 0.01, 20.0) add_in("UV Scale", 'NodeSocketFloat', DEFAULTS["UV Scale"], 0.01, 20.0)
add_in("Detail", 'NodeSocketFloat', DEFAULTS["Detail"], 0.2, 2.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(_out(sreal, "Geometry"), _sock(sbset, "Geometry"))
L(sboff.outputs[0], _sock(sbset, "Offset")) L(sboff.outputs[0], _sock(sbset, "Offset"))
join = N("GeometryNodeJoinGeometry"); join.location = (3150, 200) # ---------- Curve -> Mesh, PRO EBENE mit eigener Profilaufloesung ----------
L(_out(sbset, "Geometry"), join.inputs[0]) # Referenz (TreeIt): Stamm ~9 Segmente, duenne Zweige nur Dreiecke. Ein
L(_out(rsetp, "Geometry"), join.inputs[0]) # "Curve to Mesh" kennt aber nur EIN Profil -> deshalb wird jede Ebene
L(_out(setrad, "Curve"), join.inputs[0]) # 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 ---------- def _to_mesh(curve_node, curve_out_name, sides_socket, x, y):
# Falle 6: Der Profil-Radius wird mit dem Curve-Radius MULTIPLIZIERT. """Kurven -> Mesh inkl. world-space UVs. Pro Ast-Ebene einmal."""
# Profil deshalb auf 1.0 lassen waere richtig -- aber nur, wenn der cs = N("GeometryNodeSplineParameter"); cs.location = (x, y - 140)
# Curve-Radius bereits die echte Staerke ist. Hier ist er das, also 1.0. sv = N("GeometryNodeStoreNamedAttribute"); sv.location = (x + 150, y)
circ = N("GeometryNodeCurvePrimitiveCircle"); circ.location = (1350, -100) sv.domain = 'POINT'; sv.data_type = 'FLOAT'
L(V["Sides"], _sock(circ, "Resolution")) L(_out(curve_node, curve_out_name), _sock(sv, "Geometry"))
_sock(circ, "Radius").default_value = 1.0 _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) ---------- prof = _profile(sides_socket, x - 1100, y - 500)
# 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"))
# ---------- UVs (Curve to Mesh erzeugt KEINE - gemessen!) ---------- cm = N("GeometryNodeCurveToMesh"); cm.location = (x + 500, y)
# V = echte Bogenlaenge entlang Stamm/Ast, U = Bogenlaenge um das Profil mal L(_out(sw, "Geometry"), _sock(cm, "Curve"))
# Radius => world-space UVs: Rinde sitzt auf dickem Stamm und duennem Zweig L(_out(prof, "Geometry"), _sock(cm, "Profile Curve"))
# gleich dicht. Attribute VOR Curve to Mesh ablegen, danach kombinieren. # Falle 7 (Blender 5.x): Radius-Attribut wird NICHT mehr implizit
cspar = N("GeometryNodeSplineParameter"); cspar.location = (2250, 60) # ausgewertet -> muss an den "Scale"-Eingang.
stv = N("GeometryNodeStoreNamedAttribute"); stv.location = (2400, 200) if "Scale" in cm.inputs:
stv.domain = 'POINT'; stv.data_type = 'FLOAT' ra = N("GeometryNodeInputRadius"); ra.location = (x + 350, y - 420)
L(_out(join, "Geometry"), _sock(stv, "Geometry")) L(ra.outputs[0], _sock(cm, "Scale"))
_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(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: try:
_sock(c2m, "Fill Caps").default_value = True _sock(cm, "Fill Caps").default_value = True
except KeyError: except KeyError:
pass pass
# UVMap aus den drei Attributen zusammensetzen (FACE_CORNER, sonst kein UV) nu = N("GeometryNodeInputNamedAttribute"); nu.location = (x + 500, y - 200)
na_u = N("GeometryNodeInputNamedAttribute"); na_u.location = (2700, -300) nu.data_type = 'FLOAT'; _sock(nu, "Name").default_value = "uv_u"
na_u.data_type = 'FLOAT'; _sock(na_u, "Name").default_value = "uv_u" nv = N("GeometryNodeInputNamedAttribute"); nv.location = (x + 500, y - 320)
na_v = N("GeometryNodeInputNamedAttribute"); na_v.location = (2700, -420) nv.data_type = 'FLOAT'; _sock(nv, "Name").default_value = "uv_v"
na_v.data_type = 'FLOAT'; _sock(na_v, "Name").default_value = "uv_v" nr = N("GeometryNodeInputNamedAttribute"); nr.location = (x + 500, y - 440)
na_r = N("GeometryNodeInputNamedAttribute"); na_r.location = (2700, -540) nr.data_type = 'FLOAT'; _sock(nr, "Name").default_value = "uv_r"
na_r.data_type = 'FLOAT'; _sock(na_r, "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
umul = N("ShaderNodeMath"); umul.location = (2880, -300); umul.operation = 'MULTIPLY' mesh_trunk = _to_mesh(setrad, "Curve", V["Sides"], 3400, 900)
L(_out(na_u, "Attribute"), umul.inputs[0]); L(_out(na_r, "Attribute"), umul.inputs[1]) mesh_branch = _to_mesh(rsetp, "Geometry", V["Branch Sides"], 3400, 200)
us = N("ShaderNodeMath"); us.location = (3030, -300); us.operation = 'MULTIPLY' mesh_sub = _to_mesh(sbset, "Geometry", V["Sub Sides"], 3400, -500)
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) join = N("GeometryNodeJoinGeometry"); join.location = (4700, 300)
stuv.domain = 'CORNER'; stuv.data_type = 'FLOAT2' L(_out(mesh_sub, "Geometry"), join.inputs[0])
L(_out(c2m, "Mesh", "Geometry"), _sock(stuv, "Geometry")) L(_out(mesh_branch, "Geometry"), join.inputs[0])
_sock(stuv, "Name").default_value = "UVMap" L(_out(mesh_trunk, "Geometry"), join.inputs[0])
L(uvvec.outputs[0], _sock(stuv, "Value")) stuv = join # Name beibehalten fuer den nachfolgenden Merge-Zweig
# ---------- optional: Aeste mit dem Stamm VERSCHMELZEN ---------- # ---------- optional: Aeste mit dem Stamm VERSCHMELZEN ----------
# Voxel-Remesh (Mesh to Volume -> Volume to Mesh) macht aus den sich # Voxel-Remesh (Mesh to Volume -> Volume to Mesh) macht aus den sich