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:
+257
-3
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user