Aeste/Zweige werden per Instanz-Scale an den Radius ihres Elternpunktes gekoppelt
('Input Radius' auf der Punkt-Domain von Instance on Points). Oben am duennen
Stamm sind die Aeste dadurch automatisch kuerzer und duenner statt gleich dick.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
863 lines
38 KiB
Python
863 lines
38 KiB
Python
bl_info = {
|
|
"name": "Stylized Tree Generator",
|
|
"author": "D4rkst3r",
|
|
"version": (1, 5, 0),
|
|
"blender": (4, 2, 0),
|
|
"location": "View3D > Sidebar > Tree Gen",
|
|
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
|
"category": "Add Mesh",
|
|
}
|
|
|
|
# Parametrischer Stylized-Baum als Geometry-Nodes-Gruppe (Stamm + Aeste).
|
|
# Blaetter/Krone macht der Nutzer selbst - dieses Addon liefert das Geaest.
|
|
#
|
|
# HINWEIS: Der Node-Aufbau unten ist identisch mit EcoGame tools/blender_tree_gen.py
|
|
# (dort als CLI-Variante). Aenderungen bitte in beiden pflegen.
|
|
#
|
|
# LIVE BEARBEITEN: Nach dem Erzeugen liegen alle Regler am Modifier "GN_Tree" -
|
|
# im Viewport ziehen, der Baum aktualisiert sich sofort.
|
|
|
|
import bpy
|
|
import sys
|
|
import os
|
|
|
|
GROUP_NAME = "GN_StylizedTree"
|
|
OBJ_NAME = "StylizedTree"
|
|
|
|
# ============================ REGLER (Defaults) ============================
|
|
DEFAULTS = {
|
|
"Seed": 0,
|
|
"Height": 5.0,
|
|
"Trunk Radius": 0.13, # schlanker Stamm; zu dick wirkt sofort "stumpf"
|
|
"Bend": 0.5,
|
|
"Taper": 0.9,
|
|
"Branch Count": 9,
|
|
"Branch Length": 1.3,
|
|
"Branch Up": 1.3, # >1 = Aeste zeigen nach oben; negativ = haengend
|
|
"Branch Start": 0.35, # ab wo am Stamm Aeste sitzen (0..1)
|
|
"Branch End": 0.95,
|
|
"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 Thickness": 0.32, # Ast-Dicke relativ zum Stamm
|
|
"Branch Taper": 0.55, # schwach verjuengen -> Roehre statt Kegel/Dorn
|
|
"Sub Count": 3, # Sub-Aeste je Hauptast (0 = aus)
|
|
"Sub Length": 0.55,
|
|
"Sub Up": 1.4,
|
|
"Sides": 6,
|
|
"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,
|
|
}
|
|
|
|
# Presets: nur die Abweichungen von DEFAULTS.
|
|
PRESETS = {
|
|
"baum": {},
|
|
"palme": {
|
|
"Height": 7.0, "Trunk Radius": 0.16, "Bend": 1.1, "Taper": 0.55,
|
|
"Branch Count": 11, "Branch Length": 2.6, "Branch Up": 0.55,
|
|
"Branch Start": 0.93, "Branch End": 1.0,
|
|
"Branch Bend": 0.25, "Branch Droop": 1.5,
|
|
"Sub Count": 0, # Wedel-Fiederung macht der User als Blattwerk
|
|
"Sides": 6,
|
|
},
|
|
"busch": {
|
|
"Height": 1.6, "Trunk Radius": 0.07, "Bend": 0.4, "Taper": 0.8,
|
|
"Branch Count": 14, "Branch Length": 0.9, "Branch Up": 1.0,
|
|
"Branch Start": 0.15, "Branch End": 0.95,
|
|
"Branch Bend": 0.6, "Branch Droop": 0.0,
|
|
"Sub Count": 2, "Sub Length": 0.35, "Sub Up": 1.6, "Sides": 5,
|
|
},
|
|
# Kaktus: dicker, kaum verjuengter Stamm, wenige Arme, die per NEGATIVEM
|
|
# Droop nach oben kruemmen (Saguaro-Silhouette). Keine Sub-Aeste.
|
|
"kaktus": {
|
|
"Height": 3.0, "Trunk Radius": 0.28, "Bend": 0.08, "Taper": 0.10,
|
|
"Branch Count": 2, "Branch Length": 1.6, "Branch Up": 0.05,
|
|
"Branch Start": 0.28, "Branch End": 0.5,
|
|
"Branch Bend": 0.0,
|
|
"Branch Droop": -2.2, # negativ = Arme kruemmen nach OBEN (Saguaro)
|
|
"Branch Thickness": 0.72, # Arme fast so dick wie der Stamm
|
|
"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,
|
|
},
|
|
}
|
|
# ===========================================================================
|
|
|
|
|
|
def _sock(node, *names):
|
|
"""Falle 1: Socket nach Namen holen, mit Alternativen."""
|
|
for n in names:
|
|
if n in node.inputs:
|
|
return node.inputs[n]
|
|
raise KeyError("Socket %s nicht in %s" % (names, node.bl_idname))
|
|
|
|
|
|
def _out(node, *names):
|
|
for n in names:
|
|
if n in node.outputs:
|
|
return node.outputs[n]
|
|
return node.outputs[0]
|
|
|
|
|
|
def build_group():
|
|
old = bpy.data.node_groups.get(GROUP_NAME)
|
|
if old:
|
|
bpy.data.node_groups.remove(old)
|
|
ng = bpy.data.node_groups.new(GROUP_NAME, "GeometryNodeTree")
|
|
N, L = ng.nodes.new, ng.links.new
|
|
iface = ng.interface
|
|
|
|
def _new_any(*bl_idnames):
|
|
"""Falle 2: erster Node-Typ, den diese Blender-Version kennt."""
|
|
for bid in bl_idnames:
|
|
try:
|
|
return N(bid)
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
iface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
|
|
|
def add_in(name, stype, default, mn=None, mx=None):
|
|
s = iface.new_socket(name, in_out='INPUT', socket_type=stype)
|
|
s.default_value = default
|
|
if mn is not None:
|
|
s.min_value = mn
|
|
if mx is not None:
|
|
s.max_value = mx
|
|
|
|
add_in("Seed", 'NodeSocketInt', DEFAULTS["Seed"], 0, 9999)
|
|
add_in("Height", 'NodeSocketFloat', DEFAULTS["Height"], 0.5, 30.0)
|
|
add_in("Trunk Radius", 'NodeSocketFloat', DEFAULTS["Trunk Radius"], 0.01, 3.0)
|
|
add_in("Bend", 'NodeSocketFloat', DEFAULTS["Bend"], 0.0, 3.0)
|
|
add_in("Taper", 'NodeSocketFloat', DEFAULTS["Taper"], 0.0, 1.0)
|
|
add_in("Branch Count", 'NodeSocketInt', DEFAULTS["Branch Count"], 0, 60)
|
|
add_in("Branch Length", 'NodeSocketFloat', DEFAULTS["Branch Length"], 0.1, 10.0)
|
|
add_in("Branch Up", 'NodeSocketFloat', DEFAULTS["Branch Up"], -3.0, 3.0)
|
|
add_in("Branch Start", 'NodeSocketFloat', DEFAULTS["Branch Start"], 0.0, 1.0)
|
|
add_in("Branch End", 'NodeSocketFloat', DEFAULTS["Branch End"], 0.0, 1.0)
|
|
add_in("Branch Bend", 'NodeSocketFloat', DEFAULTS["Branch Bend"], 0.0, 2.0)
|
|
# Droop darf NEGATIV sein: dann kruemmen sich die Aeste nach OBEN
|
|
# (= Kaktus-Arme statt Palmwedel).
|
|
add_in("Branch Droop", 'NodeSocketFloat', DEFAULTS["Branch Droop"], -3.0, 3.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("Sub Count", 'NodeSocketInt', DEFAULTS["Sub Count"], 0, 12)
|
|
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("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)
|
|
|
|
gin = N("NodeGroupInput"); gin.location = (-1400, 0)
|
|
gout = N("NodeGroupOutput"); gout.location = (1400, 0)
|
|
V = gin.outputs
|
|
|
|
# ---------- Stamm ----------
|
|
line = N("GeometryNodeCurvePrimitiveLine"); line.location = (-1150, 200)
|
|
top = N("ShaderNodeCombineXYZ"); top.location = (-1300, 120)
|
|
L(V["Height"], top.inputs["Z"])
|
|
L(top.outputs[0], _sock(line, "End"))
|
|
|
|
res = N("GeometryNodeResampleCurve"); res.location = (-950, 200)
|
|
_sock(res, "Count").default_value = 24 # genug Punkte, damit die Kuppel rund wird
|
|
L(_out(line, "Curve"), _sock(res, "Curve"))
|
|
|
|
spar = N("GeometryNodeSplineParameter"); spar.location = (-950, -60)
|
|
pos = N("GeometryNodeInputPosition"); pos.location = (-1150, -220)
|
|
|
|
noise = N("ShaderNodeTexNoise"); noise.location = (-950, -260)
|
|
noise.noise_dimensions = '4D'
|
|
_sock(noise, "Scale").default_value = 0.55
|
|
L(pos.outputs[0], _sock(noise, "Vector"))
|
|
L(V["Seed"], _sock(noise, "W"))
|
|
|
|
nsub = N("ShaderNodeVectorMath"); nsub.location = (-750, -260)
|
|
nsub.operation = 'SUBTRACT'; nsub.inputs[1].default_value = (0.5, 0.5, 0.5)
|
|
L(noise.outputs["Color"], nsub.inputs[0])
|
|
|
|
# Falle 5: Z des Offsets platt machen
|
|
flat = N("ShaderNodeVectorMath"); flat.location = (-580, -260)
|
|
flat.operation = 'MULTIPLY'; flat.inputs[1].default_value = (1.0, 1.0, 0.0)
|
|
L(nsub.outputs[0], flat.inputs[0])
|
|
|
|
# Falle 4: Offset mit Spline-Faktor skalieren -> Fuss bleibt stehen
|
|
bendf = N("ShaderNodeMath"); bendf.location = (-750, -60)
|
|
bendf.operation = 'MULTIPLY'
|
|
L(V["Bend"], bendf.inputs[0])
|
|
L(spar.outputs["Factor"], bendf.inputs[1])
|
|
|
|
offs = N("ShaderNodeVectorMath"); offs.location = (-400, -200)
|
|
offs.operation = 'SCALE'
|
|
L(flat.outputs[0], offs.inputs[0])
|
|
L(bendf.outputs[0], _sock(offs, "Scale"))
|
|
|
|
setpos = N("GeometryNodeSetPosition"); setpos.location = (-250, 200)
|
|
L(_out(res, "Curve"), _sock(setpos, "Geometry"))
|
|
L(offs.outputs[0], _sock(setpos, "Offset"))
|
|
|
|
# Radius: TrunkRadius * (1 - Taper * factor)
|
|
tf = N("ShaderNodeMath"); tf.location = (-580, 60); tf.operation = 'MULTIPLY'
|
|
L(V["Taper"], tf.inputs[0]); L(spar.outputs["Factor"], tf.inputs[1])
|
|
inv = N("ShaderNodeMath"); inv.location = (-420, 60); inv.operation = 'SUBTRACT'
|
|
inv.inputs[0].default_value = 1.0
|
|
L(tf.outputs[0], inv.inputs[1])
|
|
# Kuppel-Profil: (1 - f^3)^0.5 -> breite, gleichmaessige Kuppe. Mit einem
|
|
# zu spaeten/steilen Profil (f^8) trifft die Rundung nur 1-2 Resample-
|
|
# Punkte und wird zum KEGEL. Breit + genug Punkte = runde Kuppe.
|
|
# Bei starkem Taper (Baum) dominiert ohnehin die lineare Verjuengung.
|
|
def _dome(spar_node, x, y):
|
|
pw = N("ShaderNodeMath"); pw.location = (x, y); pw.operation = 'POWER'
|
|
pw.inputs[1].default_value = 3.0
|
|
L(spar_node.outputs["Factor"], pw.inputs[0])
|
|
s = N("ShaderNodeMath"); s.location = (x + 150, y); s.operation = 'SUBTRACT'
|
|
s.inputs[0].default_value = 1.0
|
|
L(pw.outputs[0], s.inputs[1])
|
|
rt = N("ShaderNodeMath"); rt.location = (x + 300, y); rt.operation = 'POWER'
|
|
rt.inputs[1].default_value = 0.5
|
|
L(s.outputs[0], rt.inputs[0])
|
|
return rt
|
|
|
|
# Verjuengung UND Kuppel GEMEINSAM begrenzen, dann erst mit dem Basisradius
|
|
# multiplizieren. Wirkt der Mindestwert nur auf die Kuppel, bleibt der bereits
|
|
# verjuengte Radius trotzdem winzig — gemessen: Sub-Ast-Spitze 0.8 mm bei
|
|
# 31 mm Basis, also weiterhin eine Nadel.
|
|
def _shape(inv_taper_socket, dome_node, base_socket, x, y):
|
|
mul = N("ShaderNodeMath"); mul.location = (x, y); mul.operation = 'MULTIPLY'
|
|
L(inv_taper_socket, mul.inputs[0]); L(dome_node.outputs[0], mul.inputs[1])
|
|
mx = N("ShaderNodeMath"); mx.location = (x + 150, y); mx.operation = 'MAXIMUM'
|
|
L(mul.outputs[0], mx.inputs[0]); L(V["Tip Blunt"], mx.inputs[1])
|
|
r = N("ShaderNodeMath"); r.location = (x + 300, y); r.operation = 'MULTIPLY'
|
|
L(base_socket, r.inputs[0]); L(mx.outputs[0], r.inputs[1])
|
|
return r
|
|
|
|
tdome = _dome(spar, -900, 220)
|
|
trad = _shape(inv.outputs[0], tdome, V["Trunk Radius"], -260, 60)
|
|
|
|
setrad = N("GeometryNodeSetCurveRadius"); setrad.location = (-80, 200)
|
|
L(_out(setpos, "Geometry"), _sock(setrad, "Curve"))
|
|
L(trad.outputs[0], _sock(setrad, "Radius"))
|
|
|
|
# ---------- Ast-Ursprünge auf dem oberen Stamm ----------
|
|
trim = N("GeometryNodeTrimCurve"); trim.location = (100, 320)
|
|
L(_out(setrad, "Curve"), _sock(trim, "Curve"))
|
|
# Ansatzbereich der Aeste: Baum = breit gestreut, Palme = alles ganz oben.
|
|
try:
|
|
L(V["Branch Start"], _sock(trim, "Start"))
|
|
L(V["Branch End"], _sock(trim, "End"))
|
|
except KeyError:
|
|
pass
|
|
|
|
c2p = N("GeometryNodeCurveToPoints"); c2p.location = (280, 320)
|
|
try:
|
|
c2p.mode = 'COUNT' # Falle 3
|
|
except Exception:
|
|
pass
|
|
L(_out(trim, "Curve"), _sock(c2p, "Curve"))
|
|
L(V["Branch Count"], _sock(c2p, "Count"))
|
|
|
|
# ---------- Ast-Geometrie (verjüngte Linie) ----------
|
|
bline = N("GeometryNodeCurvePrimitiveLine"); bline.location = (100, 40)
|
|
btop = N("ShaderNodeCombineXYZ"); btop.location = (-60, -20)
|
|
L(V["Branch Length"], btop.inputs["Z"])
|
|
L(btop.outputs[0], _sock(bline, "End"))
|
|
bres = N("GeometryNodeResampleCurve"); bres.location = (280, 40)
|
|
_sock(bres, "Count").default_value = 14 # zu wenig -> Kuppel wird zum Kegel
|
|
L(_out(bline, "Curve"), _sock(bres, "Curve"))
|
|
|
|
# Ast-Verjuengung: 1 - BranchTaper*factor. Bei Kakteen klein halten,
|
|
# sonst laufen die Arme spitz zu wie Dornen.
|
|
bspar = N("GeometryNodeSplineParameter"); bspar.location = (280, -140)
|
|
btap = N("ShaderNodeMath"); btap.location = (360, -240); btap.operation = 'MULTIPLY'
|
|
L(V["Branch Taper"], btap.inputs[0])
|
|
L(bspar.outputs["Factor"], btap.inputs[1])
|
|
binv = N("ShaderNodeMath"); binv.location = (440, -140)
|
|
binv.operation = 'SUBTRACT'; binv.inputs[0].default_value = 1.0
|
|
L(btap.outputs[0], binv.inputs[1])
|
|
# Ast-Dicke relativ zum Stamm. Beim Kaktus muessen die Arme fast so dick
|
|
# sein wie der Stamm (0.7+), beim Baum deutlich duenner (~0.3).
|
|
bbase = N("ShaderNodeMath"); bbase.location = (600, -140); bbase.operation = 'MULTIPLY'
|
|
L(V["Trunk Radius"], bbase.inputs[0]); L(V["Branch Thickness"], bbase.inputs[1])
|
|
bdome = _dome(bspar, 600, -600)
|
|
bscl = _shape(binv.outputs[0], bdome, bbase.outputs[0], 780, -140)
|
|
|
|
bsetr = N("GeometryNodeSetCurveRadius"); bsetr.location = (600, 40)
|
|
L(_out(bres, "Curve"), _sock(bsetr, "Curve"))
|
|
L(bscl.outputs[0], _sock(bsetr, "Radius"))
|
|
|
|
# ---------- Ausrichtung relativ zur ELTERNKURVE ----------
|
|
# Curve to Points liefert Tangent + Normal des Ansatzpunktes gratis mit.
|
|
# Die Normale um die Tangente zu drehen (Index * Goldener Winkel) verteilt
|
|
# die Aeste spiralig um den Elternast (Phyllotaxis) UND folgt automatisch
|
|
# dessen Biegung. Vorher wurde die Richtung aus Weltkoordinaten gerechnet ->
|
|
# Aeste standen wie angeklebte Nadeln quer zum Ast.
|
|
def _child_dir(points_node, up_socket, x, y):
|
|
i = N("GeometryNodeInputIndex"); i.location = (x, y + 220)
|
|
a = N("ShaderNodeMath"); a.location = (x + 150, y + 220); a.operation = 'MULTIPLY'
|
|
a.inputs[1].default_value = 2.399963 # Goldener Winkel
|
|
L(i.outputs[0], a.inputs[0])
|
|
vr = N("ShaderNodeVectorRotate"); vr.location = (x + 320, y + 120)
|
|
vr.rotation_type = 'AXIS_ANGLE'
|
|
L(_out(points_node, "Normal"), vr.inputs["Vector"])
|
|
L(_out(points_node, "Tangent"), vr.inputs["Axis"])
|
|
L(a.outputs[0], vr.inputs["Angle"])
|
|
lean = N("ShaderNodeVectorMath"); lean.location = (x + 320, y - 60)
|
|
lean.operation = 'SCALE'
|
|
L(_out(points_node, "Tangent"), lean.inputs[0])
|
|
L(up_socket, _sock(lean, "Scale"))
|
|
add = N("ShaderNodeVectorMath"); add.location = (x + 500, y + 40)
|
|
add.operation = 'ADD'
|
|
L(vr.outputs[0], add.inputs[0]); L(lean.outputs[0], add.inputs[1])
|
|
return add
|
|
|
|
# ---------- radiale Ausrichtung je Ast ----------
|
|
dirv = _child_dir(c2p, V["Branch Up"], 100, 560)
|
|
|
|
align = _new_any("FunctionNodeAlignRotationToVector", "FunctionNodeAlignEulerToVector")
|
|
if align is not None:
|
|
align.location = (900, 560)
|
|
try:
|
|
align.axis = 'Z'
|
|
except Exception:
|
|
pass
|
|
L(dirv.outputs[0], _sock(align, "Vector"))
|
|
|
|
# Aeste an die ELTERNDICKE anpassen: "Instance on Points" wertet Felder auf
|
|
# der Punkt-Domain aus, dort liefert "Input Radius" den Stammradius genau am
|
|
# Ansatzpunkt. Ohne das sind Aeste oben (duenner Stamm) genauso dick wie
|
|
# unten - und damit dicker als der Stamm selbst.
|
|
prad = N("GeometryNodeInputRadius"); prad.location = (900, 120)
|
|
pratio = N("ShaderNodeMath"); pratio.location = (1050, 120); pratio.operation = 'DIVIDE'
|
|
L(prad.outputs[0], pratio.inputs[0]); L(V["Trunk Radius"], pratio.inputs[1])
|
|
pscale = N("ShaderNodeMapRange"); pscale.location = (1200, 120)
|
|
L(pratio.outputs[0], pscale.inputs["Value"])
|
|
pscale.inputs["To Min"].default_value = 0.35 # nicht ganz kollabieren lassen
|
|
pscale.inputs["To Max"].default_value = 1.0
|
|
pscale.clamp = True
|
|
|
|
inst = N("GeometryNodeInstanceOnPoints"); inst.location = (1400, 320)
|
|
L(_out(c2p, "Points"), _sock(inst, "Points"))
|
|
L(_out(bsetr, "Curve"), _sock(inst, "Instance"))
|
|
L(pscale.outputs[0], _sock(inst, "Scale"))
|
|
if align is not None:
|
|
L(align.outputs[0], _sock(inst, "Rotation"))
|
|
|
|
real = N("GeometryNodeRealizeInstances"); real.location = (1200, 320)
|
|
L(_out(inst, "Instances", "Geometry"), _sock(real, "Geometry"))
|
|
|
|
# ---------- Ast-Biegung NACH dem Realize ----------
|
|
# Trick fuer Variation je Ast: hier ist die Position bereits die WELT-Position
|
|
# des jeweiligen Astes. Dieselbe Noise liefert damit pro Ast einen anderen
|
|
# Wert — vor dem Realize haetten alle Instanzen identische lokale Coords
|
|
# (und wuerden exakt gleich gebogen).
|
|
rspar = N("GeometryNodeSplineParameter"); rspar.location = (1200, 60)
|
|
rpos = N("GeometryNodeInputPosition"); rpos.location = (1200, -100)
|
|
rnoise = N("ShaderNodeTexNoise"); rnoise.location = (1350, -140)
|
|
rnoise.noise_dimensions = '4D'
|
|
_sock(rnoise, "Scale").default_value = 0.9
|
|
L(rpos.outputs[0], _sock(rnoise, "Vector"))
|
|
L(V["Seed"], _sock(rnoise, "W"))
|
|
rsub = N("ShaderNodeVectorMath"); rsub.location = (1500, -140)
|
|
rsub.operation = 'SUBTRACT'; rsub.inputs[1].default_value = (0.5, 0.5, 0.5)
|
|
L(rnoise.outputs["Color"], rsub.inputs[0])
|
|
|
|
# Staerke waechst zur Astspitze -> Ansatz bleibt am Stamm
|
|
rfac = N("ShaderNodeMath"); rfac.location = (1350, 60); rfac.operation = 'MULTIPLY'
|
|
L(V["Branch Bend"], rfac.inputs[0])
|
|
L(rspar.outputs["Factor"], rfac.inputs[1])
|
|
rscale = N("ShaderNodeVectorMath"); rscale.location = (1650, -140)
|
|
rscale.operation = 'SCALE'
|
|
L(rsub.outputs[0], rscale.inputs[0])
|
|
L(rfac.outputs[0], _sock(rscale, "Scale"))
|
|
|
|
# Droop: Spitzen haengen nach unten (Palmwedel) -- quadratisch = schoene Kurve
|
|
dsq = N("ShaderNodeMath"); dsq.location = (1350, -320); dsq.operation = 'MULTIPLY'
|
|
L(rspar.outputs["Factor"], dsq.inputs[0]); L(rspar.outputs["Factor"], dsq.inputs[1])
|
|
dmul = N("ShaderNodeMath"); dmul.location = (1500, -320); dmul.operation = 'MULTIPLY'
|
|
L(V["Branch Droop"], dmul.inputs[0]); L(dsq.outputs[0], dmul.inputs[1])
|
|
dneg = N("ShaderNodeMath"); dneg.location = (1650, -320); dneg.operation = 'MULTIPLY'
|
|
dneg.inputs[1].default_value = -1.0
|
|
L(dmul.outputs[0], dneg.inputs[0])
|
|
dvec = N("ShaderNodeCombineXYZ"); dvec.location = (1800, -320)
|
|
L(dneg.outputs[0], dvec.inputs["Z"])
|
|
|
|
roffs = N("ShaderNodeVectorMath"); roffs.location = (1800, -140)
|
|
roffs.operation = 'ADD'
|
|
L(rscale.outputs[0], roffs.inputs[0])
|
|
L(dvec.outputs[0], roffs.inputs[1])
|
|
|
|
rsetp = N("GeometryNodeSetPosition"); rsetp.location = (1950, 320)
|
|
L(_out(real, "Geometry"), _sock(rsetp, "Geometry"))
|
|
L(roffs.outputs[0], _sock(rsetp, "Offset"))
|
|
|
|
# ---------- Sub-Aeste (zweite Verzweigungsebene) ----------
|
|
# Sitzen auf den bereits gebogenen Hauptaesten. Bei "Sub Count" = 0 liefert
|
|
# Curve to Points keine Punkte -> es entsteht schlicht nichts (kein Fehler).
|
|
strim = N("GeometryNodeTrimCurve"); strim.location = (2100, 520)
|
|
L(_out(rsetp, "Geometry"), _sock(strim, "Curve"))
|
|
try:
|
|
_sock(strim, "Start").default_value = 0.3
|
|
_sock(strim, "End").default_value = 0.9
|
|
except KeyError:
|
|
pass
|
|
sp = N("GeometryNodeCurveToPoints"); sp.location = (2250, 520)
|
|
try:
|
|
sp.mode = 'COUNT'
|
|
except Exception:
|
|
pass
|
|
L(_out(strim, "Curve"), _sock(sp, "Curve"))
|
|
L(V["Sub Count"], _sock(sp, "Count"))
|
|
|
|
sline = N("GeometryNodeCurvePrimitiveLine"); sline.location = (2100, 760)
|
|
stop = N("ShaderNodeCombineXYZ"); stop.location = (1950, 700)
|
|
L(V["Sub Length"], stop.inputs["Z"])
|
|
L(stop.outputs[0], _sock(sline, "End"))
|
|
sres = N("GeometryNodeResampleCurve"); sres.location = (2250, 760)
|
|
_sock(sres, "Count").default_value = 9 # genug Punkte, damit die Biegung sichtbar wird
|
|
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)
|
|
# ... und verjuengen sich nur SCHWACH. Ein dicker Ansatz, der spitz zulaeuft,
|
|
# ist genau die Dorn-Form - ein echter Zweig ist duenn und gleichmaessig.
|
|
shalf = N("ShaderNodeMath"); shalf.location = (2180, 1000); shalf.operation = 'MULTIPLY'
|
|
L(V["Branch Taper"], shalf.inputs[0]); shalf.inputs[1].default_value = 0.35
|
|
stap = N("ShaderNodeMath"); stap.location = (2330, 1000); stap.operation = 'MULTIPLY'
|
|
L(shalf.outputs[0], 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(stap.outputs[0], sinv.inputs[1])
|
|
sbase = N("ShaderNodeMath"); sbase.location = (2550, 900); sbase.operation = 'MULTIPLY'
|
|
L(V["Trunk Radius"], sbase.inputs[0])
|
|
sbase.inputs[1].default_value = 0.12 # Zweige sind DUENN ...
|
|
sdome = _dome(sspar, 2550, 1200)
|
|
srad2 = _shape(sinv.outputs[0], sdome, sbase.outputs[0], 2750, 900)
|
|
ssetr = N("GeometryNodeSetCurveRadius"); ssetr.location = (2550, 760)
|
|
L(_out(sres, "Curve"), _sock(ssetr, "Curve"))
|
|
L(srad2.outputs[0], _sock(ssetr, "Radius"))
|
|
|
|
# Richtung: radial gestreut (Index) + "Sub Up"
|
|
sdir = _child_dir(sp, V["Sub Up"], 2100, 1060)
|
|
|
|
salign = _new_any("FunctionNodeAlignRotationToVector", "FunctionNodeAlignEulerToVector")
|
|
if salign is not None:
|
|
salign.location = (2700, 1060)
|
|
try:
|
|
salign.axis = 'Z'
|
|
except Exception:
|
|
pass
|
|
L(sdir.outputs[0], _sock(salign, "Vector"))
|
|
|
|
# Zweige ebenso an die Dicke ihres Elternastes koppeln.
|
|
sprad = N("GeometryNodeInputRadius"); sprad.location = (2700, 300)
|
|
sbaseref = N("ShaderNodeMath"); sbaseref.location = (2700, 180); sbaseref.operation = 'MULTIPLY'
|
|
L(V["Trunk Radius"], sbaseref.inputs[0]); L(V["Branch Thickness"], sbaseref.inputs[1])
|
|
sratio = N("ShaderNodeMath"); sratio.location = (2850, 240); sratio.operation = 'DIVIDE'
|
|
L(sprad.outputs[0], sratio.inputs[0]); L(sbaseref.outputs[0], sratio.inputs[1])
|
|
sscale = N("ShaderNodeMapRange"); sscale.location = (3000, 240)
|
|
L(sratio.outputs[0], sscale.inputs["Value"])
|
|
sscale.inputs["To Min"].default_value = 0.4
|
|
sscale.inputs["To Max"].default_value = 1.0
|
|
sscale.clamp = True
|
|
|
|
sinst = N("GeometryNodeInstanceOnPoints"); sinst.location = (3150, 520)
|
|
L(_out(sp, "Points"), _sock(sinst, "Points"))
|
|
L(_out(ssetr, "Curve"), _sock(sinst, "Instance"))
|
|
L(sscale.outputs[0], _sock(sinst, "Scale"))
|
|
if salign is not None:
|
|
L(salign.outputs[0], _sock(sinst, "Rotation"))
|
|
sreal = N("GeometryNodeRealizeInstances"); sreal.location = (3000, 520)
|
|
L(_out(sinst, "Instances", "Geometry"), _sock(sreal, "Geometry"))
|
|
|
|
# Sub-Aeste genauso biegen wie Stamm und Hauptaeste. Ohne diesen Schritt
|
|
# blieben sie schnurgerade - das ist der Grund, warum sie wie angeklebte
|
|
# Staebe wirkten. Auch hier NACH dem Realize, damit die Weltposition jedem
|
|
# Zweig ein eigenes Noise gibt (sonst biegen alle identisch).
|
|
sbspar = N("GeometryNodeSplineParameter"); sbspar.location = (3000, 300)
|
|
sbpos = N("GeometryNodeInputPosition"); sbpos.location = (3000, 160)
|
|
sbnoise = N("ShaderNodeTexNoise"); sbnoise.location = (3150, 160)
|
|
sbnoise.noise_dimensions = '4D'
|
|
_sock(sbnoise, "Scale").default_value = 1.6
|
|
L(sbpos.outputs[0], _sock(sbnoise, "Vector"))
|
|
L(V["Seed"], _sock(sbnoise, "W"))
|
|
sbsub = N("ShaderNodeVectorMath"); sbsub.location = (3320, 160)
|
|
sbsub.operation = 'SUBTRACT'; sbsub.inputs[1].default_value = (0.5, 0.5, 0.5)
|
|
L(sbnoise.outputs["Color"], sbsub.inputs[0])
|
|
sbfac = N("ShaderNodeMath"); sbfac.location = (3150, 300); sbfac.operation = 'MULTIPLY'
|
|
L(V["Branch Bend"], sbfac.inputs[0])
|
|
L(sbspar.outputs["Factor"], sbfac.inputs[1])
|
|
sbamt = N("ShaderNodeMath"); sbamt.location = (3320, 300); sbamt.operation = 'MULTIPLY'
|
|
sbamt.inputs[1].default_value = 0.5 # Zweige biegen etwas weniger als Aeste
|
|
L(sbfac.outputs[0], sbamt.inputs[0])
|
|
sboff = N("ShaderNodeVectorMath"); sboff.location = (3480, 220)
|
|
sboff.operation = 'SCALE'
|
|
L(sbsub.outputs[0], sboff.inputs[0])
|
|
L(sbamt.outputs[0], _sock(sboff, "Scale"))
|
|
sbset = N("GeometryNodeSetPosition"); sbset.location = (3640, 520)
|
|
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 ----------
|
|
# 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
|
|
|
|
# ---------- 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"))
|
|
|
|
# ---------- 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"))
|
|
try:
|
|
_sock(c2m, "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"
|
|
|
|
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(stuv, "Geometry"), _sock(shade, "Geometry"))
|
|
L(_out(shade, "Geometry"), gout.inputs[0])
|
|
return ng
|
|
|
|
|
|
# Wachstums-Stufen: Faktoren/Werte fuer t = 0 (Setzling) .. 1 (ausgewachsen).
|
|
# Gleicher Seed -> dieselbe Baum-"Identitaet", nur juenger. Genau das braucht
|
|
# ein Wachstums-System, damit Stufe 1 und Stufe 4 wie DERSELBE Baum wirken.
|
|
GROWTH_YOUNG = {
|
|
"Height": 0.16, # Faktoren (werden mit dem Zielwert multipliziert)
|
|
"Trunk Radius": 0.30,
|
|
"Branch Length": 0.30,
|
|
"Branch Count": 0.35,
|
|
"Sub Count": 0.0, # Setzling hat noch keine Sub-Aeste
|
|
}
|
|
|
|
|
|
def growth_values(preset, t):
|
|
"""Parameter fuer Wachstums-Fortschritt t (0 = Setzling, 1 = ausgewachsen)."""
|
|
full = dict(DEFAULTS)
|
|
full.update(PRESETS.get(preset, {}))
|
|
out = dict(full)
|
|
t = max(0.0, min(1.0, t))
|
|
for key, young_factor in GROWTH_YOUNG.items():
|
|
if key not in full:
|
|
continue
|
|
target = full[key]
|
|
young = target * young_factor
|
|
v = young + (target - young) * t
|
|
out[key] = max(1, int(round(v))) if isinstance(target, int) and key != "Sub Count" \
|
|
else (int(round(v)) if isinstance(target, int) else v)
|
|
return out
|
|
|
|
|
|
def socket_ids(ng):
|
|
"""Name -> Modifier-Key ('Socket_3'). Fuer Presets/Skripting."""
|
|
out = {}
|
|
for s in ng.interface.items_tree:
|
|
if getattr(s, "in_out", "") == 'INPUT':
|
|
out[s.name] = s.identifier
|
|
return out
|
|
|
|
|
|
def make_object(ng, preset="baum", name=None, growth=None, seed=None):
|
|
name = name or OBJ_NAME
|
|
old = bpy.data.objects.get(name)
|
|
if old:
|
|
bpy.data.objects.remove(old, do_unlink=True)
|
|
me = bpy.data.meshes.new(name + "Mesh")
|
|
ob = bpy.data.objects.new(name, me)
|
|
bpy.context.scene.collection.objects.link(ob)
|
|
md = ob.modifiers.new("GN_Tree", 'NODES')
|
|
md.node_group = ng
|
|
|
|
ids = socket_ids(ng)
|
|
if growth is None:
|
|
values = dict(DEFAULTS)
|
|
values.update(PRESETS.get(preset, {}))
|
|
else:
|
|
values = growth_values(preset, growth)
|
|
if seed is not None:
|
|
values["Seed"] = seed
|
|
for k, v in values.items():
|
|
if k in ids:
|
|
md[ids[k]] = v
|
|
|
|
bpy.context.view_layer.objects.active = ob
|
|
return ob
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Addon-UI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
from bpy.props import (
|
|
IntProperty, FloatProperty, EnumProperty, BoolProperty, PointerProperty,
|
|
)
|
|
from bpy.types import Operator, Panel, PropertyGroup
|
|
|
|
|
|
def _preset_enum(self, context):
|
|
return [(k, k.capitalize(), "") for k in PRESETS]
|
|
|
|
|
|
class TreeGenSettings(PropertyGroup):
|
|
preset: EnumProperty(name="Preset", items=_preset_enum)
|
|
seed: IntProperty(name="Seed", default=0, min=0, max=9999)
|
|
use_growth: BoolProperty(
|
|
name="Wachstums-Stufen", default=False,
|
|
description="Erzeugt mehrere Stufen (Setzling .. ausgewachsen) mit gleichem Seed",
|
|
)
|
|
stages: IntProperty(name="Anzahl Stufen", default=4, min=2, max=8)
|
|
spacing: FloatProperty(name="Abstand", default=4.0, min=0.0, max=20.0)
|
|
apply_modifier: BoolProperty(
|
|
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"
|
|
bl_description = "Erzeugt einen parametrischen Baum (Regler danach am Modifier)"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
s = context.scene.tree_gen_settings
|
|
try:
|
|
ng = build_group()
|
|
except Exception as exc: # noqa: BLE001
|
|
self.report({'ERROR'}, "Node-Gruppe fehlgeschlagen: %s" % exc)
|
|
return {'CANCELLED'}
|
|
try:
|
|
ng.asset_mark()
|
|
ng.asset_data.description = "Parametrischer Stylized-Baum (Stamm + Aeste)"
|
|
except Exception:
|
|
pass
|
|
|
|
created = []
|
|
x = 0.0
|
|
try:
|
|
if s.use_growth:
|
|
for i in range(s.stages):
|
|
t = i / float(s.stages - 1) if s.stages > 1 else 1.0
|
|
ob = make_object(ng, preset=s.preset, growth=t, seed=s.seed,
|
|
name="%s_%s_stage%d" % (OBJ_NAME, s.preset, i))
|
|
_place(ob, x); x += s.spacing
|
|
created.append(ob)
|
|
else:
|
|
ob = make_object(ng, preset=s.preset, seed=s.seed,
|
|
name="%s_%s" % (OBJ_NAME, s.preset))
|
|
created.append(ob)
|
|
except Exception as exc: # noqa: BLE001
|
|
self.report({'ERROR'}, "Erzeugen fehlgeschlagen: %s" % exc)
|
|
return {'CANCELLED'}
|
|
|
|
if s.apply_modifier:
|
|
for ob in created:
|
|
context.view_layer.objects.active = ob
|
|
try:
|
|
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
|
|
dg = context.evaluated_depsgraph_get()
|
|
for ob in created:
|
|
try:
|
|
total += len(ob.evaluated_get(dg).data.vertices)
|
|
except Exception:
|
|
pass
|
|
self.report({'INFO'}, "%d Objekt(e) erzeugt, %d Verts gesamt." % (len(created), total))
|
|
return {'FINISHED'}
|
|
|
|
|
|
class VIEW3D_PT_tree_generator(Panel):
|
|
bl_label = "Tree Generator"
|
|
bl_idname = "VIEW3D_PT_tree_generator"
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = "Tree Gen"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
s = context.scene.tree_gen_settings
|
|
|
|
box = layout.box()
|
|
box.label(text="Vorlage", icon='PRESET')
|
|
box.prop(s, "preset", text="")
|
|
box.prop(s, "seed")
|
|
|
|
box = layout.box()
|
|
box.label(text="Wachstum")
|
|
box.prop(s, "use_growth")
|
|
sub = box.column(align=True)
|
|
sub.enabled = s.use_growth
|
|
sub.prop(s, "stages")
|
|
sub.prop(s, "spacing")
|
|
|
|
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')
|
|
layout.label(text="Feinjustierung: am Modifier 'GN_Tree'", icon='INFO')
|
|
|
|
|
|
classes = (
|
|
TreeGenSettings,
|
|
TREEGEN_OT_create,
|
|
VIEW3D_PT_tree_generator,
|
|
)
|
|
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.tree_gen_settings = PointerProperty(type=TreeGenSettings)
|
|
|
|
|
|
def unregister():
|
|
del bpy.types.Scene.tree_gen_settings
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|