diff --git a/README.md b/README.md index d17074f..71f76a6 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,7 @@ gemessen und abgenommen: | | vorher | jetzt | Wirkung | |---|---|---|---| | Groesse | 0.22 | **0.85** | Bueschel statt Bonsai | -| Dichte | 500 | **170** | wenige grosse statt viele Fussel | +| Dichte | 500 | **150** | wenige grosse statt viele Fussel | | **Max Radius** | 0.02 | **0.04** | der Volumen-Hebel | | Kippung | 0.5 | **0.9** | Rotations-Chaos schliesst Sichtluecken | @@ -454,6 +454,15 @@ 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. + +> **Falle, aus UE gemeldet:** `_origin_bottom_center` backt zuerst die Weltmatrix +> ins Mesh und wendet **dann** die Zentrierung an — zurueck gibt es aber nur die +> Zentrierung. Wer den Stufen 1–3 nur diese aufdrueckt, laesst ihre Weltmatrix +> ungebacken: sie landen bei **minus der Standposition**. Gemessen an einem Baum +> bei x = 18 m: LOD1–3 bei x = −17.87. Bei einem Baum am Ursprung faellt das +> nicht auf, weil minus null eben null ist — genau deshalb steht der Baum im +> Test jetzt **bewusst nicht** am Ursprung, und die X/Y-Mitte wird je Stufe im +> reimportierten FBX gemessen. Das Decimate der Rinde laeuft **vor** dem Verschmelzen — danach wuerde es die Blatt-Cards mit einschmelzen, und eine halbe Card ist keine Card. diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index bfa9c34..0f61dd5 100644 --- a/blender_manifest_tree.toml +++ b/blender_manifest_tree.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_tree_generator" -version = "1.30.0" +version = "1.31.0" name = "Stylized Tree Generator" tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen" maintainer = "D4rkst3r" diff --git a/dist/index.html b/dist/index.html index 5d738c9..30d5d95 100644 --- a/dist/index.html +++ b/dist/index.html @@ -50,17 +50,17 @@ 34.1KB - stylized_tree_generator-1.30.0 + stylized_tree_generator-1.31.0 Stylized Tree Generator Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen link 4.2.0 - ~ all all - 36.0KB + 36.3KB -

Built 2026-08-06, 06:22

+

Built 2026-08-06, 07:20

