Tree Gen 1.29.0: LOD-Kette (Punkt 5) + Aufraeumen/Pruefen (Punkt 6)
Punkt 5 - LOD-Kette fuer den Stand-Baum
Haken "LOD-Kette erzeugen" liefert <Name>_LODGroup.fbx mit _LOD0..3 unter
einem fbx_type=LodGroup-Empty, wie die Fels-Pipeline. NUR der Stand-Baum -
die Faell-Teile leben Sekunden als Physik-Actor, dort reicht LOD0.
Blatt-Reduktion ueber die DICHTE, nicht ueber Decimate: eine Card besteht aus
zwei Dreiecken und laesst sich nicht vereinfachen, man muss weniger davon
setzen. Die verbleibenden werden groesser, damit die Krone nicht ausduennt.
LOD0 578 Rinde / 170 Blatt / 1306 Tris
LOD1 477 / 118 / 912
LOD2 364 / 66 / 575
LOD3 221 / 28 / 302 (16 % der Cards von LOD0)
Kugel-Normalen auf JEDER Stufe gemessen, nicht nur LOD0 - LOD1-3 haben eine
eigene Card-Verteilung. Am reimportierten FBX: 100 % radial auf allen vier.
Zwei Reihenfolge-Sachen: das Rinden-Decimate laeuft VOR dem Verschmelzen,
danach wuerde es die Blatt-Cards mit einschmelzen. Und der Ursprung kommt aus
LOD0 (Unreal platziert danach); alle Stufen werden um denselben Betrag
verschoben, sonst springt der Baum beim LOD-Wechsel.
Punkt 6 - Aufraeumen und Pruefen
Blatt-Streuung ueber alle neun Presets nachgemessen. Der Max-Radius-Filter
arbeitet richtig: 0-22 % der Cards landen am Stamm (meist unter 9 %), und
alle Presets bleiben unter dem 2500-Tri-Budget (Maximum weide 2246).
Messfalle, die ich dabei selbst produziert habe: zuerst hatte ich den Anteil
der VERTICES unter der Radius-Schwelle gemessen - beim Busch 95 %, sah nach
einem Problem aus. Das misst aber die Vertex-Verteilung, nicht wo Blaetter
landen; ein Busch besteht fast nur aus duennen Zweigen. Die richtige Frage
beantwortet erst ast_id.
Der Kaktus erzeugt regulaer 0 Cards (kein Ast duenner als Max Radius, botanisch
richtig). Das lief still durch und man stand vor einer leeren _Leaf-Ebene -
jetzt sagt der Operator es und nennt den Regler beim Namen.
Panels: das Export-Panel trug Stand-Baum, LODs und Faell-Teile in einem
Kasten. Die Teile haben jetzt "5 - Faell-Teile" fuer sich.
Neu: tests/test_baum_lod.py. 22 Tests bestehen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ nummerierten, eingeklappten Unter-Panels in der Reihenfolge des Arbeitsablaufs:
|
||||
| Addon | Unter-Panels |
|
||||
|---|---|
|
||||
| Rock Gen | 1 Benennung & Groesse · 2 Form · 3 Detail & Cleanup · 4 Shading · 5 Vertex-Farben · 6 UV/LOD/Material · 7 Batch & Transform · 8 FBX-Export · 9 Textur-Bake |
|
||||
| Tree Gen | 1 Wachstum · 2 Blattwerk · 3 Zusatz-Ebenen · 4 Export |
|
||||
| Tree Gen | 1 Wachstum · 2 Blattwerk · 3 Zusatz-Ebenen · 4 Export · 5 Faell-Teile |
|
||||
| Grass Gen | 1 Form · 2 Haltung · 3 Batch & Budget · 4 Kanal-Belegung |
|
||||
| Asset Utils | 1 Vorbereiten · 2 Vertex-Farben · 3 Benennung · 4 Pruefen · 5 Export |
|
||||
|
||||
@@ -385,6 +385,64 @@ Zwei Fallen beim Bauen:
|
||||
- `join` loescht das zweite Objekt; jede gehaltene Referenz darauf ist danach
|
||||
tot. Die Aufraeumliste fuehrt deshalb **Namen** statt Objekte.
|
||||
|
||||
### LOD-Kette (Stand-Baum)
|
||||
|
||||
Haken `LOD-Kette erzeugen`: exportiert `<Name>_LODGroup.fbx` mit `_LOD0..3` unter
|
||||
einem `fbx_type=LodGroup`-Empty — dasselbe Schema wie die Fels-Pipeline. **Nur
|
||||
der Stand-Baum**; die Faell-Teile leben Sekunden als Physik-Actor, dort reicht LOD0.
|
||||
|
||||
Die Blatt-Reduktion laeuft ueber die **Dichte**, nicht ueber Decimate: eine Card
|
||||
besteht aus zwei Dreiecken und laesst sich nicht vereinfachen — man muss weniger
|
||||
davon setzen. Damit die Krone dabei nicht ausduennt, werden die verbleibenden
|
||||
Cards groesser. LOD3 ist der Impostor-Ersatz.
|
||||
|
||||
| Stufe | Dichte | Card-Groesse | Rinde | gemessen (Rinde/Blatt/Tris) |
|
||||
|---|---|---|---|---|
|
||||
| LOD0 | 100 % | 100 % | 100 % | 578 / 170 / 1306 |
|
||||
| LOD1 | 60 % | 115 % | 70 % | 477 / 118 / 912 |
|
||||
| LOD2 | 30 % | 145 % | 45 % | 364 / 66 / 575 |
|
||||
| LOD3 | 12 % | 200 % | 25 % | 221 / 28 / 302 |
|
||||
|
||||
**Kugel-Normalen auf jeder Stufe** — LOD1–3 haben eine eigene Card-Verteilung,
|
||||
die Normalen muessen dazu passen. Am reimportierten FBX gemessen: **100 %
|
||||
radial auf allen vier Stufen**. LOD3 traegt 16 % der Cards von LOD0.
|
||||
|
||||
Der Ursprung kommt aus **LOD0** (Unreal platziert danach); alle Stufen werden um
|
||||
denselben Betrag verschoben, damit der Baum beim LOD-Wechsel nicht springt.
|
||||
Das Decimate der Rinde laeuft **vor** dem Verschmelzen — danach wuerde es die
|
||||
Blatt-Cards mit einschmelzen, und eine halbe Card ist keine Card.
|
||||
|
||||
### Blatt-Streuung: nachgemessen (Punkt 6)
|
||||
|
||||
`Max Radius` filtert die dicken Aeste heraus, damit keine Blaetter am Stamm
|
||||
kleben. Ueber alle Presets gemessen, wie viele Cards tatsaechlich am Stamm
|
||||
landen (moeglich, weil `ast_id` mitlaeuft):
|
||||
|
||||
| Preset | Cards | am Stamm | Tris gesamt |
|
||||
|---|---|---|---|
|
||||
| baum | 85 | 19 % | 1306 |
|
||||
| palme | 246 | 0 % | 1710 |
|
||||
| busch | 118 | 2 % | 876 |
|
||||
| tanne | 176 | 6 % | 2244 |
|
||||
| eiche | 172 | 2 % | 1772 |
|
||||
| birke | 133 | 9 % | 1918 |
|
||||
| weide | 297 | 1 % | 2246 |
|
||||
| toter_baum | 49 | 22 % | 838 |
|
||||
| kaktus | **0** | – | 740 |
|
||||
|
||||
Der Filter arbeitet also richtig, und **alle Presets bleiben unter dem
|
||||
2500-Tri-Budget** (Maximum weide 2246).
|
||||
|
||||
> **Messfalle dabei:** Zuerst hatte ich den Anteil der Vertices unter der
|
||||
> Radius-Schwelle gemessen — der lag beim Busch bei 95 % und sah nach einem
|
||||
> Problem aus. Das misst aber die Vertex-Verteilung, nicht wo Blaetter landen:
|
||||
> ein Busch besteht nun mal fast nur aus duennen Zweigen. Die richtige Frage
|
||||
> beantwortet erst `ast_id`.
|
||||
|
||||
Der Kaktus erzeugt regulaer **0 Cards** — kein Ast ist duenner als `Max Radius`,
|
||||
und das ist botanisch richtig. Vorher lief das still durch und man stand vor
|
||||
einer leeren `_Leaf`-Ebene; jetzt sagt der Operator es und nennt den Regler.
|
||||
|
||||
### Faell-Teile (Punkt 4)
|
||||
|
||||
Kein Laufzeit-Zerlegen: beim Faellen tauscht das Spiel die ISM-Instanz gegen
|
||||
@@ -641,6 +699,7 @@ gefiltert `.\run_tests.ps1 -Filter panels`.
|
||||
| Datei | prueft |
|
||||
|---|---|
|
||||
| `tests/test_baum_export.py` | Stand-Baum: ein Mesh, zwei Slots, Ursprung, **Kugel-Normalen im exportierten FBX** |
|
||||
| `tests/test_baum_lod.py` | LOD-Kette: 4 Stufen unter LodGroup, Cards werden nach oben duenner, **Kugel-Normalen auf jeder Stufe**, kein Hoehenversatz |
|
||||
| `tests/test_faell_teile.py` | Faell-Teile: **Blatt-Bilanz 170=170**, Pivot am Massenmittelpunkt, UCX je Teil, Gesamtdatei mit gemeinsamem Ursprung |
|
||||
| `tests/test_blatt_varianten.py` | Card-Sammlung: alle Varianten werden gezogen, seed-stabil, Einzelobjekt weiter moeglich |
|
||||
| `tests/test_proportionen.py` | Baum-Proportionen: Palmenkrone, Strauch-Breite, Setzling-Schlankheit, Branch Count 0 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "stylized_tree_generator"
|
||||
version = "1.28.0"
|
||||
version = "1.29.0"
|
||||
name = "Stylized Tree Generator"
|
||||
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
|
||||
maintainer = "D4rkst3r"
|
||||
|
||||
Vendored
+3
-3
@@ -50,17 +50,17 @@
|
||||
<td>34.1KB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><tt><a href="./stylized_tree_generator-1.28.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.28.0</a></tt></td>
|
||||
<td><tt><a href="./stylized_tree_generator-1.29.0.zip?repository=.%2Findex.json&blender_version_min=4.2.0">stylized_tree_generator-1.29.0</a></tt></td>
|
||||
<td>Stylized Tree Generator</td>
|
||||
<td>Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen</td>
|
||||
<td><a href="https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator">link</a></td>
|
||||
<td>4.2.0 - ~</td>
|
||||
<td>all</td>
|
||||
<td>all</td>
|
||||
<td>33.0KB</td>
|
||||
<td>34.4KB</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<center><p>Built 2026-08-06, 05:40</p></center>
|
||||
<center><p>Built 2026-08-06, 05:55</p></center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Vendored
+4
-4
@@ -82,7 +82,7 @@
|
||||
"id": "stylized_tree_generator",
|
||||
"name": "Stylized Tree Generator",
|
||||
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
|
||||
"version": "1.28.0",
|
||||
"version": "1.29.0",
|
||||
"type": "add-on",
|
||||
"maintainer": "D4rkst3r",
|
||||
"license": [
|
||||
@@ -98,9 +98,9 @@
|
||||
"Mesh",
|
||||
"Modeling"
|
||||
],
|
||||
"archive_url": "./stylized_tree_generator-1.28.0.zip",
|
||||
"archive_size": 33772,
|
||||
"archive_hash": "sha256:84ff026fecccddc944a1f847d832f3d8b45b37f142bb9e1cf342a103e2ca290f"
|
||||
"archive_url": "./stylized_tree_generator-1.29.0.zip",
|
||||
"archive_size": 35207,
|
||||
"archive_hash": "sha256:faa1e629b1ee4cfd465db79246a6d4acfae12af61f8b65aafcabcc64ffdcde72"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+137
-26
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "Stylized Tree Generator",
|
||||
"author": "D4rkst3r",
|
||||
"version": (1, 28, 0),
|
||||
"version": (1, 29, 0),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "View3D > Sidebar > Tree Gen",
|
||||
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
||||
@@ -1405,6 +1405,21 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, **overri
|
||||
# Spielfertiger Export: verschmolzener Stand-Baum
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# LOD-Stufen fuer den Stand-Baum. Die Blatt-Reduktion laeuft ueber die DICHTE,
|
||||
# nicht ueber Decimate: eine Card ist ein Billboard aus zwei Dreiecken - die
|
||||
# laesst sich nicht sinnvoll vereinfachen, man muss weniger davon setzen. Damit
|
||||
# die Krone dabei nicht ausduennt, werden die verbleibenden Cards groesser.
|
||||
# LOD3 ist der Impostor-Ersatz: wenige, dafuer sehr grosse Cards.
|
||||
#
|
||||
# Dichte Card-Groesse Rinden-Decimate
|
||||
LOD_STUFEN = (
|
||||
(1.00, 1.00, 1.00), # LOD0 - wie der Stand-Baum
|
||||
(0.60, 1.15, 0.70), # LOD1
|
||||
(0.30, 1.45, 0.45), # LOD2
|
||||
(0.12, 2.00, 0.25), # LOD3
|
||||
)
|
||||
|
||||
# Slot 0 = Rinde, Slot 1 = Blaetter. Die Reihenfolge ist Teil des Vertrags -
|
||||
# die UE-Skripte mappen ueber den Slot-Namen, aber der Index entscheidet, welche
|
||||
# Faces welches Material bekommen.
|
||||
@@ -1464,7 +1479,7 @@ def _sphere_normals(ob, slot_index, center=None):
|
||||
return center
|
||||
|
||||
|
||||
def _merge_for_export(context, tree_ob, leaf_ob, name):
|
||||
def _merge_for_export(context, tree_ob, leaf_ob, name, bark_decimate=1.0):
|
||||
"""Baut EINE Export-Kopie aus Stamm + Blattebene mit zwei Material-Slots.
|
||||
|
||||
Die getrennten Objekte im Blend bleiben unangetastet - das ist die
|
||||
@@ -1486,6 +1501,22 @@ def _merge_for_export(context, tree_ob, leaf_ob, name):
|
||||
return ob
|
||||
|
||||
stamm = _kopie(tree_ob)
|
||||
|
||||
# Rinde VOR dem Verschmelzen vereinfachen. Danach ginge es nicht mehr: ein
|
||||
# Decimate ueber das fertige Mesh wuerde auch die Blatt-Cards einschmelzen,
|
||||
# und eine halbe Card ist keine Card mehr.
|
||||
if bark_decimate < 0.999 and len(stamm.data.polygons) > 8:
|
||||
dec = stamm.modifiers.new("LODdec", 'DECIMATE')
|
||||
dec.ratio = max(0.02, bark_decimate)
|
||||
dg = context.evaluated_depsgraph_get()
|
||||
neu = bpy.data.meshes.new_from_object(
|
||||
stamm.evaluated_get(dg), preserve_all_data_layers=True, depsgraph=dg)
|
||||
alt = stamm.data
|
||||
stamm.modifiers.remove(dec)
|
||||
stamm.data = neu
|
||||
if alt.users == 0:
|
||||
bpy.data.meshes.remove(alt)
|
||||
|
||||
blatt = _kopie(leaf_ob) if leaf_ob is not None else None
|
||||
|
||||
rinde = _get_or_create_mat(TREE_MATERIALS[0], (0.29, 0.21, 0.14, 1.0))
|
||||
@@ -1837,6 +1868,14 @@ class TreeGenSettings(PropertyGroup):
|
||||
name="Export-Ordner", default="", subtype='DIR_PATH',
|
||||
description="Zielordner fuer die spielfertigen FBX",
|
||||
)
|
||||
generate_lods: BoolProperty(
|
||||
name="LOD-Kette erzeugen", default=False,
|
||||
description=("Exportiert LOD0-LOD3 als LOD-Gruppe (Empty mit "
|
||||
"fbx_type=LodGroup) wie in der Fels-Pipeline. Die Blaetter "
|
||||
"werden je Stufe duenner gestreut und dafuer groesser - "
|
||||
"eine Card aus zwei Dreiecken laesst sich nicht vereinfachen"),
|
||||
)
|
||||
|
||||
sphere_normals: BoolProperty(
|
||||
name="Kugel-Normalen auf Blaetter", default=True,
|
||||
description=("Setzt die Blatt-Normalen radial vom Kronen-Schwerpunkt nach "
|
||||
@@ -1940,7 +1979,7 @@ def _origin_bottom_center(ob):
|
||||
ob.matrix_basis = Matrix.Identity(4)
|
||||
ob.matrix_parent_inverse = Matrix.Identity(4)
|
||||
if not me.vertices:
|
||||
return
|
||||
return Matrix.Identity(4)
|
||||
xs = [v.co.x for v in me.vertices]
|
||||
ys = [v.co.y for v in me.vertices]
|
||||
zs = [v.co.z for v in me.vertices]
|
||||
@@ -1950,6 +1989,7 @@ def _origin_bottom_center(ob):
|
||||
-min(zs),
|
||||
))
|
||||
me.transform(versatz)
|
||||
return versatz
|
||||
|
||||
|
||||
|
||||
@@ -1997,25 +2037,78 @@ class TREEGEN_OT_export_game(Operator):
|
||||
umbenannt.append((quelle, quelle.name))
|
||||
quelle.name = quelle.name + ".__src"
|
||||
|
||||
merge, aufraeumen = _merge_for_export(
|
||||
context, baum, blatt, umbenannt[0][1])
|
||||
basis_name = umbenannt[0][1]
|
||||
stufen = LOD_STUFEN if s.generate_lods else LOD_STUFEN[:1]
|
||||
gruppe = None
|
||||
lods = []
|
||||
|
||||
# Kugel-Normalen auf die Blatt-Faces (Slot 1).
|
||||
for i, (dichte, groesse, dec) in enumerate(stufen):
|
||||
lod_name = ("%s_LOD%d" % (basis_name, i)) if s.generate_lods \
|
||||
else basis_name
|
||||
|
||||
# Blatt-Ebene je Stufe NEU streuen statt sie zu dezimieren:
|
||||
# eine Card besteht aus zwei Dreiecken, die laesst sich nicht
|
||||
# vereinfachen - man muss weniger davon setzen. Die
|
||||
# verbleibenden werden groesser, damit die Krone nicht
|
||||
# ausduennt.
|
||||
lod_blatt = blatt
|
||||
if blatt is not None and s.generate_lods and i > 0:
|
||||
lod_blatt = make_leaves(
|
||||
context, baum, s.leaf_card,
|
||||
name="%s__leaflod%d" % (basis_name, i),
|
||||
card_coll=s.leaf_cards,
|
||||
Density=s.leaf_density * dichte,
|
||||
Size=s.leaf_size * groesse,
|
||||
**{"Size Var": s.leaf_size_var,
|
||||
"Max Radius": s.leaf_max_radius,
|
||||
"Tilt": s.leaf_tilt, "Seed": s.seed})
|
||||
context.view_layer.update()
|
||||
aufraeumen.append(lod_blatt.name)
|
||||
|
||||
merge, gemacht = _merge_for_export(
|
||||
context, baum, lod_blatt, lod_name, bark_decimate=dec)
|
||||
aufraeumen.extend(gemacht)
|
||||
|
||||
# Kugel-Normalen auf JEDER Stufe - LOD1..3 haben eine eigene
|
||||
# Card-Verteilung, die Normalen muessen dazu passen.
|
||||
mitte = None
|
||||
if s.sphere_normals and len(merge.data.materials) > 1:
|
||||
mitte = _sphere_normals(merge, 1)
|
||||
if mitte is not None:
|
||||
print("[TreeGen] %s: Kugel-Normalen um (%.2f, %.2f, %.2f)"
|
||||
% (merge.name, mitte.x, mitte.y, mitte.z))
|
||||
lods.append(merge)
|
||||
|
||||
# Ursprung: Bbox-Unterseite Mitte, wie in der Fels-Pipeline.
|
||||
_origin_bottom_center(merge)
|
||||
# Ursprung aus LOD0 - Unreal platziert nach LOD0. Alle Stufen um
|
||||
# denselben Betrag verschieben, damit sie deckungsgleich bleiben.
|
||||
versatz = _origin_bottom_center(lods[0])
|
||||
for weitere in lods[1:]:
|
||||
weitere.data.transform(versatz)
|
||||
weitere.matrix_basis = Matrix.Identity(4)
|
||||
weitere.matrix_parent_inverse = Matrix.Identity(4)
|
||||
|
||||
_deselect(context)
|
||||
merge.select_set(True)
|
||||
context.view_layer.objects.active = merge
|
||||
if s.generate_lods:
|
||||
gruppe = bpy.data.objects.new(basis_name + "_LODGroup", None)
|
||||
gruppe.empty_display_type = 'PLAIN_AXES'
|
||||
gruppe["fbx_type"] = "LodGroup" # von Unreal ausgewertet
|
||||
context.scene.collection.objects.link(gruppe)
|
||||
aufraeumen.append(gruppe.name)
|
||||
for lod in lods:
|
||||
lod.parent = gruppe
|
||||
lod.matrix_parent_inverse = Matrix.Identity(4)
|
||||
lod.select_set(True)
|
||||
gruppe.select_set(True)
|
||||
context.view_layer.objects.active = gruppe
|
||||
ziel = os.path.join(verzeichnis, gruppe.name + ".fbx")
|
||||
else:
|
||||
lods[0].select_set(True)
|
||||
context.view_layer.objects.active = lods[0]
|
||||
|
||||
bpy.ops.export_scene.fbx(
|
||||
filepath=ziel,
|
||||
use_selection=True,
|
||||
object_types={'MESH'},
|
||||
object_types={'MESH', 'EMPTY'}, # EMPTY = die LodGroup
|
||||
use_mesh_modifiers=False, # ist bereits ausgewertet
|
||||
mesh_smooth_type='OFF', # OFF = Custom Normals bleiben
|
||||
use_custom_props=True,
|
||||
@@ -2026,9 +2119,6 @@ class TREEGEN_OT_export_game(Operator):
|
||||
apply_scale_options='FBX_SCALE_ALL',
|
||||
)
|
||||
geschrieben += 1
|
||||
if mitte is not None:
|
||||
print("[TreeGen] %s: Kugel-Normalen um (%.2f, %.2f, %.2f)"
|
||||
% (merge.name, mitte.x, mitte.y, mitte.z))
|
||||
except Exception as exc: # ein Baum darf den Rest nicht killen
|
||||
fehler += 1
|
||||
print("[TreeGen] Export '%s' fehlgeschlagen: %s" % (baum.name, exc))
|
||||
@@ -2285,7 +2375,16 @@ class TREEGEN_OT_leaves(Operator):
|
||||
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:
|
||||
if total_leaf == 0:
|
||||
# Passiert regulaer beim Kaktus (kein Ast duenner als Max Radius) -
|
||||
# aber auch, wenn Max Radius fuer den Baum zu klein steht. Beides
|
||||
# sieht in der Szene gleich aus: eine leere _Leaf-Ebene. Ohne
|
||||
# Hinweis sucht man den Fehler im Material.
|
||||
self.report({'WARNING'},
|
||||
"Keine Blaetter gestreut - kein Ast ist duenner als "
|
||||
"Max Radius (%.3f m). Beim Kaktus ist das richtig, "
|
||||
"sonst Max Radius erhoehen." % s.leaf_max_radius)
|
||||
elif s.leaf_budget and total > limit:
|
||||
self.report({'WARNING'}, msg + " - UEBER Budget (%d). Dichte senken." % limit)
|
||||
else:
|
||||
self.report({'INFO'}, msg)
|
||||
@@ -2495,6 +2594,26 @@ class VIEW3D_PT_tree_layers(_TreeSub):
|
||||
layout.prop(s, "make_frucht")
|
||||
|
||||
|
||||
class VIEW3D_PT_tree_pieces(_TreeSub):
|
||||
"""Eigenes Unter-Panel: das Export-Panel war mit Stand-Baum, LODs und
|
||||
Faell-Teilen ueberladen - drei Themen in einem Kasten."""
|
||||
bl_label = "5 - Faell-Teile"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
s = context.scene.tree_gen_settings
|
||||
layout.prop(s, "piece_trunk_segments")
|
||||
layout.prop(s, "piece_ucx")
|
||||
row = layout.row()
|
||||
row.active = s.piece_ucx
|
||||
row.prop(s, "piece_ucx_verts")
|
||||
layout.prop(s, "piece_export_combined")
|
||||
layout.prop(s, "piece_keep")
|
||||
layout.separator()
|
||||
layout.operator("treegen.export_pieces", icon='MOD_BUILD')
|
||||
layout.label(text="Pivot je Teil = Massenmittelpunkt", icon='INFO')
|
||||
|
||||
|
||||
class VIEW3D_PT_tree_export(_TreeSub):
|
||||
bl_label = "4 - Export"
|
||||
|
||||
@@ -2507,19 +2626,10 @@ class VIEW3D_PT_tree_export(_TreeSub):
|
||||
row.prop(s, "generate_lightmap_uv")
|
||||
layout.separator()
|
||||
layout.prop(s, "sphere_normals")
|
||||
layout.prop(s, "generate_lods")
|
||||
layout.prop(s, "export_dir")
|
||||
layout.operator("treegen.export_game", icon='EXPORT')
|
||||
layout.separator()
|
||||
box = layout.box()
|
||||
box.label(text="Faell-Teile", icon='MOD_BUILD')
|
||||
box.prop(s, "piece_trunk_segments")
|
||||
box.prop(s, "piece_ucx")
|
||||
row = box.row()
|
||||
row.active = s.piece_ucx
|
||||
row.prop(s, "piece_ucx_verts")
|
||||
box.prop(s, "piece_export_combined")
|
||||
box.prop(s, "piece_keep")
|
||||
box.operator("treegen.export_pieces", icon='MOD_BUILD')
|
||||
|
||||
layout.label(text="Slot 0 = %s, Slot 1 = %s"
|
||||
% (TREE_MATERIALS[0], TREE_MATERIALS[1]), icon='MATERIAL')
|
||||
|
||||
@@ -2537,6 +2647,7 @@ classes = (
|
||||
VIEW3D_PT_tree_leaves,
|
||||
VIEW3D_PT_tree_layers,
|
||||
VIEW3D_PT_tree_export,
|
||||
VIEW3D_PT_tree_pieces,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Punkt 5: LOD-Kette fuer den Stand-Baum.
|
||||
|
||||
Die Blatt-Reduktion laeuft ueber die DICHTE, nicht ueber Decimate: eine Card
|
||||
besteht aus zwei Dreiecken und laesst sich nicht vereinfachen - man muss weniger
|
||||
davon setzen und die verbleibenden groesser machen.
|
||||
|
||||
Gemessen wird am reimportierten FBX, und die Kugel-Normalen werden auf JEDER
|
||||
Stufe geprueft: LOD1-3 haben eine eigene Card-Verteilung, die Normalen muessen
|
||||
dazu passen. Nur LOD0 zu pruefen wuerde genau das verfehlen.
|
||||
|
||||
Aufruf: blender --background --factory-startup --python tests/test_baum_lod.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import importlib.util
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"tg", os.path.join(ROOT, "stylized_tree_generator.py"))
|
||||
tg = importlib.util.module_from_spec(spec)
|
||||
sys.modules["tg"] = tg
|
||||
spec.loader.exec_module(tg)
|
||||
tg.register()
|
||||
|
||||
fails = []
|
||||
OUT = tempfile.mkdtemp()
|
||||
|
||||
for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
|
||||
me = bpy.data.meshes.new("Leaf_Card")
|
||||
me.from_pydata([(-0.5, 0, 0), (0, 0, 0.7), (0.5, 0, 0), (0, 0, -0.1)],
|
||||
[], [(0, 1, 2), (0, 2, 3)])
|
||||
me.update()
|
||||
card = bpy.data.objects.new("Leaf_Card", me)
|
||||
bpy.context.scene.collection.objects.link(card)
|
||||
|
||||
s = bpy.context.scene.tree_gen_settings
|
||||
s.preset = 'baum'
|
||||
s.use_growth = False
|
||||
s.apply_modifier = True
|
||||
s.art_name = "Eiche"
|
||||
s.variant = 1
|
||||
s.export_dir = OUT
|
||||
s.sphere_normals = True
|
||||
s.generate_lods = True
|
||||
|
||||
bpy.ops.object.treegen_create()
|
||||
baum = [o for o in bpy.context.scene.objects
|
||||
if o.type == 'MESH' and o.name.startswith("Eiche_01")
|
||||
and not o.name.endswith(("_Leaf", "_Frucht"))][0]
|
||||
for o in bpy.context.scene.objects:
|
||||
o.select_set(o is baum)
|
||||
bpy.context.view_layer.objects.active = baum
|
||||
s.leaf_card = card
|
||||
bpy.ops.object.treegen_leaves()
|
||||
|
||||
for o in bpy.context.scene.objects:
|
||||
o.select_set(o is baum)
|
||||
bpy.context.view_layer.objects.active = baum
|
||||
namen_vorher = sorted(o.name for o in bpy.context.scene.objects)
|
||||
|
||||
bpy.ops.treegen.export_game()
|
||||
|
||||
if sorted(o.name for o in bpy.context.scene.objects) != namen_vorher:
|
||||
fehlt = [n for n in namen_vorher
|
||||
if n not in [o.name for o in bpy.context.scene.objects]]
|
||||
neu = [o.name for o in bpy.context.scene.objects if o.name not in namen_vorher]
|
||||
fails.append("Szene veraendert - fehlt: %s, neu: %s" % (fehlt, neu))
|
||||
|
||||
dateien = sorted(f for f in os.listdir(OUT) if f.lower().endswith(".fbx"))
|
||||
lodfile = [f for f in dateien if "_LODGroup" in f]
|
||||
print("Export: %s" % dateien)
|
||||
if not lodfile:
|
||||
print("ERGEBNIS: FEHLER: keine _LODGroup-Datei")
|
||||
sys.exit(1)
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.fbx(filepath=os.path.join(OUT, lodfile[0]))
|
||||
|
||||
meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||||
empties = [o for o in bpy.context.scene.objects if o.type == 'EMPTY']
|
||||
stufen = sorted(meshes, key=lambda o: o.name)
|
||||
print("")
|
||||
print("%-24s %7s %7s %7s %9s %s"
|
||||
% ("Stufe", "Rinde", "Blatt", "Tris", "Radial", "Slots"))
|
||||
|
||||
if len(stufen) != len(tg.LOD_STUFEN):
|
||||
fails.append("%d LOD-Meshes, erwartet %d" % (len(stufen), len(tg.LOD_STUFEN)))
|
||||
if not empties:
|
||||
fails.append("Kein LodGroup-Empty im FBX - Unreal erkennt die Kette nicht")
|
||||
|
||||
blatt_zahlen = []
|
||||
rinde_zahlen = []
|
||||
for ob in stufen:
|
||||
m = ob.data
|
||||
rinde = sum(1 for p in m.polygons if p.material_index == 0)
|
||||
laub = sum(1 for p in m.polygons if p.material_index == 1)
|
||||
tris = sum(len(p.vertices) - 2 for p in m.polygons)
|
||||
blatt_zahlen.append(laub)
|
||||
rinde_zahlen.append(rinde)
|
||||
|
||||
# Kugel-Normalen auf DIESER Stufe nachmessen.
|
||||
quote = 0.0
|
||||
blatt_polys = [p for p in m.polygons if p.material_index == 1]
|
||||
if blatt_polys:
|
||||
pts = [m.vertices[vi].co for p in blatt_polys for vi in p.vertices]
|
||||
mitte = Vector((sum(p.x for p in pts) / len(pts),
|
||||
sum(p.y for p in pts) / len(pts),
|
||||
sum(p.z for p in pts) / len(pts)))
|
||||
if hasattr(m, "calc_normals_split"):
|
||||
m.calc_normals_split()
|
||||
treffer = gesamt = 0
|
||||
for p in blatt_polys:
|
||||
for li in p.loop_indices:
|
||||
n = Vector(m.loops[li].normal)
|
||||
radial = m.vertices[m.loops[li].vertex_index].co - mitte
|
||||
if n.length_squared < 1e-9 or radial.length_squared < 1e-9:
|
||||
continue
|
||||
gesamt += 1
|
||||
if n.normalized().dot(radial.normalized()) > 0.8:
|
||||
treffer += 1
|
||||
quote = 100.0 * treffer / gesamt if gesamt else 0.0
|
||||
|
||||
print("%-24s %7d %7d %7d %8.0f%% %s"
|
||||
% (ob.name, rinde, laub, tris, quote,
|
||||
[x.name if x else "-" for x in m.materials]))
|
||||
|
||||
if len(m.materials) != 2:
|
||||
fails.append("%s: %d Slots, erwartet 2" % (ob.name, len(m.materials)))
|
||||
if blatt_polys and quote < 90.0:
|
||||
fails.append("%s: nur %.0f%% der Blatt-Normalen radial - die "
|
||||
"Kugel-Normalen fehlen auf dieser Stufe" % (ob.name, quote))
|
||||
if not blatt_polys:
|
||||
fails.append("%s: keine Blatt-Faces" % ob.name)
|
||||
|
||||
# --- Die Ketten-Eigenschaft: es muss nach oben duenner werden -------------
|
||||
print("")
|
||||
print("Blatt-Faces je Stufe: %s" % blatt_zahlen)
|
||||
print("Rinden-Faces je Stufe: %s" % rinde_zahlen)
|
||||
for i in range(1, len(blatt_zahlen)):
|
||||
if blatt_zahlen[i] >= blatt_zahlen[i - 1]:
|
||||
fails.append("LOD%d hat %d Blatt-Faces, LOD%d hatte %d - die Kette wird "
|
||||
"nicht duenner" % (i, blatt_zahlen[i], i - 1,
|
||||
blatt_zahlen[i - 1]))
|
||||
for i in range(1, len(rinde_zahlen)):
|
||||
if rinde_zahlen[i] > rinde_zahlen[i - 1]:
|
||||
fails.append("LOD%d hat mehr Rinden-Faces als LOD%d" % (i, i - 1))
|
||||
|
||||
# LOD3 muss deutlich guenstiger sein als LOD0, sonst lohnt die Kette nicht.
|
||||
if blatt_zahlen and blatt_zahlen[0] > 0:
|
||||
anteil = 100.0 * blatt_zahlen[-1] / blatt_zahlen[0]
|
||||
print("LOD3 traegt %.0f%% der Cards von LOD0" % anteil)
|
||||
if anteil > 30.0:
|
||||
fails.append("LOD3 hat noch %.0f%% der Cards - als Impostor-Ersatz zu "
|
||||
"teuer" % anteil)
|
||||
|
||||
# --- Alle Stufen deckungsgleich am Ursprung ------------------------------
|
||||
for ob in stufen:
|
||||
co = [ob.matrix_world @ v.co for v in ob.data.vertices]
|
||||
lo = [min(c[i] for c in co) for i in range(3)]
|
||||
hi = [max(c[i] for c in co) for i in range(3)]
|
||||
if abs((lo[0] + hi[0]) * 0.5) > 0.25 or abs((lo[1] + hi[1]) * 0.5) > 0.25:
|
||||
fails.append("%s: Bbox nicht zentriert (%.2f/%.2f)"
|
||||
% (ob.name, (lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5))
|
||||
# Die Stufen muessen auf derselben Hoehe stehen - sonst springt der Baum
|
||||
# beim LOD-Wechsel.
|
||||
if abs(lo[2]) > 0.15:
|
||||
fails.append("%s: steht %.3f m ueber/unter dem Ursprung - der Baum "
|
||||
"wuerde beim LOD-Wechsel springen" % (ob.name, lo[2]))
|
||||
|
||||
print("")
|
||||
if fails:
|
||||
print("ERGEBNIS: %d FEHLER" % len(fails))
|
||||
for x in fails:
|
||||
print(" - " + x)
|
||||
sys.exit(1)
|
||||
print("ERGEBNIS: ALLE CHECKS OK")
|
||||
@@ -28,7 +28,7 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ADDONS = {
|
||||
"stylized_rock_generator.py": ("VIEW3D_PT_rock_generator", 9,
|
||||
"rock_gen_settings", set()),
|
||||
"stylized_tree_generator.py": ("VIEW3D_PT_tree_generator", 4,
|
||||
"stylized_tree_generator.py": ("VIEW3D_PT_tree_generator", 5,
|
||||
"tree_gen_settings", set()),
|
||||
"stylized_grass_generator.py": ("VIEW3D_PT_grass_generator", 4,
|
||||
"grass_gen_settings", set()),
|
||||
|
||||
Reference in New Issue
Block a user