Install
$ agentstack add skill-max-786-claude-3d-harness-blender-geonodes ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Skill: Blender Geometry Nodes (Procedurale)
Sei un esperto di Geometry Nodes in Blender via Python. Costruisci node tree parametrici e non-distruttivi da codice.
Connessione — MCP (predefinito)
mcp__Blender__execute_blender_code(code="""
import bpy
# ... codice ...
result = {"ok": True}
""")
mcp__Blender__get_screenshot_of_window_as_image()
mcp__Blender__render_viewport_to_path(output_path="/gn.png")
IL PRINCIPIO — GeoNodes vs Python bmesh
| | bmesh (blender-arch/procedural) | Geometry Nodes | |-|----------------------------------|----------------| | Distruttivo | Sì — modifica la mesh | No — modifier on top | | Parametrico | No (dopo apply) | Sì — sliders sempre | | Istanziazione | Slow (N oggetti separati) | Fast (GPU instancing) | | Animabile | Solo con driver | Sì — natively | | Complessità API | Media | Alta (node graph) |
Usa GeoNodes quando:
- Il risultato deve rimanere modificabile (number of instances, density...)
- Stai istanziando molti oggetti (> 50)
- Vuoi animare i parametri
- La forma deriva da una curva o da una superficie esistente
SETUP BASE — Modifier + Node Group
import bpy
def create_gn_modifier(obj, name="GeoNodes"):
"""
Crea un modifier Geometry Nodes su obj e un node group vuoto.
Ritorna (modifier, node_group, nodes, links).
Blender 4.x/5.x: usa ng.interface.new_socket() per I/O,
NON ng.inputs.new() / ng.outputs.new() (deprecati in 4.0).
"""
mod = obj.modifiers.new(name, 'NODES')
ng = bpy.data.node_groups.new(name + "_Tree", 'GeometryNodeTree')
mod.node_group = ng
# Interfaccia I/O — ORDINE IMPORTANTE: OUTPUT prima di INPUT
if hasattr(ng, 'interface'):
ng.interface.new_socket("Geometry", in_out="OUTPUT",
socket_type="NodeSocketGeometry")
ng.interface.new_socket("Geometry", in_out="INPUT",
socket_type="NodeSocketGeometry")
# Nodi Group Input e Output (terminali del grafo)
in_node = ng.nodes.new("NodeGroupInput")
out_node = ng.nodes.new("NodeGroupOutput")
in_node.location = (-400, 0)
out_node.location = ( 400, 0)
# Pass-through di default (geometry → output invariata)
ng.links.new(in_node.outputs[0], out_node.inputs[0])
return mod, ng, ng.nodes, ng.links
# Uso base:
# bpy.ops.mesh.primitive_plane_add(size=2)
# plane = bpy.context.active_object
# mod, ng, nodes, links = create_gn_modifier(plane, "MyGeoNodes")
HELPER FUNCTIONS
def add_node(nodes, bl_idname, location=(0,0), **props):
"""
Aggiunge un nodo e imposta proprietà.
props: coppie nome=valore per inputs o attributi del nodo.
Esempi:
add_node(nodes, 'GeometryNodeMeshPrimitiveSphere',
location=(200, 0),
inputs={'Radius': 0.05})
add_node(nodes, 'FunctionNodeRandomValue',
location=(0, -200),
data_type='FLOAT_VECTOR')
"""
n = nodes.new(bl_idname)
n.location = location
for k, v in props.items():
if k == 'inputs':
for input_name, val in v.items():
if input_name in n.inputs:
n.inputs[input_name].default_value = val
else:
setattr(n, k, v)
return n
def link(links, from_node, from_socket, to_node, to_socket):
"""
Collega due nodi. Accetta indici interi o nomi stringa per i socket.
Esempi:
link(links, dist, "Points", inst, "Points")
link(links, math, 0, out_node, 0) # per indice
"""
if isinstance(from_socket, int):
fs = from_node.outputs[from_socket]
else:
fs = from_node.outputs[from_socket]
if isinstance(to_socket, int):
ts = to_node.inputs[to_socket]
else:
ts = to_node.inputs[to_socket]
return links.new(fs, ts)
def add_group_input(ng, name, socket_type, default=None, min_val=None, max_val=None):
"""
Aggiunge un Group Input parametrico (slider visibile nel modifier).
socket_type: 'NodeSocketFloat' | 'NodeSocketInt' | 'NodeSocketVector'
'NodeSocketBool' | 'NodeSocketObject' | 'NodeSocketMaterial'
'NodeSocketGeometry' | 'NodeSocketColor'
Esempio:
add_group_input(ng, "Density", "NodeSocketFloat", default=500, min_val=0, max_val=5000)
add_group_input(ng, "Scale", "NodeSocketFloat", default=1.0, min_val=0.01, max_val=5.0)
add_group_input(ng, "Instance", "NodeSocketObject")
add_group_input(ng, "Seed", "NodeSocketInt", default=0)
Dopo averlo aggiunto, accedilo via in_node.outputs[name] nel grafo.
Il valore è modificabile via modifier nel Properties panel.
"""
sock = ng.interface.new_socket(name, in_out="INPUT", socket_type=socket_type)
if default is not None:
try: sock.default_value = default
except: pass
if min_val is not None:
try: sock.min_value = min_val
except: pass
if max_val is not None:
try: sock.max_value = max_val
except: pass
return sock
PATTERN 1 — Scatter istanze su superficie
Il pattern più comune: distribuisce copie di un oggetto su una mesh. Usato per: sprinkles su donut, erba su terreno, pietre su pavimento, foglie su rami, chiodi su tavola, bottoni su tessuto.
def scatter_on_surface(host_obj, instance_obj, density=500.0,
random_rotation=True, align_to_normal=True,
scale_min=0.8, scale_max=1.2, seed=0,
name="Scatter"):
"""
Scatter di instance_obj sulla superficie di host_obj.
density : istanze per m² [BU²]
random_rotation: ruota casualmente ogni istanza sull'asse normale
align_to_normal: orienta le istanze lungo la normale della superficie
scale_min/max : range scala casuale (1.0 = nessuna variazione)
seed : seed casuale — cambia per layout diverso
Pipeline: Geometry → Distribute Points on Faces →
Instance on Points → Rotate → Scale → Realize → Output
Esempi:
# Sprinkles su donut
scatter_on_surface(icing, sprinkle, density=3000, seed=42)
# Erba su terreno
scatter_on_surface(terrain, grass_blade, density=200,
scale_min=0.7, scale_max=1.5, seed=7)
# Pietre su pavimento
scatter_on_surface(floor, rock, density=50,
random_rotation=True, align_to_normal=False)
"""
mod, ng, nds, lks = create_gn_modifier(host_obj, name)
# Rimuovi il link pass-through di default
for l in list(lks): lks.remove(l)
in_nd = next(n for n in nds if n.bl_idname == "NodeGroupInput")
out_nd = next(n for n in nds if n.bl_idname == "NodeGroupOutput")
# Group Input parametrici
add_group_input(ng, "Density", "NodeSocketFloat",
default=density, min_val=0, max_val=10000)
add_group_input(ng, "Seed", "NodeSocketInt", default=seed)
# Distribute Points on Faces
dist = add_node(nds, "GeometryNodeDistributePointsOnFaces",
location=(0, 0), inputs={"Density": density})
dist.distribute_method = "RANDOM"
# Object Info (geometria dell'istanza)
obj_info = add_node(nds, "GeometryNodeObjectInfo", location=(0, -200))
obj_info.inputs["Object"].default_value = instance_obj
obj_info.transform_space = 'ORIGINAL'
# Instance on Points
inst = add_node(nds, "GeometryNodeInstanceOnPoints", location=(300, 0))
cur_x = 500
last_out = ("Instances", inst)
if align_to_normal:
lks.new(dist.outputs["Normal"], inst.inputs["Rotation"])
# Rotazione casuale
if random_rotation:
rand_rot = add_node(nds, "FunctionNodeRandomValue",
location=(0, -400))
rand_rot.data_type = "FLOAT_VECTOR"
rand_rot.inputs["Min"].default_value = (0, 0, 0)
rand_rot.inputs["Max"].default_value = (6.2832, 6.2832, 6.2832)
rot = add_node(nds, "GeometryNodeRotateInstances",
location=(cur_x, 0))
lks.new(last_out[1].outputs[last_out[0]], rot.inputs["Instances"])
lks.new(rand_rot.outputs[0], rot.inputs["Rotation"])
last_out = ("Instances", rot)
cur_x += 200
# Scala casuale
if scale_min != 1.0 or scale_max != 1.0:
rand_sc = add_node(nds, "FunctionNodeRandomValue",
location=(cur_x - 200, -300))
rand_sc.data_type = "FLOAT"
rand_sc.inputs[2].default_value = scale_min # Min float
rand_sc.inputs[3].default_value = scale_max # Max float
sc_inst = add_node(nds, "GeometryNodeScaleInstances",
location=(cur_x, 0))
lks.new(last_out[1].outputs[last_out[0]], sc_inst.inputs["Instances"])
lks.new(rand_sc.outputs[1], sc_inst.inputs["Scale"])
last_out = ("Instances", sc_inst)
cur_x += 200
# Realize Instances (converte in mesh reale)
realize = add_node(nds, "GeometryNodeRealizeInstances",
location=(cur_x, 0))
# Join Geometry (mantiene la superficie host + le istanze)
join = add_node(nds, "GeometryNodeJoinGeometry",
location=(cur_x + 200, 0))
# Collega tutto
lks.new(in_nd.outputs[0], dist.inputs["Mesh"])
lks.new(in_nd.outputs["Density"], dist.inputs["Density"])
lks.new(dist.outputs["Points"], inst.inputs["Points"])
lks.new(obj_info.outputs["Geometry"], inst.inputs["Instance"])
lks.new(last_out[1].outputs[last_out[0]], realize.inputs["Geometry"])
lks.new(in_nd.outputs[0], join.inputs["Geometry"])
lks.new(realize.outputs["Geometry"], join.inputs["Geometry"])
lks.new(join.outputs["Geometry"], out_nd.inputs[0])
return mod, ng
PATTERN 2 — Curve to Mesh (tubo da curva)
Crea tubi, cavi, cornici, tubi idraulici da curve Bezier/NURBS. Parametrico: cambia il profilo o la curva e il tubo si aggiorna.
def curve_to_pipe(curve_obj, profile_radius=0.02, resolution=12,
name="CurvePipe"):
"""
Genera un tubo circolare lungo una curva con Geometry Nodes.
curve_obj : oggetto curva Bezier/NURBS/Poly
profile_radius : raggio del tubo [BU]
resolution : divisioni angolari della sezione circolare
Pipeline: Curve Input → Curve to Mesh (con Circle profile) → Output
Più flessibile di blender-arch pipe_along_points perché:
- Il profilo può essere qualsiasi curva (ovale, quadrato...)
- Tutto è non-distruttivo e animabile
- La risoluzione è regolabile dopo creazione
Esempi:
# Tubo idraulico
curve_to_pipe(pipe_curve, profile_radius=0.015)
# Cavo elettrico (più sottile)
curve_to_pipe(cable_curve, profile_radius=0.004, resolution=8)
# Cornice architettonica (profilo rettangolare → usa curve_to_profile)
curve_to_pipe(cornice_curve, profile_radius=0.05)
"""
mod, ng, nds, lks = create_gn_modifier(curve_obj, name)
for l in list(lks): lks.remove(l)
in_nd = next(n for n in nds if n.bl_idname == "NodeGroupInput")
out_nd = next(n for n in nds if n.bl_idname == "NodeGroupOutput")
# Group Input per raggio (parametrico)
add_group_input(ng, "Radius", "NodeSocketFloat",
default=profile_radius, min_val=0.001, max_val=1.0)
# Curve Circle (profilo circolare)
circle = add_node(nds, "GeometryNodeCurvePrimitiveCircle",
location=(0, -200),
inputs={"Resolution": resolution,
"Radius": profile_radius})
circle.mode = 'RADIUS'
# Curve to Mesh
c2m = add_node(nds, "GeometryNodeCurveToMesh", location=(300, 0))
c2m.inputs["Fill Caps"].default_value = True
# Set Shade Smooth
smooth = add_node(nds, "GeometryNodeSetShadeSmooth", location=(500, 0))
smooth.inputs["Shade Smooth"].default_value = True
lks.new(in_nd.outputs[0], c2m.inputs["Curve"])
lks.new(in_nd.outputs["Radius"], circle.inputs["Radius"])
lks.new(circle.outputs["Curve"], c2m.inputs["Profile Curve"])
lks.new(c2m.outputs["Mesh"], smooth.inputs["Geometry"])
lks.new(smooth.outputs["Geometry"], out_nd.inputs[0])
return mod, ng
def curve_to_profile(curve_obj, profile_curve_obj, name="CurveProfile"):
"""
Estrue un profilo personalizzato lungo una curva.
profile_curve_obj: curva 2D che definisce la sezione (cornice, binario...)
Esempio:
# Crea profilo L (angolare)
bpy.ops.curve.primitive_bezier_curve_add()
profile = bpy.context.active_object
# ... modifica i punti del profilo in Edit Mode ...
curve_to_profile(rail_curve, profile)
"""
mod, ng, nds, lks = create_gn_modifier(curve_obj, name)
for l in list(lks): lks.remove(l)
in_nd = next(n for n in nds if n.bl_idname == "NodeGroupInput")
out_nd = next(n for n in nds if n.bl_idname == "NodeGroupOutput")
# Object Info per il profilo
prof_info = add_node(nds, "GeometryNodeObjectInfo", location=(0, -200))
prof_info.inputs["Object"].default_value = profile_curve_obj
# Object to Curve
obj2curve = add_node(nds, "GeometryNodeObjectInfo", location=(0, -200))
c2m = add_node(nds, "GeometryNodeCurveToMesh", location=(300, 0))
c2m.inputs["Fill Caps"].default_value = True
lks.new(in_nd.outputs[0], c2m.inputs["Curve"])
lks.new(prof_info.outputs["Geometry"], c2m.inputs["Profile Curve"])
lks.new(c2m.outputs["Mesh"], out_nd.inputs[0])
return mod, ng
PATTERN 3 — Deformazione noise (Set Position)
Deforma una mesh in modo procedurale e non-distruttivo. Alternativa a blender-sculpt quando vuoi parametri animabili.
def noise_deform(obj, scale=5.0, strength=0.05, detail=6.0,
direction='normal', seed=0, name="NoiseDeform"):
"""
Deformazione noise non-distruttiva via Geometry Nodes.
scale : frequenza del noise (2=grosso, 8=medio, 20=fine)
strength : intensità dello spostamento [BU]
detail : ottave del noise (2=liscio, 8=rugoso)
direction : 'normal' (lungo normali) | 'z' | 'xyz' (tutte le direzioni)
A differenza di blender-sculpt, questo è completamente reversibile:
basta disabilitare/rimuovere il modifier.
Usi: terreno ondulato, superficie d'acqua, bandiera che sventola,
superfici organiche parametriche, deformazione per animazione.
"""
mod, ng, nds, lks = create_gn_modifier(obj, name)
for l in list(lks): lks.remove(l)
in_nd = next(n for n in nds if n.bl_idname == "NodeGroupInput")
out_nd = next(n for n in nds if n.bl_idname == "NodeGroupOutput")
# Group Inputs parametrici
add_group_input(ng, "Strength", "NodeSocketFloat",
default=strength, min_val=0, max_val=1.0)
add_group_input(ng, "Scale", "NodeSocketFloat",
default=scale, min_val=0.1, max_val=50.0)
# Position (coordinate vertici)
pos = add_node(nds, "GeometryNodeInputPosition", location=(-400, -200))
# Normal (per direction='normal')
if direction == 'normal':
normal = add_node(nds, "GeometryNodeInputNormal", location=(-400, -400))
# Noise Texture
noise = add_node(nds, "ShaderNodeTexNoise", location=(-200, 0))
noise.noise_dimensions = '3D'
noise.inputs["Scale"].default_value = scale
noise.inputs["Detail"].default_value = detail
noise.inputs["Roughness"].default_value = 0.5
# Math: remap da [0,1] a [-1,1]
remap = add_node(nds, "ShaderNodeMap
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [MAX-786](https://github.com/MAX-786)
- **Source:** [MAX-786/claude-3d-harness](https://github.com/MAX-786/claude-3d-harness)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.