Tree Gen 1.27.0: Blatt-Cards aus einer Collection (Punkt 3)

"Leaf Card" kann jetzt eine COLLECTION sein: je Blattpunkt wird zufaellig eine
Card gezogen (Pick Instance, seed-stabil). Damit legt der Nutzer 4-6
Diamond-Cards mit unterschiedlichen Atlas-UVs an und die Krone mischt sie.
Rueckwaertskompatibel - ein einzelnes Objekt geht weiter, die Sammlung hat
Vorrang.

Umsetzung: Collection Info mit Separate Children (sonst kaeme die Sammlung als
ein Klumpen an und Pick Instance haette nichts zum Auswaehlen), Geometry-Switch
zwischen Einzel- und Sammlungspfad, Instanz-Index aus einem Random Value, dessen
Obergrenze aus der TATSAECHLICHEN Anzahl kommt - eine feste Obergrenze wuerde
bei weniger Cards Luecken erzeugen. Geometry Nodes kann einen leeren
Collection-Zeiger nicht selbst pruefen, deshalb setzt make_leaves einen
"Use Collection"-Schalter.

Beweisfuehrung im Test: die Zufalls-Groesse wird ABGESCHALTET, dann kommt jede
Groessenabweichung nur von der Card-Variante und die Anzahl verschiedener
Face-Flaechen ist ein exakter Zaehler. Gemessen: Einzel-Card 2 Flaechen (ein
Card-Paar), Sammlung mit 4 Cards 10 Flaechen - alle Varianten kommen vor.
Gleicher Seed identisch, anderer Seed anders.

Nebenbei: die Vorpruefung des Blattwerk-Operators war eine zusammengesetzte
Bedingung, die bei gesetzter Sammlung ohne Einzel-Card auf None zugriff.

