Tree Generator v1.2.0: world-space UVs, UV1-Lightmap, keine Nadelspitzen

- UVMap aus Geometry Nodes (V = Bogenlaenge, U = Umfang*Radius) -> Rinde ueberall
  gleich dicht, Regler 'UV Scale'
- 'UV1 Lightmap-UV' beim Modifier-Apply (Parity zum Rock-Generator; aktiver Kanal
  wird gesetzt, damit lightmap_pack UV0 nicht ueberschreibt)
- Sub-Aeste enden nicht mehr als Nadeln
- Test prueft jetzt beide UV-Kanaele + world-space-Skalierung

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 10:41:08 +02:00
co-authored by Claude Opus 4.8
parent f9e3fe4223
commit bb576292fa
5 changed files with 110 additions and 15 deletions
+105 -10
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 1, 0),
"version": (1, 2, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -39,12 +39,13 @@ DEFAULTS = {
"Branch Bend": 0.45, # Variation je Ast (0 = gerade Staebe)
"Branch Droop": 0.0, # >0 haengt nach unten (Palme), <0 kruemmt nach oben (Kaktus)
"Branch Thickness": 0.32, # Ast-Dicke relativ zum Stamm
"Branch Taper": 1.0, # 1 = laeuft spitz zu, 0.2 = bleibt dick (Kaktus)
"Branch Taper": 0.85, # wie der Stamm: laeuft NICHT auf 0 -> keine Nadeln
"Sub Count": 3, # Sub-Aeste je Hauptast (0 = aus)
"Sub Length": 0.55,
"Sub Up": 1.4,
"Sides": 6,
"Tip Blunt": 0.0, # 0 = laeuft spitz aus (Baum), 0.45 = stumpfes Ende (Kaktus)
"UV Scale": 1.0, # Rinden-Dichte; UVs sind world-space (m)
"Tip Blunt": 0.18, # >0 verhindert Nadelspitzen (0 = spitz, 0.45 = Kaktus)
"Ribs": 0.0, # senkrechte Kanneluren (0 = glatt); Kaktus ~9
"Rib Depth": 0.0,
}
@@ -148,6 +149,7 @@ def build_group():
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("UV Scale", 'NodeSocketFloat', DEFAULTS["UV Scale"], 0.01, 20.0)
add_in("Ribs", 'NodeSocketFloat', DEFAULTS["Ribs"], 0.0, 20.0)
add_in("Rib Depth", 'NodeSocketFloat', DEFAULTS["Rib Depth"], 0.0, 0.5)
@@ -405,15 +407,22 @@ def build_group():
_sock(sres, "Count").default_value = 6
L(_out(sline, "Curve"), _sock(sres, "Curve"))
# Sub-Aeste: gleiche Verjuengungs-Logik wie Hauptaeste (Branch Taper + Kuppel).
# Vorher lief der Radius hier hart auf 0 -> die Sub-Aeste wurden zu Nadeln.
sspar = N("GeometryNodeSplineParameter"); sspar.location = (2250, 900)
stap = N("ShaderNodeMath"); stap.location = (2330, 1000); stap.operation = 'MULTIPLY'
L(V["Branch Taper"], stap.inputs[0]); L(sspar.outputs["Factor"], stap.inputs[1])
sinv = N("ShaderNodeMath"); sinv.location = (2400, 900)
sinv.operation = 'SUBTRACT'; sinv.inputs[0].default_value = 1.0
L(sspar.outputs["Factor"], sinv.inputs[1])
L(stap.outputs[0], sinv.inputs[1])
srad = N("ShaderNodeMath"); srad.location = (2550, 900); srad.operation = 'MULTIPLY'
L(V["Trunk Radius"], srad.inputs[0]); L(sinv.outputs[0], srad.inputs[1])
srad2 = N("ShaderNodeMath"); srad2.location = (2700, 900); srad2.operation = 'MULTIPLY'
srad2.inputs[1].default_value = 0.14 # deutlich duenner als Hauptaeste
L(srad.outputs[0], srad2.inputs[0])
srad1 = N("ShaderNodeMath"); srad1.location = (2700, 900); srad1.operation = 'MULTIPLY'
srad1.inputs[1].default_value = 0.24 # duenner als Hauptaeste
L(srad.outputs[0], srad1.inputs[0])
sdome = _dome(sspar, 2550, 1200)
srad2 = N("ShaderNodeMath"); srad2.location = (3050, 900); srad2.operation = 'MULTIPLY'
L(srad1.outputs[0], srad2.inputs[0]); L(sdome.outputs[0], srad2.inputs[1])
ssetr = N("GeometryNodeSetCurveRadius"); ssetr.location = (2550, 760)
L(_out(sres, "Curve"), _sock(ssetr, "Curve"))
L(srad2.outputs[0], _sock(ssetr, "Radius"))
@@ -478,9 +487,34 @@ def build_group():
L(_out(circ, "Curve"), _sock(pset, "Geometry"))
L(poff.outputs[0], _sock(pset, "Offset"))
# ---------- 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(join, "Geometry"), _sock(c2m, "Curve"))
L(_out(pset, "Geometry"), _sock(c2m, "Profile Curve"))
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),
@@ -493,8 +527,31 @@ def build_group():
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"
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"])
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"))
shade = N("GeometryNodeSetShadeSmooth"); shade.location = (1650, 200)
L(_out(c2m, "Mesh", "Geometry"), _sock(shade, "Geometry"))
L(_out(stuv, "Geometry"), _sock(shade, "Geometry"))
L(_out(shade, "Geometry"), gout.inputs[0])
return ng
@@ -593,12 +650,43 @@ class TreeGenSettings(PropertyGroup):
name="Modifier anwenden", default=False,
description="Ergebnis als normales Mesh einfrieren (fuer den Export)",
)
generate_lightmap_uv: BoolProperty(
name="UV1 Lightmap-UV", default=True,
description=("Zweiter, nicht ueberlappender UV-Kanal fuer Unreal-Lightmaps. "
"Braucht 'Modifier anwenden' (nur echte Meshes lassen sich unwrappen)"),
)
def _place(ob, x):
ob.location.x = x
def _add_lightmap_uv(context, ob, op):
"""UV1 fuer Unreal-Lightmaps. Nur auf echten Meshes moeglich (nach Apply)."""
if not ob.data.polygons:
return
lm = ob.data.uv_layers.get("Lightmap") or ob.data.uv_layers.new(name="Lightmap")
# WICHTIG: aktiven Kanal setzen, sonst ueberschreibt lightmap_pack UV0.
ob.data.uv_layers.active = lm
for o in context.view_layer.objects:
try:
o.select_set(False)
except (ReferenceError, RuntimeError):
pass
ob.select_set(True)
context.view_layer.objects.active = ob
try:
bpy.ops.uv.lightmap_pack(PREF_CONTEXT='ALL_FACES', PREF_PACK_IN_ONE=True,
PREF_NEW_UVLAYER=False, PREF_BOX_DIV=12,
PREF_MARGIN_DIV=0.2)
except RuntimeError as exc:
op.report({'WARNING'}, "Lightmap-UV: %s" % exc)
# UV0 wieder als Kanal 0 / Render-UV (Unreal nutzt die Reihenfolge)
if ob.data.uv_layers:
ob.data.uv_layers.active_index = 0
ob.data.uv_layers[0].active_render = True
class TREEGEN_OT_create(Operator):
bl_idname = "object.treegen_create"
bl_label = "Baum erzeugen"
@@ -643,6 +731,10 @@ class TREEGEN_OT_create(Operator):
bpy.ops.object.modifier_apply(modifier="GN_Tree")
except RuntimeError as exc:
self.report({'WARNING'}, "Modifier-Apply: %s" % exc)
continue
# UV0 heisst nach dem Apply "UVMap" (aus den Geometry Nodes).
if s.generate_lightmap_uv:
_add_lightmap_uv(context, ob, self)
context.view_layer.update()
total = 0
@@ -683,6 +775,9 @@ class VIEW3D_PT_tree_generator(Panel):
box = layout.box()
box.label(text="Export")
box.prop(s, "apply_modifier")
row = box.row()
row.enabled = s.apply_modifier
row.prop(s, "generate_lightmap_uv")
layout.separator()
layout.operator("object.treegen_create", icon='OUTLINER_OB_MESH')