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"
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"
+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.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"
}
]
}
Binary file not shown.
Binary file not shown.
+94 -96
View File
@@ -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"))
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"))
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(c2m, "Fill Caps").default_value = True
_sock(cm, "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"
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
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"])
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)
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