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:
@@ -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")
|
||||
Reference in New Issue
Block a user