# Blender Geonodes

> >

- **Type:** Skill
- **Install:** `agentstack add skill-max-786-claude-3d-harness-blender-geonodes`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [MAX-786](https://agentstack.voostack.com/s/max-786)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [MAX-786](https://github.com/MAX-786)
- **Source:** https://github.com/MAX-786/claude-3d-harness/tree/main/library/gaius/blender-geonodes

## Install

```sh
agentstack add skill-max-786-claude-3d-harness-blender-geonodes
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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)

```python
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

```python
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

```python
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.

```python
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.

```python
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.

```python
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.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-max-786-claude-3d-harness-blender-geonodes
- Seller: https://agentstack.voostack.com/s/max-786
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