diff --git a/dist/index.json b/dist/index.json index 92ad9d3..bc7bffa 100644 --- a/dist/index.json +++ b/dist/index.json @@ -82,7 +82,7 @@ "id": "stylized_tree_generator", "name": "Stylized Tree Generator", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", - "version": "1.30.0", + "version": "1.31.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -98,9 +98,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.30.0.zip", - "archive_size": 36830, - "archive_hash": "sha256:d54440c3d577440ff97b0415ef101801b0b7bf50b6df05e3645cc3d1e1eb15e5" + "archive_url": "./stylized_tree_generator-1.31.0.zip", + "archive_size": 37196, + "archive_hash": "sha256:2afb514c39d4378baa8885b53e117bb460987ac57178a826bc20cc8b1efd71b5" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.30.0.zip b/dist/stylized_tree_generator-1.30.0.zip deleted file mode 100644 index 42f2c20..0000000 Binary files a/dist/stylized_tree_generator-1.30.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.31.0.zip b/dist/stylized_tree_generator-1.31.0.zip new file mode 100644 index 0000000..c446860 Binary files /dev/null and b/dist/stylized_tree_generator-1.31.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 996435a..9d609f8 100644 --- a/stylized_tree_generator.py +++ b/stylized_tree_generator.py @@ -1,7 +1,7 @@ bl_info = { "name": "Stylized Tree Generator", "author": "D4rkst3r", - "version": (1, 30, 0), + "version": (1, 31, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -1195,7 +1195,11 @@ LEAF_GROUP_NAME = "GN_TreeLeaves" # hingehoeren. Mit Bueschel-Atlanten liegen die Groessenordnungen bei # Size ~0.7 und Density ~90. LEAF_DEFAULTS = { - "Density": 170.0, # Punkte pro m2 Zweigoberflaeche + # 150 statt der zunaechst gemessenen 170: mit der REALEN Diamond-Card + # (6 Tris, nicht 2) landet die Eiche bei 170 auf 4206 Tris und damit ueber + # dem 4000er-Budget aus ASSETS.md Teil 6 - 2778 Krone plus 1428 Stamm. + # 150 liegt im abgesegneten Sweetspot 130-170 und haelt das Budget (3816). + "Density": 150.0, # Punkte pro m2 Zweigoberflaeche # DER Volumen-Hebel. Bei 0.02 trugen nur die aeussersten Zweigspitzen Laub - # die Krone hatte nur Kontur und war innen nackt. Bei 0.04 wachsen Bueschel # auch am mittleren Geaest und die Krone bekommt Tiefe. Am fertigen Baum in @@ -1861,7 +1865,7 @@ class TreeGenSettings(PropertyGroup): "damit mischt die Krone 4-6 Varianten statt einer"), ) leaf_density: FloatProperty( - name="Dichte", default=170.0, min=0.0, max=20000.0, + 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"), @@ -2147,13 +2151,24 @@ class TREEGEN_OT_export_game(Operator): % (merge.name, mitte.x, mitte.y, mitte.z)) lods.append(merge) - # Ursprung aus LOD0 - Unreal platziert nach LOD0. Alle Stufen um - # denselben Betrag verschieben, damit sie deckungsgleich bleiben. + # Ursprung aus LOD0 - Unreal platziert nach LOD0. Alle Stufen + # bekommen DIESELBE Zentrierung, damit sie deckungsgleich bleiben. + # + # FALLE: _origin_bottom_center backt zuerst die Weltmatrix ins + # Mesh und wendet DANN die Zentrierung an - zurueck gibt es aber + # nur die Zentrierung. Wer den Stufen 1-3 nur diese aufdrueckt, + # laesst ihre Weltmatrix ungebacken: sie landen bei MINUS der + # Standposition. Gemeldet aus UE fuer einen Baum bei x = 18 m, + # dessen LOD1-3 bei x = -1894 cm sassen - exakt die + # Standposition, in Zentimetern wegen FBX_SCALE_ALL. + # Deshalb je Stufe erst die EIGENE Weltmatrix backen, dann die + # gemeinsame Zentrierung. versatz = _origin_bottom_center(lods[0]) for weitere in lods[1:]: - weitere.data.transform(versatz) + weitere.data.transform(weitere.matrix_world) weitere.matrix_basis = Matrix.Identity(4) weitere.matrix_parent_inverse = Matrix.Identity(4) + weitere.data.transform(versatz) _deselect(context) if s.generate_lods: diff --git a/tests/test_baum_lod.py b/tests/test_baum_lod.py index f9fbe3f..4b483c5 100644 --- a/tests/test_baum_lod.py +++ b/tests/test_baum_lod.py @@ -8,6 +8,12 @@ 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. +WICHTIG: Der Baum steht hier NICHT am Ursprung. Eine fruehere Fassung dieses +Tests baute ihn bei (0,0,0) - und ist damit an einem Fehler vorbeigelaufen, den +die Spielseite gefunden hat: LOD1-3 landeten bei MINUS der Standposition, weil +ihre Weltmatrix nie ins Mesh gebacken wurde. Bei x = 0 faellt das nicht auf, +weil minus null eben null ist. + Aufruf: blender --background --factory-startup --python tests/test_baum_lod.py """ import os @@ -53,11 +59,19 @@ 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] + +# Der Baum wird BEWUSST weit vom Ursprung gestellt - siehe Kopf der Datei. +STANDORT = (18.0, -7.5, 0.0) +baum.location = STANDORT 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() +# Die Blattebene muss mit an die Standposition - sie folgt dem Stamm. +_bl = bpy.data.objects.get(baum.name + "_Leaf") +if _bl is not None: + _bl.location = STANDORT for o in bpy.context.scene.objects: o.select_set(o is baum) @@ -86,8 +100,11 @@ 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")) +print("Baum stand bei x=%.1f, y=%.1f - die Stufen muessen trotzdem um den " + "Ursprung liegen." % (STANDORT[0], STANDORT[1])) +print("") +print("%-24s %7s %7s %7s %9s %9s %9s" + % ("Stufe", "Rinde", "Blatt", "Tris", "Radial", "X-Mitte", "Y-Mitte")) if len(stufen) != len(tg.LOD_STUFEN): fails.append("%d LOD-Meshes, erwartet %d" % (len(stufen), len(tg.LOD_STUFEN))) @@ -96,6 +113,7 @@ if not empties: blatt_zahlen = [] rinde_zahlen = [] +spannen = [] for ob in stufen: m = ob.data rinde = sum(1 for p in m.polygons if p.material_index == 0) @@ -126,9 +144,18 @@ for ob in stufen: 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])) + # X/Y-Spanne dieser Stufe im FBX. Alle Stufen muessen deckungsgleich um + # den Ursprung liegen - eine verschobene Stufe waere in UE beim + # LOD-Wechsel schlagartig woanders. + co = [ob.matrix_world @ v.co for v in m.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)] + xm = (lo[0] + hi[0]) * 0.5 + ym = (lo[1] + hi[1]) * 0.5 + spannen.append((ob.name, xm, ym, lo, hi)) + + print("%-24s %7d %7d %7d %8.0f%% %9.3f %9.3f" + % (ob.name, rinde, laub, tris, quote, xm, ym)) if len(m.materials) != 2: fails.append("%s: %d Slots, erwartet 2" % (ob.name, len(m.materials))) @@ -160,18 +187,28 @@ if blatt_zahlen and blatt_zahlen[0] > 0: "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. +# Das ist der Test fuer den aus UE gemeldeten Fehler: LOD1-3 sassen bei MINUS +# der Standposition, weil ihre Weltmatrix nie ins Mesh gebacken wurde. +print("") +for name, xm, ym, lo, hi in spannen: + if abs(xm) > 0.25 or abs(ym) > 0.25: + fails.append("%s: Mitte bei %.2f/%.2f statt am Ursprung - die Stufe " + "traegt die Standposition des Baums (%.1f/%.1f) mit" + % (name, xm, ym, STANDORT[0], STANDORT[1])) 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])) + "wuerde beim LOD-Wechsel springen" % (name, lo[2])) + +# Und die Stufen muessen ZUEINANDER passen, nicht nur je einzeln zum Ursprung. +if len(spannen) > 1: + _, x0, y0, lo0, hi0 = spannen[0] + for name, xm, ym, lo, hi in spannen[1:]: + if abs(xm - x0) > 0.30 or abs(ym - y0) > 0.30: + fails.append("%s liegt %.2f/%.2f neben LOD0 - beim LOD-Wechsel " + "wuerde der Baum springen" % (name, xm - x0, ym - y0)) + print("Versatz zu LOD0: %s" + % ", ".join("%s %.3f/%.3f" % (n.split("_")[-1], x - x0, y - y0) + for n, x, y, _, _ in spannen[1:])) print("") if fails: diff --git a/tests/test_leaves.py b/tests/test_leaves.py index c3c877e..cbd0483 100644 --- a/tests/test_leaves.py +++ b/tests/test_leaves.py @@ -29,7 +29,11 @@ card=bpy.data.objects.new("Diamon Plane", me); bpy.context.collection.objects.li 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 +# Groesse und Dichte aus den Defaults nehmen. Frueher stand hier fest +# leaf_size=0.22 - der alte Blaettchen-Wert. Zusammen mit der neuen Dichte 170 +# ergab das genau die Kombination, die wir loswerden wollten: hunderte winziger +# Cards (gemessen 2778 Blatt-Tris statt ~1000). +s.leaf_card=card 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') @@ -41,9 +45,15 @@ 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)) +# Budget aus den Einstellungen holen, nicht fest verdrahten: es steht seit +# ASSETS.md Teil 6 pro Wachstumsstufe (S1 800 / S2 1200 / S3+S4 4000) und wurde +# von 2500 angehoben, weil bei ISM-Instanzen mit LOD-Kette nicht die Dreiecke +# der Engpass sind, sondern Overdraw und Schatten der Cards. +budget = s.leaf_budget +print("TRIS Stamm=%d Blatt=%d Summe=%d / %d %s (Dichte %.0f)" + % (tt,tl,tt+tl,budget,"OK" if tt+tl<=budget else "UEBER", s.leaf_density)) if tl==0: fails.append("blattwerk_leer") -if tt+tl>2500: fails.append("budget") +if tt+tl>budget: 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