WIP Kronen-Huelle: Kugeln verschmelzen statt Einzelkappen
NICHT auf main - ein Kriterium steht noch offen (siehe unten). 26 von 27 Tests gruen. Die Cards sitzen nicht mehr auf je einer eigenen Halbkugel je Cluster, sondern auf EINER verschmolzenen Kronen-Huelle: Kugeln auf die Zweige, Mesh to Volume, Volume to Mesh, dann Distribute Points on Faces auf der Huelle. Das ist der Standard-Ablauf aus den Produktions-Breakdowns (80.lv Meadows, The Witness), den wir uebersprungen hatten. Wirkung: geschlossene Kronen, die Aeste verschwinden im Inneren, keine Polster-Grenzen und keine nackten Spitzen mehr. 381 Cards an der Eiche in 0.9 s. Drei Befunde auf dem Weg, alle gemessen statt geraten: 1) ast_id kam nur an 3 von 10 Aesten an. Ursache war NICHT das Sampling, sondern die Eingabe: es existierten nur VIER Cluster-Zentren auf drei Aesten. In der alten Bauform regelte die Bisektion die Cluster-Dichte selbst auf ~550 hoch; seit sie stattdessen die Cards auf der Huelle regelt, muss der Startwert stimmen. Sichtbar gemacht mit Points to Vertices - to_mesh() auf eine reine Punktwolke liefert eine leere Mesh und misst nichts. 2) S1 stand komplett kahl. Die Reihe ueber die Stufen zeigte 0 / 20 / 41 / 109 Zentren - allmaehlicher Abfall bis null, keine Klippe. Also die Cluster-Dichte, nicht die Voxel-Groesse. "Dichte" ist Kugeln je m2 Zweigflaeche, und die schrumpft mit dem Quadrat der Groesse; sie laeuft jetzt mit 1/g^2 gegen die Baumgroesse, damit die ANZAHL ueber die Stufen konstant bleibt. S1 hat jetzt 8 Cluster und 58 Cards. 3) 31 % der Cards lagen unter dem Kronenansatz. Die Verschmelzung weitet die Form um bis zu einen Kugelradius nach unten, der Filter griff aber nur auf den Zentren. Jetzt auch auf der Huelle. OFFEN: test_obstbaum, u/m 0.75-0.80 gegen die Schranke 0.82. Die stammt aus der Einzelkappen-Aera. Bevor dagegen kalibriert wird, muss geklaert sein, ob die Zahl unter der neuen Bauform noch dasselbe bedeutet - genau wie beim Kugel-Kennwert, der sich als geerbte Konstante herausstellte. Die ehrliche Antwort ist vermutlich ein neu gemessenes Band, relativ zur Eiche aus demselben Lauf. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+144
-43
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user