Neu: tests/test_blatt_varianten.py. 20 Tests bestehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 06:48:25 +02:00
co-authored by Claude Opus 4.8
parent 46b4e9ee34
commit 29899c0aa0
8 changed files with 263 additions and 18 deletions
+85 -10
View File
@@ -1,7 +1,7 @@
bl_info = {
"name": "Stylized Tree Generator",
"author": "D4rkst3r",
"version": (1, 26, 0),
"version": (1, 27, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Tree Gen",
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
@@ -1176,6 +1176,7 @@ LEAF_DEFAULTS = {
"Size Var": 0.35,
"Tilt": 0.5, # wie stark die Cards aus der Senkrechten kippen (rad)
"Seed": 0,
"Use Collection": False,
}
@@ -1211,6 +1212,11 @@ def build_leaf_group(rebuild=False):
add_in("Tree", 'NodeSocketObject')
add_in("Leaf Card", 'NodeSocketObject')
# Mehrere Atlas-Varianten: die Collection liefert je Punkt eine zufaellig
# gewaehlte Card. "Use Collection" wird von make_leaves gesetzt - Geometry
# Nodes kann einen leeren Collection-Zeiger nicht selbst pruefen.
add_in("Leaf Cards", 'NodeSocketCollection')
add_in("Use Collection", 'NodeSocketBool', False)
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)
@@ -1228,6 +1234,23 @@ def build_leaf_group(rebuild=False):
card = N("GeometryNodeObjectInfo"); card.location = (-700, -200)
L(V["Leaf Card"], _sock(card, "Object"))
# Separate Children = jedes Objekt der Collection wird eine EIGENE Instanz.
# Ohne das kaeme die Collection als ein einziger Klumpen an und "Pick
# Instance" haette nichts zum Auswaehlen.
coll = N("GeometryNodeCollectionInfo"); coll.location = (-700, -420)
L(V["Leaf Cards"], _sock(coll, "Collection"))
try:
_sock(coll, "Separate Children").default_value = True
_sock(coll, "Reset Children").default_value = True
except (AttributeError, KeyError):
pass
quelle = N("GeometryNodeSwitch"); quelle.location = (-500, -300)
quelle.input_type = 'GEOMETRY'
L(V["Use Collection"], _sock(quelle, "Switch"))
L(_out(card, "Geometry"), _sock(quelle, "False"))
L(_out(coll, "Instances", "Geometry"), _sock(quelle, "True"))
# Auswahl: nur duenne Aeste (uv_r < Max Radius)
na = N("GeometryNodeInputNamedAttribute"); na.location = (-700, 20)
na.data_type = 'FLOAT'
@@ -1272,7 +1295,35 @@ def build_leaf_group(rebuild=False):
inst = N("GeometryNodeInstanceOnPoints"); inst.location = (100, 200)
L(_out(dist, "Points"), _sock(inst, "Points"))
L(_out(card, "Geometry"), _sock(inst, "Instance"))
L(_out(quelle, "Output"), _sock(inst, "Instance"))
try:
L(V["Use Collection"], _sock(inst, "Pick Instance"))
except (KeyError, RuntimeError):
pass
# Zufaelliger Index je Punkt, aus der tatsaechlichen Anzahl abgeleitet -
# eine feste Obergrenze wuerde bei weniger Cards Luecken erzeugen.
dsize = N("GeometryNodeAttributeDomainSize"); dsize.location = (-320, -560)
try:
dsize.component = 'INSTANCES'
except (AttributeError, TypeError):
pass
L(_out(coll, "Instances", "Geometry"), _sock(dsize, "Geometry"))
dmax = N("ShaderNodeMath"); dmax.location = (-150, -560)
dmax.operation = 'SUBTRACT'; dmax.inputs[1].default_value = 1.0
try:
L(_out(dsize, "Instance Count", "Point Count"), dmax.inputs[0])
except (KeyError, RuntimeError):
dmax.inputs[0].default_value = 1.0
ridx = N("FunctionNodeRandomValue"); ridx.location = (100, -560)
ridx.data_type = 'INT'
L(V["Seed"], _sock(ridx, "Seed"))
ridx.inputs[4].default_value = 0
L(dmax.outputs[0], ridx.inputs[5])
try:
L(ridx.outputs["Value"], _sock(inst, "Instance Index"))
except (KeyError, RuntimeError):
pass
L(rrot.outputs["Value"], _sock(inst, "Rotation"))
L(rsc.outputs[1], _sock(inst, "Scale"))
@@ -1287,7 +1338,7 @@ def leaf_socket_ids(ng):
if getattr(so, "in_out", "") == 'INPUT'}
def make_leaves(context, tree_obj, card_obj, name=None, **overrides):
def make_leaves(context, tree_obj, card_obj, name=None, card_coll=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)
@@ -1306,6 +1357,10 @@ def make_leaves(context, tree_obj, card_obj, name=None, **overrides):
ids = leaf_socket_ids(ng)
md[ids["Tree"]] = tree_obj
md[ids["Leaf Card"]] = card_obj
# Rueckwaertskompatibel: ohne Collection bleibt alles wie bisher.
if "Leaf Cards" in ids:
md[ids["Leaf Cards"]] = card_coll
overrides.setdefault("Use Collection", card_coll is not None)
vals = dict(LEAF_DEFAULTS); vals.update(overrides)
for k, v in vals.items():
if k in ids:
@@ -1484,8 +1539,15 @@ class TreeGenSettings(PropertyGroup):
)
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"),
description=("Einzelne Card, die gestreut wird (z.B. dein 'Diamon "
"Plane'). Leer = _Leaf bleibt leer zum Selbermodellieren. "
"Wird ignoriert, sobald eine Card-Sammlung gesetzt ist"),
)
leaf_cards: PointerProperty(
name="Card-Sammlung", type=bpy.types.Collection,
description=("Sammlung mehrerer Cards mit unterschiedlichen Atlas-UVs. "
"Je Blattpunkt wird zufaellig eine gewaehlt (Seed-stabil) - "
"damit mischt die Krone 4-6 Varianten statt einer"),
)
leaf_density: FloatProperty(
name="Dichte", default=150.0, min=0.0, max=20000.0,
@@ -1755,8 +1817,13 @@ class TREEGEN_OT_leaves(Operator):
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).")
# Card-Sammlung hat Vorrang; ohne sie muss die Einzel-Card ein Mesh sein.
if s.leaf_cards is not None:
if not [o for o in s.leaf_cards.objects if o.type == 'MESH']:
self.report({'ERROR'}, "Die Card-Sammlung enthaelt keine Meshes.")
return {'CANCELLED'}
elif s.leaf_card is None or s.leaf_card.type != 'MESH':
self.report({'ERROR'}, "Bitte eine Blatt-Card oder eine Card-Sammlung waehlen.")
return {'CANCELLED'}
trees = [o for o in context.selected_objects
if o.type == 'MESH' and not o.name.endswith(("_Leaf", "_Frucht"))]
@@ -1768,6 +1835,7 @@ class TREEGEN_OT_leaves(Operator):
for t in trees:
try:
lf = make_leaves(context, t, s.leaf_card,
card_coll=s.leaf_cards,
Density=s.leaf_density, Size=s.leaf_size,
**{"Size Var": s.leaf_size_var,
"Max Radius": s.leaf_max_radius,
@@ -1969,9 +2037,12 @@ class VIEW3D_PT_tree_leaves(_TreeSub):
def draw(self, context):
layout = self.layout
s = context.scene.tree_gen_settings
layout.prop(s, "leaf_card")
layout.prop(s, "leaf_cards")
row = layout.row()
row.active = s.leaf_cards is None
row.prop(s, "leaf_card")
col = layout.column(align=True)
col.active = s.leaf_card is not None
col.active = s.leaf_card is not None or s.leaf_cards is not None
col.prop(s, "leaf_density")
col.prop(s, "leaf_size")
col.prop(s, "leaf_size_var")
@@ -1980,7 +2051,11 @@ class VIEW3D_PT_tree_leaves(_TreeSub):
col.prop(s, "leaf_budget")
layout.separator()
layout.operator("object.treegen_leaves", icon='OUTLINER_OB_POINTCLOUD')
if s.leaf_card is None:
if s.leaf_cards is not None:
n = len([o for o in s.leaf_cards.objects if o.type == 'MESH'])
layout.label(text="%d Card-Variante(n), zufaellig je Blatt" % n,
icon='INFO')
elif s.leaf_card is None:
layout.label(text="Ohne Card bleibt _Leaf leer", icon='INFO')