diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 2d4525c..dcad1ae 100644 --- a/stylized_tree_generator.py +++ b/stylized_tree_generator.py @@ -1381,8 +1381,22 @@ LEAF_DEFAULTS = { # CLUSTER-Zentren, nicht mehr einzelne Cards - jedes Zentrum traegt SECHS # Cards auf seiner Halbkugel. Die Dichte liegt entsprechend niedriger; die # Bisektion regelt sie ohnehin auf die Ziel-Cardzahl ein. - "Density": 30.0, + # Cards je m2 KRONEN-HUELLE. Die Bisektion regelt sie auf die Ziel-Cardzahl + # ein; der Startwert muss nur die Groessenordnung treffen. + "Density": 12.0, "Cluster Radius": 0.45, + # Kugeln je m2 Zweigflaeche - bestimmt, wie klumpig die Huelle wird. + # + # Der Wert sieht hoch aus, ist es aber nicht: die zulaessige ZWEIGflaeche + # eines Baums ist winzig (duenne Zweige oberhalb des Kronenansatzes), und + # die Spitzen-Gewichtung nimmt nochmal davon weg. Mit 30 entstanden + # gemessen VIER Kugeln auf drei Aesten - die Huelle war ein Klumpen, und + # sieben Aeste bekamen beim Faellen kein Laub. In der alten Bauform regelte + # die Bisektion diese Dichte selbst auf rund 550 hoch; seit sie stattdessen + # die Cards auf der Huelle regelt, muss der Startwert hier stimmen. + "Cluster Density": 550.0, + # Aufloesung der Verschmelzung, in Metern. + "Voxel Size": 0.12, # 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 @@ -1445,10 +1459,19 @@ def build_leaf_group(rebuild=False): 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) - # Radius der Bueschel-Kappe um jedes Cluster-Zentrum. Siehe den Block - # "BUESCHEL-SCHALE" weiter unten. + # Radius der Kugeln, aus denen die Kronen-Huelle verschmilzt. Siehe den + # Block "KRONEN-HUELLE" weiter unten. add_in("Cluster Radius", 'NodeSocketFloat', LEAF_DEFAULTS["Cluster Radius"], 0.02, 5.0) + # Wie dicht die Kugeln auf den Zweigen sitzen. Getrennt von "Density": + # die regelt jetzt die CARDS auf der fertigen Huelle, diese hier die Zahl + # der Kugeln, aus denen die Huelle ueberhaupt entsteht. + add_in("Cluster Density", 'NodeSocketFloat', + LEAF_DEFAULTS["Cluster Density"], 0.0, 2000.0) + # Aufloesung der Verschmelzung. Gross = klumpig und billig, klein = feine + # Kontur und teuer. Der eigentliche Kostenregler dieser Bauform. + add_in("Voxel Size", 'NodeSocketFloat', + LEAF_DEFAULTS["Voxel Size"], 0.02, 2.0) gin = N("NodeGroupInput"); gin.location = (-900, 0) gout = N("NodeGroupOutput"); gout.location = (900, 0) @@ -1570,7 +1593,7 @@ def build_leaf_group(rebuild=False): wmap.inputs["To Max"].default_value = 0.02 wmul = N("ShaderNodeMath"); wmul.location = (-320, 300) wmul.operation = 'MULTIPLY' - L(V["Density"], wmul.inputs[0]) + L(V["Cluster Density"], wmul.inputs[0]) L(wmap.outputs["Result"], wmul.inputs[1]) dist = N("GeometryNodeDistributePointsOnFaces"); dist.location = (-320, 200) @@ -1621,40 +1644,29 @@ def build_leaf_group(rebuild=False): L(_out(lna, "Attribute"), _sock(lstore, "Value")) # ------------------------------------------------------------------ - # BUESCHEL-SCHALE: aus jedem gestreuten Punkt wird ein Cluster-ZENTRUM, - # und die Cards sitzen auf der oberen Halbkugel darum herum. + # KRONEN-HUELLE: die Kugeln VERSCHMELZEN erst, dann wird auf der + # gemeinsamen Oberflaeche gestreut. # - # Vorher sassen die Cards direkt auf der Astkurve. Aus der Naehe sah man - # deshalb nackte Ast-Enden mit einem Mini-Bueschel daran - das Laub folgte - # der Linie des Astes statt ein Polster zu bilden. Jetzt bildet jeder - # Cluster eine Kappe, die AUF dem Ast sitzt; die Astspitze verschwindet - # darin, weil das Zentrum auf ihr liegt und die Schale sie ueberdeckt. + # Vorher bekam jeder Cluster seine eigene Halbkugel und wurde einzeln + # bestueckt. Die Polster blieben dadurch als getrennte Koerper sichtbar - + # ein Laubhandschuh je Ast, im Spiel als "behaarte Finger" gemeldet. Mehr + # oder groessere Cards heilen das nicht: das Problem ist die STREUFLAECHE, + # nicht ihre Belegung. # - # Untere Halbkugel weglassen: von unten sieht man die Krone gegen den - # Himmel, dort braucht es keine Cards - und es waere die Haelfte des - # Overdraws fuer nichts. - # Ikosaeder: 12 Vertices, davon 6 auf der oberen Haelfte - also SECHS Cards - # je Cluster. "Subdivisions" 0 und 1 liefern beide das Ikosaeder (gemessen), - # erst 2 unterteilt auf 42. Sechs reicht: mehr Cards je Cluster heisst bei - # festem Budget weniger Cluster, und dann bleiben Aeste ganz ohne Polster. - ico = N("GeometryNodeMeshIcoSphere"); ico.location = (-320, 420) - _sock(ico, "Radius").default_value = 1.0 - _sock(ico, "Subdivisions").default_value = 1 + # Der Weg ist der Standard aus den Produktions-Breakdowns (80.lv "Meadows", + # "The Witness"-Baeume): Kugeln auf die Aeste setzen, zu EINER Kronenmasse + # verschmelzen, Cards ueber deren Oberflaeche streuen. Die Aeste + # verschwinden dabei im Inneren der Krone. + # + # Verschmolzen wird ueber Volumen (Mesh to Volume -> Volume to Mesh) - das + # ist die Metaball-Wirkung: sich beruehrende Kugeln werden zu einem Koerper + # mit weichem Uebergang, statt sich zu durchdringen. + kugel = N("GeometryNodeMeshUVSphere"); kugel.location = (-320, 460) + _sock(kugel, "Segments").default_value = 8 + _sock(kugel, "Rings").default_value = 6 + _sock(kugel, "Radius").default_value = 1.0 - spos = N("GeometryNodeInputPosition"); spos.location = (-320, 560) - ssep = N("ShaderNodeSeparateXYZ"); ssep.location = (-180, 560) - L(spos.outputs[0], ssep.inputs[0]) - soben = N("FunctionNodeCompare"); soben.location = (-40, 560) - soben.data_type = 'FLOAT'; soben.operation = 'GREATER_THAN' - L(ssep.outputs["Z"], soben.inputs[0]) - # Knapp unter den Aequator, damit die Kappe seitlich geschlossen wirkt. - soben.inputs[1].default_value = -0.15 - - m2p = N("GeometryNodeMeshToPoints"); m2p.location = (-40, 420) - L(_out(ico, "Mesh"), _sock(m2p, "Mesh")) - L(soben.outputs[0], _sock(m2p, "Selection")) - - # Cluster-Radius mit Streuung, damit die Polster nicht uniform wirken. + # Radius mit Streuung, damit die Krone keine Perlenkette wird. rrad = N("FunctionNodeRandomValue"); rrad.location = (-180, 320) rrad.data_type = 'FLOAT' L(V["Seed"], _sock(rrad, "Seed")) @@ -1664,15 +1676,88 @@ def build_leaf_group(rebuild=False): L(rlo.outputs[0], rrad.inputs[2]) L(V["Cluster Radius"], rrad.inputs[3]) - schale = N("GeometryNodeInstanceOnPoints"); schale.location = (120, 420) - L(_out(lstore, "Geometry"), _sock(schale, "Points")) - L(_out(m2p, "Points"), _sock(schale, "Instance")) - L(rrad.outputs[1], _sock(schale, "Scale")) - sreal = N("GeometryNodeRealizeInstances"); sreal.location = (300, 420) - L(_out(schale, "Instances", "Geometry"), _sock(sreal, "Geometry")) + ballen = N("GeometryNodeInstanceOnPoints"); ballen.location = (120, 460) + L(_out(lstore, "Geometry"), _sock(ballen, "Points")) + L(_out(kugel, "Mesh"), _sock(ballen, "Instance")) + L(rrad.outputs[1], _sock(ballen, "Scale")) + breal = N("GeometryNodeRealizeInstances"); breal.location = (280, 460) + L(_out(ballen, "Instances", "Geometry"), _sock(breal, "Geometry")) - inst = N("GeometryNodeInstanceOnPoints"); inst.location = (100, 200) - L(_out(sreal, "Geometry"), _sock(inst, "Points")) + m2v = N("GeometryNodeMeshToVolume"); m2v.location = (420, 460) + L(_out(breal, "Geometry"), _sock(m2v, "Mesh")) + try: + m2v.resolution_mode = 'VOXEL_SIZE' + except (AttributeError, TypeError): + pass + L(V["Voxel Size"], _sock(m2v, "Voxel Size")) + + v2m = N("GeometryNodeVolumeToMesh"); v2m.location = (560, 460) + L(_out(m2v, "Volume"), _sock(v2m, "Volume")) + try: + v2m.resolution_mode = 'VOXEL_SIZE' + except (AttributeError, TypeError): + pass + L(V["Voxel Size"], _sock(v2m, "Voxel Size")) + + # Cards auf der HUELLE streuen. "Density" regelt jetzt hier - die Bisektion + # trifft die Ziel-Cardzahl damit direkt, ohne den Umweg ueber die + # Cluster-Zahl und ihre feste Kartenzahl je Cluster. + # Der KRONENANSATZ-Filter muss auch auf der Huelle greifen, nicht nur auf + # den Zentren. Die Verschmelzung weitet die Form um bis zu einen + # Kugelradius nach unten - gemessen lagen dadurch 31 % der Cards unter dem + # Ansatz und die tiefste bei 18 % der Hoehe statt bei 40 %. Das Laub klebte + # wieder am Stamm, der Fehler, gegen den der Filter ueberhaupt gebaut wurde. + hpos3 = N("GeometryNodeInputPosition"); hpos3.location = (560, 620) + hsep3 = N("ShaderNodeSeparateXYZ"); hsep3.location = (700, 620) + L(hpos3.outputs[0], hsep3.inputs[0]) + hcmp3 = N("FunctionNodeCompare"); hcmp3.location = (840, 620) + hcmp3.data_type = 'FLOAT'; hcmp3.operation = 'GREATER_THAN' + L(hsep3.outputs["Z"], hcmp3.inputs[0]) + L(bschwelle.outputs[0], hcmp3.inputs[1]) + + hdist = N("GeometryNodeDistributePointsOnFaces"); hdist.location = (700, 460) + L(_out(v2m, "Mesh"), _sock(hdist, "Mesh")) + L(hcmp3.outputs[0], _sock(hdist, "Selection")) + L(V["Density"], _sock(hdist, "Density")) + L(V["Seed"], _sock(hdist, "Seed")) + + # AST-ID zurueckholen. Die Verschmelzung erzeugt frische Geometrie und + # verliert JEDES Attribut - ohne diesen Schritt wuesste keine Card mehr, + # zu welchem Ast sie gehoert, und die Faell-Teile zerfielen falsch. + # + # Die Zuordnung ist hier zwangslaeufig raeumlich: ein Punkt auf der + # verschmolzenen Huelle gehoert nicht mehr per Konstruktion zu einem Ast. + # Gefragt wird deshalb das naechste CLUSTER-ZENTRUM - das ist die Kugel, + # aus der dieses Stueck Huelle entstanden ist, also die bestmoegliche + # Antwort. Die Blatt-Bilanz im Test prueft, ob die Summe stimmt. + snear = N("GeometryNodeSampleNearest"); snear.location = (700, 300) + # DOMAIN setzen. Ohne das sucht der Knoten in der FACE-Domaene; die + # Cluster-Zentren sind aber eine reine Punktwolke ohne Faces, und die Suche + # lief ins Leere - gemessen kamen nur 3 von 10 Ast-IDs an den Cards an, + # sieben Aeste waeren beim Faellen ohne Laub abgefallen. + snear.domain = 'POINT' + L(_out(lstore, "Geometry"), _sock(snear, "Geometry")) + hpos2 = N("GeometryNodeInputPosition"); hpos2.location = (560, 300) + L(hpos2.outputs[0], _sock(snear, "Sample Position")) + + sidx = N("GeometryNodeSampleIndex"); sidx.location = (840, 300) + sidx.data_type = 'INT' + sidx.domain = 'POINT' + L(_out(lstore, "Geometry"), _sock(sidx, "Geometry")) + lna2 = N("GeometryNodeInputNamedAttribute"); lna2.location = (700, 220) + lna2.data_type = 'INT' + _sock(lna2, "Name").default_value = "ast_id" + L(_out(lna2, "Attribute"), _sock(sidx, "Value")) + L(_out(snear, "Index"), _sock(sidx, "Index")) + + hstore = N("GeometryNodeStoreNamedAttribute"); hstore.location = (1000, 460) + hstore.domain = 'POINT'; hstore.data_type = 'INT' + L(_out(hdist, "Points"), _sock(hstore, "Geometry")) + _sock(hstore, "Name").default_value = "ast_id" + L(_out(sidx, "Value"), _sock(hstore, "Value")) + + inst = N("GeometryNodeInstanceOnPoints"); inst.location = (1160, 460) + L(_out(hstore, "Geometry"), _sock(inst, "Points")) L(_out(quelle, "Output"), _sock(inst, "Instance")) try: L(V["Use Collection"], _sock(inst, "Pick Instance")) @@ -2054,6 +2139,22 @@ def make_leaves(context, tree_obj, card_obj, name=None, card_coll=None, # Card-Groesse, nur eine Ebene hoeher. c_faktor = LEAF_CLUSTER_PRESET.get(tree_obj.get("preset") or "", 1.0) overrides["Cluster Radius"] = basis["Cluster Radius"] * g_anteil * c_faktor + # Die Voxel-Groesse muss mit der Kappe mitwachsen, sonst kostet der + # Setzling dieselbe Gitteraufloesung wie der Altbaum - und der Altbaum + # bekommt eine zu grobe Huelle. + overrides["Voxel Size"] = basis["Voxel Size"] * g_anteil * c_faktor + # Die CLUSTER-Dichte muss GEGEN die Baumgroesse laufen, nicht mit ihr. + # + # "Dichte" ist Kugeln je m2 Zweigflaeche, und die Flaeche schrumpft mit + # dem Quadrat der Groesse. Bei fester Dichte bleiben am Setzling NULL + # Kugeln uebrig - gemessen ueber die Stufen 0 / 20 / 41 / 109 Zentren, + # S1 stand ohne Huelle und damit ohne ein einziges Blatt da. + # + # Mit 1/g^2 bleibt die ANZAHL ueber die Stufen etwa konstant, was die + # richtige Groesse ist: eine Krone braucht eine bestimmte Zahl Ballen, + # damit sie verschmilzt - nicht eine bestimmte Dichte. + overrides["Cluster Density"] = (basis["Cluster Density"] + / max(g_anteil * g_anteil, 0.01)) name = name or (tree_obj.name + "_Leaf") ob = bpy.data.objects.get(name)