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:
+105
-10
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user