Tree Gen 1.42.0: UCX-Stammhuelle im Spielfertig-Export
Die Stand-Baeume bekommen eine konvexe Kollisionshuelle um den STAMM, vom Fuss bis zum Kronenansatz - keine Kronen-Huelle: gegen die liefe der Spieler, bevor er den Baum beruehrt. Die Trennung kommt aus ast_id (0 = Stamm), dasselbe Attribut wie beim Ursprung und bei den Faell-Teilen. Kronenansatz = tiefster Punkt mit ast_id > 0 plus ein Zehntel Stammhoehe Luft (der tiefste Astpunkt kann eine herabgebogene Spitze sein). Ohne Aeste ist die ganze Pflanze Stamm - beim Kaktus genau richtig. Name und Hierarchie sind NICHT geraten, sondern am Fels-Export nachgemessen, der in der Pipeline schon abgenommen ist: UCX_<LODGroup-Name>, als GESCHWISTER des LodGroup-Empty. Ein Kind der Gruppe wuerde Unreal als LOD-Stufe lesen. Zwei Befunde aus der Messung: - Die Huelle rutschte 4-6 cm UNTER den Boden. Das Vereinfachen auf 12 Verts schneidet Ecken ab, das anschliessende Aufblasen skaliert um die Huellenmitte und schiebt dabei die Unterseite nach unten. Im Spiel waere das eine Kollision, die aus dem Terrain ragt. Boden wird jetzt auf Z = 0 geklemmt. - Der erste Anlauf des Tests holte die FBX-Skala aus der Gesamthoehe - die Blatt-Cards ragen ueber die Rinde hinaus, die Skala kam zu gross heraus und die Huelle sah zu klein aus. Massstab kommt jetzt aus Material-Slot 0. Neu: tests/test_stamm_ucx.py, vier Faelle (gerader Stamm, krummer Stamm mit Bend 1.5, Obstbaum, einmal ohne LODs), gemessen im reimportierten FBX. Gegenprobe ohne Boden-Klemmung: vier Fehler. test_baum_export, test_baum_lod und test_ursprung zaehlten die Huelle als Render-Mesh mit und schlugen zu Recht an - sie filtern jetzt UCX_. Schalter im Export-Panel: "UCX-Stammhuelle" (an) + max. Verts (12). 27 Tests bestehen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+104
-6
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "Stylized Tree Generator",
|
||||
"author": "D4rkst3r",
|
||||
"version": (1, 41, 0),
|
||||
"version": (1, 42, 0),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "View3D > Sidebar > Tree Gen",
|
||||
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
||||
@@ -2177,17 +2177,25 @@ def _merge_for_export(context, tree_ob, leaf_ob, name, bark_decimate=1.0):
|
||||
# Faell-Teile: Stammsegmente und Aeste als eigene Physik-Assets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _tree_ucx(quell_objs, name, max_verts=32):
|
||||
def _tree_ucx(quell_objs, name, max_verts=32, punkte=None):
|
||||
"""Konvexe Huelle als `UCX_<Name>` - gleiches Muster wie bei den Felsen.
|
||||
|
||||
Nur die PUNKTE einlesen: mit `from_mesh` kaemen die Originalflaechen mit und
|
||||
die Huelle laege obendrauf statt darum herum.
|
||||
|
||||
`punkte` uebergeht `quell_objs` und huellt genau die gegebenen Koordinaten -
|
||||
dafuer, dass die Stand-Baum-Huelle nur den STAMM umschliesst und nicht die
|
||||
Krone.
|
||||
"""
|
||||
bm = bmesh.new()
|
||||
for o in quell_objs:
|
||||
if o.type == 'MESH':
|
||||
for v in o.data.vertices:
|
||||
bm.verts.new(o.matrix_world @ v.co)
|
||||
if punkte is not None:
|
||||
for co in punkte:
|
||||
bm.verts.new(co)
|
||||
else:
|
||||
for o in quell_objs:
|
||||
if o.type == 'MESH':
|
||||
for v in o.data.vertices:
|
||||
bm.verts.new(o.matrix_world @ v.co)
|
||||
if len(bm.verts) < 4:
|
||||
bm.free()
|
||||
return None
|
||||
@@ -2249,6 +2257,49 @@ def _tree_ucx(quell_objs, name, max_verts=32):
|
||||
return ob
|
||||
|
||||
|
||||
def _stamm_punkte(tree_obj, matrix):
|
||||
"""Stamm-Koordinaten vom Fuss bis zum KRONENANSATZ, in Weltkoordinaten.
|
||||
|
||||
Fuer die Kollision des Stand-Baums. Bewusst nur der Stamm: eine Huelle um
|
||||
die ganze Krone waere ein Zylinder von mehreren Metern Durchmesser, gegen
|
||||
den der Spieler laeuft, bevor er den Baum ueberhaupt beruehrt.
|
||||
|
||||
Die Trennung kommt aus `ast_id` (0 = Stamm) - dasselbe Attribut wie beim
|
||||
Ursprung und bei den Faell-Teilen. Der Kronenansatz ist der tiefste Punkt
|
||||
mit ast_id > 0; oberhalb davon steckt der Stamm ohnehin in der Krone.
|
||||
|
||||
Ohne Aeste (Kaktus-Setzling: `Branch Count 0` schaltet den Ast-Zweig des
|
||||
Graphen ab, dann gibt es gar kein ast_id) ist die ganze Pflanze Stamm - und
|
||||
genau die soll dann auch die Kollision sein.
|
||||
"""
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
ev = tree_obj.evaluated_get(dg)
|
||||
me = ev.to_mesh()
|
||||
attr = me.attributes.get("ast_id")
|
||||
hat_id = (attr is not None and attr.domain == 'POINT'
|
||||
and len(attr.data) == len(me.vertices))
|
||||
stamm, aeste = [], []
|
||||
for i, v in enumerate(me.vertices):
|
||||
co = matrix @ v.co
|
||||
if hat_id and attr.data[i].value > 0:
|
||||
aeste.append(co)
|
||||
else:
|
||||
stamm.append(co)
|
||||
ev.to_mesh_clear()
|
||||
if not stamm:
|
||||
return []
|
||||
if aeste:
|
||||
# Etwas Luft ueber den Ansatz: der tiefste Astpunkt kann eine
|
||||
# herabgebogene Spitze sein, dann waere die Huelle zu kurz. Ein Zehntel
|
||||
# der Stammhoehe reicht und bleibt weit unter der Krone.
|
||||
zs = [c.z for c in stamm]
|
||||
grenze = min(aeste, key=lambda c: c.z).z + (max(zs) - min(zs)) * 0.1
|
||||
gekuerzt = [c for c in stamm if c.z <= grenze]
|
||||
if len(gekuerzt) >= 8:
|
||||
return gekuerzt
|
||||
return stamm
|
||||
|
||||
|
||||
def _mass_center(me):
|
||||
"""Massenmittelpunkt als flaechengewichteter Schwerpunkt der Faces.
|
||||
|
||||
@@ -2485,6 +2536,17 @@ class TreeGenSettings(PropertyGroup):
|
||||
name="_Frucht-Ebene anlegen", default=False,
|
||||
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
|
||||
)
|
||||
stand_ucx: BoolProperty(
|
||||
name="UCX-Stammhuelle", default=True,
|
||||
description=("Legt dem spielfertigen Baum eine konvexe Huelle um den "
|
||||
"STAMM (Fuss bis Kronenansatz) ins FBX. Bewusst keine "
|
||||
"Kronen-Huelle: gegen die liefe der Spieler, bevor er den "
|
||||
"Baum beruehrt"),
|
||||
)
|
||||
stand_ucx_verts: IntProperty(
|
||||
name="Stammhuelle max. Verts", default=12, min=8, max=32,
|
||||
description="Ein Stamm braucht keine feine Huelle - 8-12 reichen",
|
||||
)
|
||||
piece_trunk_segments: IntProperty(
|
||||
name="Stamm-Segmente", default=3, min=2, max=3,
|
||||
description="In wie viele Stuecke der Stamm beim Faellen zerfaellt",
|
||||
@@ -2790,6 +2852,33 @@ class TREEGEN_OT_export_game(Operator):
|
||||
weitere.matrix_parent_inverse = Matrix.Identity(4)
|
||||
weitere.data.transform(versatz)
|
||||
|
||||
# UCX-Stammhuelle. Die Punkte kommen aus dem QUELLBAUM, weil nur
|
||||
# der noch `ast_id` traegt - die Export-Kopie ist verschmolzen
|
||||
# und bei LOD1-3 zusaetzlich dezimiert. Sie durchlaufen dieselbe
|
||||
# Kette wie die Stufen: eigene Weltmatrix, dann die gemeinsame
|
||||
# Zentrierung `versatz`.
|
||||
stamm_ucx_name = (basis_name + "_LODGroup") if s.generate_lods \
|
||||
else basis_name
|
||||
hull = None
|
||||
if s.stand_ucx:
|
||||
pts = _stamm_punkte(baum, baum.matrix_world)
|
||||
pts = [versatz @ c for c in pts]
|
||||
hull = _tree_ucx(None, stamm_ucx_name,
|
||||
max_verts=s.stand_ucx_verts, punkte=pts)
|
||||
if hull is not None:
|
||||
# Der Boden der Huelle MUSS auf Z = 0 liegen. Das
|
||||
# Vereinfachen schneidet Ecken ab, und das anschliessende
|
||||
# Aufblasen (damit wieder alles hineinpasst) skaliert um
|
||||
# die Huellenmitte - dabei rutschte die Unterseite
|
||||
# gemessen 4-6 cm unter den Boden. Im Spiel waere das
|
||||
# eine Kollision, die aus dem Terrain herausragt.
|
||||
for v in hull.data.vertices:
|
||||
if v.co.z < 0.0:
|
||||
v.co.z = 0.0
|
||||
aufraeumen.append(hull.name)
|
||||
print("[TreeGen] %s: Stamm-UCX mit %d Verts"
|
||||
% (hull.name, len(hull.data.vertices)))
|
||||
|
||||
_deselect(context)
|
||||
if s.generate_lods:
|
||||
gruppe = bpy.data.objects.new(basis_name + "_LODGroup", None)
|
||||
@@ -2807,6 +2896,11 @@ class TREEGEN_OT_export_game(Operator):
|
||||
else:
|
||||
lods[0].select_set(True)
|
||||
context.view_layer.objects.active = lods[0]
|
||||
# Die Huelle wird MITEXPORTIERT, aber NICHT unter das
|
||||
# LodGroup-Empty gehaengt: Unreal liest jedes Kind der Gruppe
|
||||
# als LOD-Stufe. Geschwister, so wie es der Fels-Export macht.
|
||||
if hull is not None:
|
||||
hull.select_set(True)
|
||||
|
||||
bpy.ops.export_scene.fbx(
|
||||
filepath=ziel,
|
||||
@@ -3339,6 +3433,10 @@ class VIEW3D_PT_tree_export(_TreeSub):
|
||||
layout.separator()
|
||||
layout.prop(s, "sphere_normals")
|
||||
layout.prop(s, "generate_lods")
|
||||
layout.prop(s, "stand_ucx")
|
||||
row = layout.row()
|
||||
row.active = s.stand_ucx
|
||||
row.prop(s, "stand_ucx_verts")
|
||||
layout.prop(s, "export_dir")
|
||||
layout.operator("treegen.export_game", icon='EXPORT')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user