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:
@@ -58,6 +58,14 @@ Gruppe. Blaetter/Krone macht man selbst — das Addon liefert die Struktur.
|
|||||||
**gleicher Seed = dieselbe Baum-Identitaet** (fuer Wachstums-Systeme im Spiel).
|
**gleicher Seed = dieselbe Baum-Identitaet** (fuer Wachstums-Systeme im Spiel).
|
||||||
- **Live bearbeiten:** Nach dem Erzeugen liegen alle Regler am Modifier `GN_Tree` —
|
- **Live bearbeiten:** Nach dem Erzeugen liegen alle Regler am Modifier `GN_Tree` —
|
||||||
im Viewport ziehen, der Baum aktualisiert sich sofort.
|
im Viewport ziehen, der Baum aktualisiert sich sofort.
|
||||||
|
- **Blattwerk:** Ein Blatt-/Billboard-Objekt als *Blatt-Card* waehlen (z. B. eine
|
||||||
|
Diamond-Plane mit Alpha-Textur) → der Button streut es auf die **Zweige** und legt
|
||||||
|
das Ergebnis als `<Name>_Leaf` mit identischem Ursprung ab. Gefiltert wird ueber
|
||||||
|
den Astradius (`Max Radius`), damit keine Blaetter am Stamm kleben. Ohne Card
|
||||||
|
bleibt `_Leaf` leer zum Selbermodellieren.
|
||||||
|
- **Tri-Budget:** Der Blattwerk-Button meldet `Stamm + Blatt = Summe` und **warnt**
|
||||||
|
bei Ueberschreitung. Default-Dichte 150 haelt auch das schwerste Preset (Eiche,
|
||||||
|
1428 Tris) mit 2460 von 2500 Tris im Budget.
|
||||||
- **Export:** „Modifier anwenden" friert das Ergebnis als normales Mesh ein.
|
- **Export:** „Modifier anwenden" friert das Ergebnis als normales Mesh ein.
|
||||||
|
|
||||||
**v1.1.0:** Aeste folgen jetzt der Elternkurve (Tangent/Normal aus *Curve to
|
**v1.1.0:** Aeste folgen jetzt der Elternkurve (Tangent/Normal aus *Curve to
|
||||||
@@ -188,6 +196,7 @@ Weitere Suiten (jeweils `blender --background --python <datei>`):
|
|||||||
| `tests/test_ecogame_rock.py` | Fels-Benennung, Bake liefert nur `_albedo`/`_normal`, AO im Albedo |
|
| `tests/test_ecogame_rock.py` | Fels-Benennung, Bake liefert nur `_albedo`/`_normal`, AO im Albedo |
|
||||||
| `tests/test_ecogame_tree.py` | Baum-Benennung `<Art>_<Variante>_S<Stufe>`, Ursprung, Lightmap-Default |
|
| `tests/test_ecogame_tree.py` | Baum-Benennung `<Art>_<Variante>_S<Stufe>`, Ursprung, Lightmap-Default |
|
||||||
| `tests/test_randomize.py` | Zufalls-Funktion beider Addons, `_Leaf`/`_Frucht` mit gleichem Ursprung |
|
| `tests/test_randomize.py` | Zufalls-Funktion beider Addons, `_Leaf`/`_Frucht` mit gleichem Ursprung |
|
||||||
|
| `tests/test_leaves.py` | Blattwerk: Streuung, Ursprung, Tri-Budget, leeres `_Leaf` ohne Card |
|
||||||
|
|
||||||
Die Suiten erwarten die gebauten Zips in `dist/` bzw. lesen die `.py` direkt.
|
Die Suiten erwarten die gebauten Zips in `dist/` bzw. lesen die `.py` direkt.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "stylized_tree_generator"
|
id = "stylized_tree_generator"
|
||||||
version = "1.22.0"
|
version = "1.23.0"
|
||||||
name = "Stylized Tree Generator"
|
name = "Stylized Tree Generator"
|
||||||
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
|
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
|
||||||
maintainer = "D4rkst3r"
|
maintainer = "D4rkst3r"
|
||||||
|
|||||||
Vendored
+4
-4
@@ -32,7 +32,7 @@
|
|||||||
"id": "stylized_tree_generator",
|
"id": "stylized_tree_generator",
|
||||||
"name": "Stylized Tree Generator",
|
"name": "Stylized Tree Generator",
|
||||||
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
|
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
|
||||||
"version": "1.22.0",
|
"version": "1.23.0",
|
||||||
"type": "add-on",
|
"type": "add-on",
|
||||||
"maintainer": "D4rkst3r",
|
"maintainer": "D4rkst3r",
|
||||||
"license": [
|
"license": [
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
"Mesh",
|
"Mesh",
|
||||||
"Modeling"
|
"Modeling"
|
||||||
],
|
],
|
||||||
"archive_url": "./stylized_tree_generator-1.22.0.zip",
|
"archive_url": "./stylized_tree_generator-1.23.0.zip",
|
||||||
"archive_size": 19331,
|
"archive_size": 22135,
|
||||||
"archive_hash": "sha256:5e5e3b5e0b73aabe416f93c27bbef4772b561122762dd52daf836f10c0e45b35"
|
"archive_hash": "sha256:a9b986aaf5e95f7a9600d529eb3eb305af3d1edc6b327862035cecde50fed6e9"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+256
-2
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Stylized Tree Generator",
|
"name": "Stylized Tree Generator",
|
||||||
"author": "D4rkst3r",
|
"author": "D4rkst3r",
|
||||||
"version": (1, 22, 0),
|
"version": (1, 23, 0),
|
||||||
"blender": (4, 2, 0),
|
"blender": (4, 2, 0),
|
||||||
"location": "View3D > Sidebar > Tree Gen",
|
"location": "View3D > Sidebar > Tree Gen",
|
||||||
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
"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
|
# Addon-UI
|
||||||
@@ -1091,11 +1244,34 @@ class TreeGenSettings(PropertyGroup):
|
|||||||
"1 = volle Bandbreite. Der Preset-Charakter bleibt erhalten"),
|
"1 = volle Bandbreite. Der Preset-Charakter bleibt erhalten"),
|
||||||
)
|
)
|
||||||
make_leaf: BoolProperty(
|
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 "
|
description=("Legt pro Baum ein leeres Objekt <Name>_Leaf mit GLEICHEM Ursprung "
|
||||||
"an. Dort modellierst du die Krone - in UE sitzen beide Ebenen "
|
"an. Dort modellierst du die Krone - in UE sitzen beide Ebenen "
|
||||||
"dadurch deckungsgleich"),
|
"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(
|
make_frucht: BoolProperty(
|
||||||
name="_Frucht-Ebene anlegen", default=False,
|
name="_Frucht-Ebene anlegen", default=False,
|
||||||
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
|
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
|
||||||
@@ -1185,6 +1361,58 @@ class TREEGEN_OT_randomize(Operator):
|
|||||||
return {'FINISHED'}
|
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):
|
class TREEGEN_OT_create(Operator):
|
||||||
bl_idname = "object.treegen_create"
|
bl_idname = "object.treegen_create"
|
||||||
bl_label = "Baum erzeugen"
|
bl_label = "Baum erzeugen"
|
||||||
@@ -1262,6 +1490,16 @@ class TREEGEN_OT_create(Operator):
|
|||||||
extra = []
|
extra = []
|
||||||
for ob in created:
|
for ob in created:
|
||||||
if s.make_leaf:
|
if s.make_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"))
|
extra.append(_add_layer(context, ob, "_Leaf"))
|
||||||
if s.make_frucht:
|
if s.make_frucht:
|
||||||
extra.append(_add_layer(context, ob, "_Frucht"))
|
extra.append(_add_layer(context, ob, "_Frucht"))
|
||||||
@@ -1314,6 +1552,21 @@ class VIEW3D_PT_tree_generator(Panel):
|
|||||||
box.prop(s, "random_amount")
|
box.prop(s, "random_amount")
|
||||||
box.operator("object.treegen_randomize", icon='FILE_REFRESH')
|
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 = layout.box()
|
||||||
box.label(text="Zusatz-Ebenen")
|
box.label(text="Zusatz-Ebenen")
|
||||||
box.prop(s, "make_leaf")
|
box.prop(s, "make_leaf")
|
||||||
@@ -1334,6 +1587,7 @@ class VIEW3D_PT_tree_generator(Panel):
|
|||||||
classes = (
|
classes = (
|
||||||
TreeGenSettings,
|
TreeGenSettings,
|
||||||
TREEGEN_OT_randomize,
|
TREEGEN_OT_randomize,
|
||||||
|
TREEGEN_OT_leaves,
|
||||||
TREEGEN_OT_create,
|
TREEGEN_OT_create,
|
||||||
VIEW3D_PT_tree_generator,
|
VIEW3D_PT_tree_generator,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import bpy, sys, zipfile, os, tempfile, importlib.util, math, bmesh
|
||||||
|
ZIP=r"A:\eco\stylized_rock_generator\dist\stylized_tree_generator-1.23.0.zip"
|
||||||
|
tmp=tempfile.mkdtemp()
|
||||||
|
with zipfile.ZipFile(ZIP) as z: z.extractall(tmp)
|
||||||
|
init=[os.path.join(r,"__init__.py") for r,_,f in os.walk(tmp) if "__init__.py" in f][0]
|
||||||
|
spec=importlib.util.spec_from_file_location("stg",init)
|
||||||
|
m=importlib.util.module_from_spec(spec); sys.modules["stg"]=m; spec.loader.exec_module(m)
|
||||||
|
try: m.unregister()
|
||||||
|
except Exception: pass
|
||||||
|
m.register()
|
||||||
|
fails=[]
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
|
||||||
|
# Card wie beim User
|
||||||
|
me=bpy.data.meshes.new("DiamondCard"); bm=bmesh.new()
|
||||||
|
for ang in (0.0, math.pi/3, 2*math.pi/3):
|
||||||
|
c,s_=math.cos(ang), math.sin(ang)
|
||||||
|
vs=[bm.verts.new(p) for p in [(-c,-s_,-1.41),(c,s_,-1.41),(c,s_,1.41),(-c,-s_,1.41)]]
|
||||||
|
bm.faces.new(vs)
|
||||||
|
bm.to_mesh(me); bm.free(); me.uv_layers.new(name="UVMap")
|
||||||
|
card=bpy.data.objects.new("Diamon Plane", me); bpy.context.collection.objects.link(card)
|
||||||
|
|
||||||
|
s=bpy.context.scene.tree_gen_settings
|
||||||
|
s.preset='eiche'; s.art_name="Eiche"; s.variant=1
|
||||||
|
s.leaf_card=card; s.leaf_size=0.22 # Dichte: Default verwenden
|
||||||
|
s.make_leaf=True; s.use_growth=False
|
||||||
|
r=bpy.ops.object.treegen_create()
|
||||||
|
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
|
||||||
|
print("OBJEKTE:", [n for n in names if n!="Diamon Plane"])
|
||||||
|
if "Eiche_01" not in names or "Eiche_01_Leaf" not in names: fails.append("objekte")
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
dg=bpy.context.evaluated_depsgraph_get()
|
||||||
|
def tris(n):
|
||||||
|
ob=bpy.context.scene.objects[n]
|
||||||
|
return sum(len(p.vertices)-2 for p in ob.evaluated_get(dg).data.polygons)
|
||||||
|
tt,tl=tris("Eiche_01"), tris("Eiche_01_Leaf")
|
||||||
|
print("TRIS Stamm=%d Blatt=%d Summe=%d / 2500 %s (Dichte %.0f)" % (tt,tl,tt+tl,"OK" if tt+tl<=2500 else "UEBER", s.leaf_density))
|
||||||
|
if tl==0: fails.append("blattwerk_leer")
|
||||||
|
if tt+tl>2500: fails.append("budget")
|
||||||
|
# Ursprung deckungsgleich
|
||||||
|
a=bpy.context.scene.objects["Eiche_01"]; b=bpy.context.scene.objects["Eiche_01_Leaf"]
|
||||||
|
d=(a.location-b.location).length
|
||||||
|
print("URSPRUNG-Abstand: %.6f" % d)
|
||||||
|
if d>1e-6: fails.append("ursprung")
|
||||||
|
# Operator auf Auswahl
|
||||||
|
for o in bpy.context.selected_objects: o.select_set(False)
|
||||||
|
a.select_set(True); bpy.context.view_layer.objects.active=a
|
||||||
|
r2=bpy.ops.object.treegen_leaves()
|
||||||
|
print("OPERATOR treegen_leaves:", list(r2))
|
||||||
|
if 'FINISHED' not in r2: fails.append("operator")
|
||||||
|
# Ohne Card -> leeres _Leaf
|
||||||
|
for o in list(bpy.data.objects):
|
||||||
|
if o.name.startswith("Eiche"): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
s.leaf_card=None; s.art_name="Birke"; s.preset='birke'
|
||||||
|
bpy.ops.object.treegen_create()
|
||||||
|
lf=bpy.context.scene.objects.get("Birke_01_Leaf")
|
||||||
|
print("ohne Card -> _Leaf vorhanden:", lf is not None, "| leer:", lf and len(lf.data.polygons)==0)
|
||||||
|
if lf is None: fails.append("leer_leaf")
|
||||||
|
m.unregister()
|
||||||
|
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
Reference in New Issue
Block a user