Tree Generator v1.23.0: Blattwerk auf die Zweige streuen

Blatt-Card waehlen -> wird auf die duennen Zweige gestreut (Filter ueber das
Radius-Attribut des Meshes) und als <Name>_Leaf mit identischem Ursprung abgelegt.
Regler fuer Dichte, Groesse, Streuung, Kippung, Zweig-Schwelle. Der Button meldet
Stamm + Blatt = Summe und warnt bei Ueberschreitung des Tri-Budgets.

Gemessen: Eiche 1428 + Blattwerk 1032 = 2460 von 2500 Tris bei Default-Dichte 150.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 11:42:52 +02:00
co-authored by Claude Opus 4.8
parent 1112797138
commit 833e929f82
7 changed files with 332 additions and 8 deletions
+257 -3
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 22, 0),
"version": (1, 23, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -1051,6 +1051,159 @@ def make_object(ng, preset="baum", name=None, growth=None, seed=None):
# ---------------------------------------------------------------------------
# Blattwerk: Billboard-Cards auf die duennen Zweige streuen
# ---------------------------------------------------------------------------
LEAF_GROUP_NAME = "GN_TreeLeaves"
LEAF_DEFAULTS = {
"Density": 300.0, # Punkte pro m2 Zweigoberflaeche. Gemessen mit der
# 6-Tri-Diamond-Card: 300 -> ~180 Cards / 1092 Tris,
# zusammen mit dem Baum 2228 von 2500 Tris. 900 waere
# mit 4118 Tris deutlich ueber Budget.
"Max Radius": 0.02, # nur wo der Ast duenner ist als das (m) -> Zweige
"Size": 0.35,
"Size Var": 0.35,
"Tilt": 0.5, # wie stark die Cards aus der Senkrechten kippen (rad)
"Seed": 0,
}
def build_leaf_group(rebuild=False):
"""Node-Gruppe, die eine Blatt-Card auf die ZWEIGE eines Baumes streut.
Der Trick: der Generator legt den Astradius als Attribut "uv_r" am Mesh ab
(gemessen 0.8 mm bis 178 mm). Damit lassen sich Blaetter gezielt nur dort
setzen, wo der Ast duenn ist - sonst klebten sie auch am Stamm.
WICHTIG: eine vorhandene Gruppe wird WIEDERVERWENDET. Sie bei jedem Aufruf
zu loeschen und neu zu bauen reisst sie allen bereits erzeugten _Leaf-
Objekten unter dem Modifier weg - die standen dann ohne Blattwerk da.
"""
existing = bpy.data.node_groups.get(LEAF_GROUP_NAME)
if existing is not None and not rebuild:
return existing
if existing is not None:
bpy.data.node_groups.remove(existing)
ng = bpy.data.node_groups.new(LEAF_GROUP_NAME, "GeometryNodeTree")
N, L = ng.nodes.new, ng.links.new
iface = ng.interface
iface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
def add_in(name, stype, default=None, mn=None, mx=None):
so = iface.new_socket(name, in_out='INPUT', socket_type=stype)
if default is not None:
so.default_value = default
if mn is not None:
so.min_value = mn
if mx is not None:
so.max_value = mx
add_in("Tree", 'NodeSocketObject')
add_in("Leaf Card", 'NodeSocketObject')
add_in("Density", 'NodeSocketFloat', LEAF_DEFAULTS["Density"], 0.0, 20000.0)
add_in("Max Radius", 'NodeSocketFloat', LEAF_DEFAULTS["Max Radius"], 0.001, 0.5)
add_in("Size", 'NodeSocketFloat', LEAF_DEFAULTS["Size"], 0.01, 5.0)
add_in("Size Var", 'NodeSocketFloat', LEAF_DEFAULTS["Size Var"], 0.0, 1.0)
add_in("Tilt", 'NodeSocketFloat', LEAF_DEFAULTS["Tilt"], 0.0, 3.14159)
add_in("Seed", 'NodeSocketInt', LEAF_DEFAULTS["Seed"], 0, 9999)
gin = N("NodeGroupInput"); gin.location = (-900, 0)
gout = N("NodeGroupOutput"); gout.location = (900, 0)
V = {so.name: gin.outputs[so.name] for so in iface.items_tree
if getattr(so, "in_out", "") == 'INPUT'}
tree = N("GeometryNodeObjectInfo"); tree.location = (-700, 200)
L(V["Tree"], _sock(tree, "Object"))
card = N("GeometryNodeObjectInfo"); card.location = (-700, -200)
L(V["Leaf Card"], _sock(card, "Object"))
# Auswahl: nur duenne Aeste (uv_r < Max Radius)
na = N("GeometryNodeInputNamedAttribute"); na.location = (-700, 20)
na.data_type = 'FLOAT'
_sock(na, "Name").default_value = "uv_r"
cmp = N("FunctionNodeCompare"); cmp.location = (-520, 20)
cmp.data_type = 'FLOAT'; cmp.operation = 'LESS_THAN'
L(_out(na, "Attribute"), cmp.inputs[0])
L(V["Max Radius"], cmp.inputs[1])
dist = N("GeometryNodeDistributePointsOnFaces"); dist.location = (-320, 200)
L(_out(tree, "Geometry"), _sock(dist, "Mesh"))
L(cmp.outputs[0], _sock(dist, "Selection"))
L(V["Density"], _sock(dist, "Density"))
L(V["Seed"], _sock(dist, "Seed"))
# Zufaellige Drehung: voll um Z, leicht gekippt (Tilt)
rrot = N("FunctionNodeRandomValue"); rrot.location = (-320, -120)
rrot.data_type = 'FLOAT_VECTOR'
L(V["Seed"], _sock(rrot, "Seed"))
tneg = N("ShaderNodeMath"); tneg.location = (-520, -220); tneg.operation = 'MULTIPLY'
tneg.inputs[1].default_value = -1.0
L(V["Tilt"], tneg.inputs[0])
vmin = N("ShaderNodeCombineXYZ"); vmin.location = (-520, -320)
L(tneg.outputs[0], vmin.inputs["X"]); L(tneg.outputs[0], vmin.inputs["Y"])
vmax = N("ShaderNodeCombineXYZ"); vmax.location = (-520, -420)
L(V["Tilt"], vmax.inputs["X"]); L(V["Tilt"], vmax.inputs["Y"])
vmax.inputs["Z"].default_value = 6.283185
L(vmin.outputs[0], rrot.inputs[0])
L(vmax.outputs[0], rrot.inputs[1])
# Zufaellige Groesse
rsc = N("FunctionNodeRandomValue"); rsc.location = (-320, -340)
rsc.data_type = 'FLOAT'
L(V["Seed"], _sock(rsc, "Seed"))
lo = N("ShaderNodeMath"); lo.location = (-520, -520); lo.operation = 'SUBTRACT'
lo.inputs[0].default_value = 1.0
L(V["Size Var"], lo.inputs[1])
slo = N("ShaderNodeMath"); slo.location = (-370, -520); slo.operation = 'MULTIPLY'
L(V["Size"], slo.inputs[0]); L(lo.outputs[0], slo.inputs[1])
L(slo.outputs[0], rsc.inputs[2])
L(V["Size"], rsc.inputs[3])
inst = N("GeometryNodeInstanceOnPoints"); inst.location = (100, 200)
L(_out(dist, "Points"), _sock(inst, "Points"))
L(_out(card, "Geometry"), _sock(inst, "Instance"))
L(rrot.outputs["Value"], _sock(inst, "Rotation"))
L(rsc.outputs[1], _sock(inst, "Scale"))
real = N("GeometryNodeRealizeInstances"); real.location = (400, 200)
L(_out(inst, "Instances", "Geometry"), _sock(real, "Geometry"))
L(_out(real, "Geometry"), gout.inputs[0])
return ng
def leaf_socket_ids(ng):
return {so.name: so.identifier for so in ng.interface.items_tree
if getattr(so, "in_out", "") == 'INPUT'}
def make_leaves(context, tree_obj, card_obj, name=None, **overrides):
"""Erzeugt/aktualisiert ein <Baum>_Leaf-Objekt mit dem Blattwerk-Modifier."""
name = name or (tree_obj.name + "_Leaf")
ob = bpy.data.objects.get(name)
if ob is None:
me = bpy.data.meshes.new(name)
ob = bpy.data.objects.new(name, me)
context.collection.objects.link(ob)
# Gleicher Ursprung wie der Stamm - in UE muessen die Ebenen deckungsgleich sein
ob.location = tree_obj.location.copy()
ob.rotation_euler = tree_obj.rotation_euler.copy()
ob.scale = tree_obj.scale.copy()
ng = build_leaf_group()
md = ob.modifiers.get("GN_Leaves") or ob.modifiers.new("GN_Leaves", 'NODES')
md.node_group = ng
ids = leaf_socket_ids(ng)
md[ids["Tree"]] = tree_obj
md[ids["Leaf Card"]] = card_obj
vals = dict(LEAF_DEFAULTS); vals.update(overrides)
for k, v in vals.items():
if k in ids:
md[ids[k]] = v
return ob
# ---------------------------------------------------------------------------
# Addon-UI
@@ -1091,11 +1244,34 @@ class TreeGenSettings(PropertyGroup):
"1 = volle Bandbreite. Der Preset-Charakter bleibt erhalten"),
)
make_leaf: BoolProperty(
name="_Leaf-Ebene anlegen", default=False,
name="_Leaf-Ebene anlegen", default=True,
description=("Legt pro Baum ein leeres Objekt <Name>_Leaf mit GLEICHEM Ursprung "
"an. Dort modellierst du die Krone - in UE sitzen beide Ebenen "
"dadurch deckungsgleich"),
)
leaf_card: PointerProperty(
name="Blatt-Card", type=bpy.types.Object,
description=("Objekt, das als Blatt/Billboard gestreut wird (z.B. dein "
"'Diamon Plane'). Leer = _Leaf bleibt leer zum Selbermodellieren"),
)
leaf_density: FloatProperty(
name="Dichte", default=150.0, min=0.0, max=20000.0,
description=("Cards pro m2 Zweigoberflaeche. Presets mit viel Geaest (Eiche) "
"bekommen dadurch automatisch mehr Blaetter als schlanke. 150 haelt "
"auch die Eiche im 2500-Tri-Budget; der Button meldet die Summe"),
)
leaf_budget: IntProperty(
name="Tri-Budget", default=2500, min=0, max=100000,
description="Bei Ueberschreitung warnt der Blattwerk-Button (0 = keine Pruefung)",
)
leaf_size: FloatProperty(name="Groesse", default=0.22, min=0.01, max=5.0)
leaf_size_var: FloatProperty(name="Groessen-Streuung", default=0.35, min=0.0, max=1.0)
leaf_max_radius: FloatProperty(
name="Nur Zweige duenner als", default=0.02, min=0.001, max=0.5,
description=("Blaetter nur dort, wo der Ast duenner als dieser Radius ist (m). "
"Verhindert Blaetter am Stamm"),
)
leaf_tilt: FloatProperty(name="Kippung", default=0.5, min=0.0, max=3.14159)
make_frucht: BoolProperty(
name="_Frucht-Ebene anlegen", default=False,
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
@@ -1185,6 +1361,58 @@ class TREEGEN_OT_randomize(Operator):
return {'FINISHED'}
class TREEGEN_OT_leaves(Operator):
bl_idname = "object.treegen_leaves"
bl_label = "Blattwerk erzeugen"
bl_description = ("Streut die Blatt-Card auf die ZWEIGE der ausgewaehlten Baeume "
"und legt sie als <Name>_Leaf ab")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.tree_gen_settings
if s.leaf_card is None or s.leaf_card.type != 'MESH':
self.report({'ERROR'}, "Bitte zuerst eine Blatt-Card waehlen (Mesh-Objekt).")
return {'CANCELLED'}
trees = [o for o in context.selected_objects
if o.type == 'MESH' and not o.name.endswith(("_Leaf", "_Frucht"))]
if not trees:
self.report({'ERROR'}, "Keinen Baum ausgewaehlt.")
return {'CANCELLED'}
dg = context.evaluated_depsgraph_get()
total_tree = total_leaf = 0
for t in trees:
try:
lf = make_leaves(context, t, s.leaf_card,
Density=s.leaf_density, Size=s.leaf_size,
**{"Size Var": s.leaf_size_var,
"Max Radius": s.leaf_max_radius,
"Tilt": s.leaf_tilt, "Seed": s.seed})
except Exception as exc: # noqa: BLE001
self.report({'WARNING'}, "%s: %s" % (t.name, exc))
continue
context.view_layer.update()
dg = context.evaluated_depsgraph_get()
for ob, acc in ((t, "tree"), (lf, "leaf")):
try:
n = sum(len(p.vertices) - 2
for p in ob.evaluated_get(dg).data.polygons)
except Exception:
n = 0
if acc == "tree":
total_tree += n
else:
total_leaf += n
total = total_tree + total_leaf
msg = ("Blattwerk fuer %d Baum/Baeume: %d Stamm + %d Blatt = %d Tris"
% (len(trees), total_tree, total_leaf, total))
limit = s.leaf_budget * max(len(trees), 1)
if s.leaf_budget and total > limit:
self.report({'WARNING'}, msg + " - UEBER Budget (%d). Dichte senken." % limit)
else:
self.report({'INFO'}, msg)
return {'FINISHED'}
class TREEGEN_OT_create(Operator):
bl_idname = "object.treegen_create"
bl_label = "Baum erzeugen"
@@ -1262,7 +1490,17 @@ class TREEGEN_OT_create(Operator):
extra = []
for ob in created:
if s.make_leaf:
extra.append(_add_layer(context, ob, "_Leaf"))
if s.leaf_card is not None and s.leaf_card.type == 'MESH':
# Card gesetzt -> Blattwerk direkt streuen
extra.append(make_leaves(
context, ob, s.leaf_card,
Density=s.leaf_density, Size=s.leaf_size,
**{"Size Var": s.leaf_size_var,
"Max Radius": s.leaf_max_radius,
"Tilt": s.leaf_tilt, "Seed": s.seed}))
else:
# keine Card -> leeres _Leaf zum Selbermodellieren
extra.append(_add_layer(context, ob, "_Leaf"))
if s.make_frucht:
extra.append(_add_layer(context, ob, "_Frucht"))
created.extend(extra)
@@ -1314,6 +1552,21 @@ class VIEW3D_PT_tree_generator(Panel):
box.prop(s, "random_amount")
box.operator("object.treegen_randomize", icon='FILE_REFRESH')
box = layout.box()
box.label(text="Blattwerk", icon='OUTLINER_OB_POINTCLOUD')
box.prop(s, "leaf_card")
sub = box.column(align=True)
sub.enabled = s.leaf_card is not None
sub.prop(s, "leaf_density")
sub.prop(s, "leaf_size")
sub.prop(s, "leaf_size_var")
sub.prop(s, "leaf_max_radius")
sub.prop(s, "leaf_tilt")
sub.prop(s, "leaf_budget")
box.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD')
if s.leaf_card is None:
box.label(text="Ohne Card bleibt _Leaf leer", icon='INFO')
box = layout.box()
box.label(text="Zusatz-Ebenen")
box.prop(s, "make_leaf")
@@ -1334,6 +1587,7 @@ class VIEW3D_PT_tree_generator(Panel):
classes = (
TreeGenSettings,
TREEGEN_OT_randomize,
TREEGEN_OT_leaves,
TREEGEN_OT_create,
VIEW3D_PT_tree_generator,
)