diff --git a/blender_manifest_tree.toml b/blender_manifest_tree.toml index 58aa4af..9e7187d 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.5.0" +version = "1.6.0" name = "Stylized Tree Generator" tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen" maintainer = "D4rkst3r" diff --git a/dist/index.json b/dist/index.json index d170991..e593669 100644 --- a/dist/index.json +++ b/dist/index.json @@ -32,7 +32,7 @@ "id": "stylized_tree_generator", "name": "Stylized Tree Generator", "tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen", - "version": "1.5.0", + "version": "1.6.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -48,9 +48,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_tree_generator-1.5.0.zip", - "archive_size": 11772, - "archive_hash": "sha256:fe45391647074729afaac194801ab55c0d004c38f36b94737fdd6715e9c9501c" + "archive_url": "./stylized_tree_generator-1.6.0.zip", + "archive_size": 12916, + "archive_hash": "sha256:48d7072d65d44c944fe7dd160147f37f496132d6c2583c5251f0d6aba499476b" } ] } \ No newline at end of file diff --git a/dist/stylized_tree_generator-1.5.0.zip b/dist/stylized_tree_generator-1.5.0.zip deleted file mode 100644 index 8d87d57..0000000 Binary files a/dist/stylized_tree_generator-1.5.0.zip and /dev/null differ diff --git a/dist/stylized_tree_generator-1.6.0.zip b/dist/stylized_tree_generator-1.6.0.zip new file mode 100644 index 0000000..bd362ce Binary files /dev/null and b/dist/stylized_tree_generator-1.6.0.zip differ diff --git a/stylized_tree_generator.py b/stylized_tree_generator.py index 907d053..ea2e5c2 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, 5, 0), + "version": (1, 6, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Tree Gen", "description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen", @@ -46,6 +46,9 @@ DEFAULTS = { "Sides": 6, "UV Scale": 1.0, # Rinden-Dichte; UVs sind world-space (m) "Tip Blunt": 0.18, # >0 verhindert Nadelspitzen (0 = spitz, 0.45 = Kaktus) + "Branch Collar": 0.4, # Ansatz-Verdickung: Uebergang Ast->Stamm (0 = aus) + "Merge": 0.0, # 1 = Aeste per Voxel-Remesh mit dem Stamm verschmelzen + "Merge Voxel": 0.03, # Voxelgroesse (m): klein = feiner, aber teuer "Ribs": 0.0, # senkrechte Kanneluren (0 = glatt); Kaktus ~9 "Rib Depth": 0.0, } @@ -150,6 +153,9 @@ def build_group(): add_in("Sides", 'NodeSocketInt', DEFAULTS["Sides"], 3, 32) add_in("Tip Blunt", 'NodeSocketFloat', DEFAULTS["Tip Blunt"], 0.0, 0.9) add_in("UV Scale", 'NodeSocketFloat', DEFAULTS["UV Scale"], 0.01, 20.0) + add_in("Branch Collar", 'NodeSocketFloat', DEFAULTS["Branch Collar"], 0.0, 3.0) + add_in("Merge", 'NodeSocketFloat', DEFAULTS["Merge"], 0.0, 1.0) + add_in("Merge Voxel", 'NodeSocketFloat', DEFAULTS["Merge Voxel"], 0.005, 0.2) add_in("Ribs", 'NodeSocketFloat', DEFAULTS["Ribs"], 0.0, 20.0) add_in("Rib Depth", 'NodeSocketFloat', DEFAULTS["Rib Depth"], 0.0, 0.5) @@ -226,17 +232,46 @@ def build_group(): # multiplizieren. Wirkt der Mindestwert nur auf die Kuppel, bleibt der bereits # verjuengte Radius trotzdem winzig — gemessen: Sub-Ast-Spitze 0.8 mm bei # 31 mm Basis, also weiterhin eine Nadel. - def _shape(inv_taper_socket, dome_node, base_socket, x, y): + def _shape(inv_taper_socket, dome_node, base_socket, x, y, + spar_node=None, collar=False): mul = N("ShaderNodeMath"); mul.location = (x, y); mul.operation = 'MULTIPLY' L(inv_taper_socket, mul.inputs[0]); L(dome_node.outputs[0], mul.inputs[1]) mx = N("ShaderNodeMath"); mx.location = (x + 150, y); mx.operation = 'MAXIMUM' L(mul.outputs[0], mx.inputs[0]); L(V["Tip Blunt"], mx.inputs[1]) - r = N("ShaderNodeMath"); r.location = (x + 300, y); r.operation = 'MULTIPLY' - L(base_socket, r.inputs[0]); L(mx.outputs[0], r.inputs[1]) + src = mx + if collar and spar_node is not None: + # Ansatz-Verdickung ("Collar"): der Ast wird an seiner Basis breiter + # und laeuft schnell aus -> der Uebergang zum Stamm sieht gewachsen + # aus statt wie eine durchgesteckte Roehre. Kostet KEINE Verts. + ci = N("ShaderNodeMath"); ci.location = (x, y - 160); ci.operation = 'SUBTRACT' + ci.inputs[0].default_value = 1.0 + L(spar_node.outputs["Factor"], ci.inputs[1]) + cp = N("ShaderNodeMath"); cp.location = (x + 150, y - 160); cp.operation = 'POWER' + cp.inputs[1].default_value = 6.0 # faellt schnell ab -> kein Trichter + L(ci.outputs[0], cp.inputs[0]) + cm = N("ShaderNodeMath"); cm.location = (x + 300, y - 160); cm.operation = 'MULTIPLY' + L(V["Branch Collar"], cm.inputs[0]); L(cp.outputs[0], cm.inputs[1]) + ca = N("ShaderNodeMath"); ca.location = (x + 450, y - 160); ca.operation = 'ADD' + ca.inputs[0].default_value = 1.0 + L(cm.outputs[0], ca.inputs[1]) + cf = N("ShaderNodeMath"); cf.location = (x + 450, y - 60); cf.operation = 'MULTIPLY' + L(mx.outputs[0], cf.inputs[0]); L(ca.outputs[0], cf.inputs[1]) + src = cf + r = N("ShaderNodeMath"); r.location = (x + 620, y); r.operation = 'MULTIPLY' + L(base_socket, r.inputs[0]); L(src.outputs[0], r.inputs[1]) return r tdome = _dome(spar, -900, 220) - trad = _shape(inv.outputs[0], tdome, V["Trunk Radius"], -260, 60) + # Stammspitze duenner auslaufen lassen als die Aeste: sonst endet der Stamm + # mit einem sichtbaren flachen Deckel. Ein eigener, kleinerer Mindestwert. + ttip = N("ShaderNodeMath"); ttip.location = (-420, -80); ttip.operation = 'MULTIPLY' + L(V["Tip Blunt"], ttip.inputs[0]); ttip.inputs[1].default_value = 0.3 + tmulr = N("ShaderNodeMath"); tmulr.location = (-260, 60); tmulr.operation = 'MULTIPLY' + L(inv.outputs[0], tmulr.inputs[0]); L(tdome.outputs[0], tmulr.inputs[1]) + tmaxr = N("ShaderNodeMath"); tmaxr.location = (-110, 60); tmaxr.operation = 'MAXIMUM' + L(tmulr.outputs[0], tmaxr.inputs[0]); L(ttip.outputs[0], tmaxr.inputs[1]) + trad = N("ShaderNodeMath"); trad.location = (40, 60); trad.operation = 'MULTIPLY' + L(V["Trunk Radius"], trad.inputs[0]); L(tmaxr.outputs[0], trad.inputs[1]) setrad = N("GeometryNodeSetCurveRadius"); setrad.location = (-80, 200) L(_out(setpos, "Geometry"), _sock(setrad, "Curve")) @@ -283,7 +318,8 @@ def build_group(): bbase = N("ShaderNodeMath"); bbase.location = (600, -140); bbase.operation = 'MULTIPLY' L(V["Trunk Radius"], bbase.inputs[0]); L(V["Branch Thickness"], bbase.inputs[1]) bdome = _dome(bspar, 600, -600) - bscl = _shape(binv.outputs[0], bdome, bbase.outputs[0], 780, -140) + bscl = _shape(binv.outputs[0], bdome, bbase.outputs[0], 780, -140, + spar_node=bspar, collar=True) bsetr = N("GeometryNodeSetCurveRadius"); bsetr.location = (600, 40) L(_out(bres, "Curve"), _sock(bsetr, "Curve")) @@ -436,7 +472,8 @@ def build_group(): L(V["Trunk Radius"], sbase.inputs[0]) sbase.inputs[1].default_value = 0.12 # Zweige sind DUENN ... sdome = _dome(sspar, 2550, 1200) - srad2 = _shape(sinv.outputs[0], sdome, sbase.outputs[0], 2750, 900) + srad2 = _shape(sinv.outputs[0], sdome, sbase.outputs[0], 2750, 900, + spar_node=sspar, collar=True) ssetr = N("GeometryNodeSetCurveRadius"); ssetr.location = (2550, 760) L(_out(sres, "Curve"), _sock(ssetr, "Curve")) L(srad2.outputs[0], _sock(ssetr, "Radius")) @@ -605,8 +642,38 @@ def build_group(): _sock(stuv, "Name").default_value = "UVMap" L(uvvec.outputs[0], _sock(stuv, "Value")) - shade = N("GeometryNodeSetShadeSmooth"); shade.location = (1650, 200) - L(_out(stuv, "Geometry"), _sock(shade, "Geometry")) + # ---------- optional: Aeste mit dem Stamm VERSCHMELZEN ---------- + # Voxel-Remesh (Mesh to Volume -> Volume to Mesh) macht aus den sich + # durchdringenden Roehren EINE organische Oberflaeche mit weichen Uebergaengen. + # ACHTUNG, ehrlicher Preis: der Remesh erzeugt komplett neue Topologie -> + # die UVs sind danach WEG und der Vert-Count steigt deutlich. Deshalb als + # Schalter (Merge = 0 laesst den schnellen, UV-tauglichen Pfad unberuehrt). + m2v = N("GeometryNodeMeshToVolume"); m2v.location = (1650, -200) + L(_out(stuv, "Geometry"), _sock(m2v, "Mesh")) + try: + _sock(m2v, "Resolution Mode").default_value = 'Size' + except Exception: + pass + L(V["Merge Voxel"], _sock(m2v, "Voxel Size")) + v2m = N("GeometryNodeVolumeToMesh"); v2m.location = (1820, -200) + L(_out(m2v, "Volume", "Geometry"), _sock(v2m, "Volume")) + try: + _sock(v2m, "Resolution Mode").default_value = 'Size' + except Exception: + pass + L(V["Merge Voxel"], _sock(v2m, "Voxel Size")) + + pick = N("GeometryNodeSwitch"); pick.location = (2000, 100) + pick.input_type = 'GEOMETRY' + mgt = N("ShaderNodeMath"); mgt.location = (1820, 60); mgt.operation = 'GREATER_THAN' + mgt.inputs[1].default_value = 0.5 + L(V["Merge"], mgt.inputs[0]) + L(mgt.outputs[0], _sock(pick, "Switch")) + L(_out(stuv, "Geometry"), _sock(pick, "False")) + L(_out(v2m, "Mesh", "Geometry"), _sock(pick, "True")) + + shade = N("GeometryNodeSetShadeSmooth"); shade.location = (2200, 200) + L(_out(pick, "Output"), _sock(shade, "Geometry")) L(_out(shade, "Geometry"), gout.inputs[0]) return ng