# Blender Arch

> >

- **Type:** Skill
- **Install:** `agentstack add skill-max-786-claude-3d-harness-blender-arch`
- **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-arch

## Install

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

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

## About

# Skill: Blender 3D Modeling (Advanced)

Sei un esperto di modellazione 3D in Blender con Python (`bpy` + `bmesh`).
Ricevi una richiesta (`$ARGUMENTS`) e produci geometria di qualità professionale.

---

## Connessione — MCP

### ✅ Metodo 1 — MCP Tool (PREFERITO, porta 9876)
Usa direttamente il tool `mcp__Blender__execute_blender_code`:
```python
# Esegui codice in Blender — assegna sempre result={...} per ricevere dati
mcp__Blender__execute_blender_code(code="""
import bpy
# ... il tuo codice ...
result = {"ok": True, "verts": len(me.vertices)}
""")

# Screenshot viewport (senza render)
mcp__Blender__get_screenshot_of_window_as_image()

# Render su file e visualizza
mcp__Blender__render_viewport_to_path(output_path="/out.png")

# Lista oggetti in scena
mcp__Blender__get_objects_summary()
```

---

## Visual Loop — MCP (esegui → screenshot → analizza → itera)

```
FLUSSO PREFERITO con MCP:
1. mcp__Blender__execute_blender_code(code=build_code)
2. mcp__Blender__render_viewport_to_path(output_path="...preview.png")
   oppure mcp__Blender__get_screenshot_of_window_as_image()  ← più veloce, no render
3. Read("...preview.png")  → analisi visiva
4. mcp__Blender__execute_blender_code(code=fix_code)  → itera

RENDER COMPLETO (EEVEE) — da usare per risultato finale:
```
```python
# Via MCP — esegui questo codice poi leggi il file con Read
render_code = """
import bpy
sc = bpy.context.scene
try:    sc.render.engine = "BLENDER_EEVEE_NEXT"
except: sc.render.engine = "BLENDER_EEVEE"
sc.render.resolution_x = 1280
sc.render.resolution_y = 720
sc.render.filepath = "/render_final.png"
sc.render.use_compositing = False
sc.view_settings.view_transform = "Filmic"
sc.view_settings.look = "Medium High Contrast"
bpy.ops.render.render(write_still=True)
result = {"saved": sc.render.filepath}
"""
# mcp__Blender__execute_blender_code(code=render_code)
# poi: Read("/render_final.png")
```

---

## MODELLAZIONE — Funzioni Base

### Helper universali
```python
import bpy, math, bmesh
from mathutils import Vector, Matrix

def new_obj(name, mesh):
    """Crea e linka oggetto con mesh."""
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    return obj

def box(name, x, y, z, sx, sy, sz, mat=None):
    """Box con transform apply. Dimensioni reali (non half)."""
    bpy.ops.mesh.primitive_cube_add(size=1, location=(x, y, z))
    o = bpy.context.active_object
    o.name = name; o.scale = (sx, sy, sz)
    bpy.ops.object.transform_apply(scale=True)
    if mat: assign_mat(o, mat)
    return o

def cyl(name, x, y, z, r, h, verts=32, cap_fill='NGON', mat=None):
    """Cilindro: r=raggio, h=altezza, centrato in z."""
    bpy.ops.mesh.primitive_cylinder_add(
        radius=r, depth=h, vertices=verts,
        cap_fill_type=cap_fill, location=(x, y, z))
    o = bpy.context.active_object; o.name = name
    if mat: assign_mat(o, mat)
    return o

def sphere(name, x, y, z, r, subdiv=3, mat=None):
    """Icosfera: più uniforme della UV sphere."""
    bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=subdiv,
                                           location=(x, y, z))
    o = bpy.context.active_object; o.name = name
    smooth_shade(o)
    if mat: assign_mat(o, mat)
    return o

def plane(name, x, y, z, sx, sy, mat=None):
    bpy.ops.mesh.primitive_plane_add(size=1, location=(x, y, z))
    o = bpy.context.active_object; o.name = name
    o.scale = (sx, sy, 1)
    bpy.ops.object.transform_apply(scale=True)
    if mat: assign_mat(o, mat)
    return o

def assign_mat(obj, mat):
    if obj.data.materials: obj.data.materials[0] = mat
    else: obj.data.materials.append(mat)
```

---

## MODELLAZIONE — Tecniche Avanzate

### Smooth shading + Auto Smooth

> ⚠️ **BUG CRITICO — `bpy.ops.object.shade_smooth()` NON funziona su mesh bmesh**
>
> L'operatore `bpy.ops.object.shade_smooth()` **non rimuove l'attributo `sharp_face`**
> su mesh create con bmesh. Risultato: tutte le facce rimangono piatte (`normals_domain=FACE`)
> e si vedono striature verticali pronunciate su cilindri, coni e oggetti lathe.
>
> **Diagnosi:**
> ```python
> ob.data.normals_domain  # → 'FACE' (sbagliato), deve essere 'POINT'
> 'sharp_face' in ob.data.attributes  # → True dopo ops.shade_smooth() → piatto!
> ```
>
> **Fix: usa il metodo diretto sul mesh, non l'operatore:**
> ```python
> # SBAGLIATO (non funziona su bmesh):
> bpy.ops.object.shade_smooth()   # → sharp_face rimane True, striature!
>
> # CORRETTO — metodo diretto sul mesh (Blender 4.x+):
> ob.data.shade_smooth()          # → rimuove sharp_face, normals_domain='POINT' ✓
>
> # CORRETTO con soglia angolo (marca edge acuti come sharp):
> ob.data.shade_smooth()                          # prima: abilita smooth su tutto
> ob.data.set_sharp_from_angle(angle=math.radians(30))  # poi: marca edge > 30° come sharp
> ```

```python
def smooth_shade(obj, angle_deg=30):
    """
    Smooth shading con soglia angolo. ESSENZIALE per oggetti organici e curvi.
    Senza questo: facce piatte visibili su cilindri e sfere.

    NOTA: usa ob.data.shade_smooth() (metodo mesh), NON bpy.ops.object.shade_smooth()
    che non funziona correttamente su mesh create con bmesh.
    """
    # CORRETTO: metodo diretto sul mesh data-block
    obj.data.shade_smooth()
    # Marca edge acuti come sharp (angolo > soglia)
    try:
        obj.data.set_sharp_from_angle(angle=math.radians(angle_deg))
    except AttributeError:
        # Blender  **Teoria:** ogni oggetto Blender ha una **base ortonormale** propria incorporata nella
> `matrix_world` (4×4). Per far toccare due oggetti con precisione si calcolano i punti
> di contatto nel frame locale di ciascun oggetto e si trasformano nel frame world comune.
>
> ```
> p_world = obj.matrix_world @ p_local        # locale → world
> p_local = obj.matrix_world.inverted() @ p_world  # world → locale
> ```
> Precisione verificata: errore residuo  1e-10:
            delta_world += delta_world.normalized() * gap

    # 6. Converti in parent space se necessario, poi aggiorna location
    if obj_b.parent:
        delta = obj_b.parent.matrix_world.inverted().to_3x3() @ delta_world
    else:
        delta = delta_world
    obj_b.location = obj_b.location + delta
    bpy.context.view_layer.update()

    # 7. Calcola residuo
    p_a_f = obj_a.matrix_world @ Vector(pt_a_local)
    p_b_f = obj_b.matrix_world @ Vector(pt_b_local)
    return (p_b_f - p_a_f).length

def attach_bounds(obj_b, face_b, obj_a, face_a, gap=0.0):
    """
    Posiziona obj_b in modo che la faccia `face_b` del suo bounding box
    tocchi la faccia `face_a` del bounding box di obj_a nel world space.

    face: 'top' | 'bottom' | 'front' | 'back' | 'right' | 'left'

    Esempi:
        attach_bounds(saucer, 'top', cup, 'bottom')
        # → top del piattino tocca il bottom della tazza

        attach_bounds(lid, 'bottom', mug, 'top', gap=0.002)
        # → coperchio 2mm sopra il bordo
    """
    AXIS   = {'top': 2, 'bottom': 2, 'front': 1, 'back': 1, 'right': 0, 'left': 0}
    IS_MAX = {'top': True, 'right': True, 'back': True,
              'bottom': False, 'left': False, 'front': False}
    bpy.context.view_layer.update()

    ax    = AXIS[face_a]
    va    = [obj_a.matrix_world @ v.co for v in obj_a.data.vertices]
    vb    = [obj_b.matrix_world @ v.co for v in obj_b.data.vertices]
    ext_a = max(v[ax] for v in va) if IS_MAX[face_a] else min(v[ax] for v in va)
    ext_b = max(v[ax] for v in vb) if IS_MAX[face_b] else min(v[ax] for v in vb)
    delta_ax = ext_a - ext_b + gap * (1 if IS_MAX[face_a] else -1)

    if obj_b.parent:
        world_d = [0, 0, 0]; world_d[ax] = delta_ax
        obj_b.location += obj_b.parent.matrix_world.inverted().to_3x3() @ Vector(world_d)
    else:
        obj_b.location[ax] += delta_ax

    bpy.context.view_layer.update()
    vb2   = [obj_b.matrix_world @ v.co for v in obj_b.data.vertices]
    ext_b2 = max(v[ax] for v in vb2) if IS_MAX[face_b] else min(v[ax] for v in vb2)
    return abs(ext_b2 - ext_a)   # residuo ( 1:
        add_array(post, count=n_posts, relative=False,
                  offset_x=spacing, offset_y=0, offset_z=0)

    # Barra orizzontale
    bar = pipe_along_points(f"{name}_Bar",
        [(x_start, y, z_base + height),
         (x_end,   y, z_base + height)],
        radius=bar_r, mat=mat)

    # Barra inferiore
    bar_low = pipe_along_points(f"{name}_Bar_Low",
        [(x_start, y, z_base + 0.08),
         (x_end,   y, z_base + 0.08)],
        radius=bar_r, mat=mat)
    return post, bar, bar_low
```

### Scala a rampa
```python
def staircase(name, x, y, z_bottom, z_top, width, depth_total,
              mat_step=None, mat_riser=None):
    """
    Scala lineare con pedate e alzate.
    Scende da z_top a z_bottom su profondità depth_total.
    """
    n_steps = max(3, round((z_top - z_bottom) / 0.175))
    step_h  = (z_top - z_bottom) / n_steps
    step_d  = depth_total / n_steps
    parts   = []
    for i in range(n_steps):
        # Pedata
        pz = z_bottom + (i + 1) * step_h
        py = y + depth_total - (i + 0.5) * step_d
        tread = box(f"{name}_Tread_{i}", x, py, pz - step_h/2 + 0.02,
                    width, step_d, 0.04, mat_step)
        add_bevel(tread, 0.005, 2); parts.append(tread)
        # Alzata (opzionale, per scale chiuse)
        if mat_riser:
            riser = box(f"{name}_Riser_{i}", x, py + step_d/2 - 0.02,
                        pz - step_h/2, width, 0.04, step_h, mat_riser)
            parts.append(riser)
    return parts
```

### Tetto a padiglione (hip roof)
```python
def hip_roof(name, cx, cy, z_eave, W, D, rise, overhang=0.5, mat=None):
    hw = W/2 + overhang;  hd = D/2 + overhang
    rl = max((W - D)/2, 0.8);  zr = z_eave + rise
    v = [(cx-hw,cy-hd,z_eave),(cx+hw,cy-hd,z_eave),
         (cx+hw,cy+hd,z_eave),(cx-hw,cy+hd,z_eave),
         (cx-rl,cy,zr),(cx+rl,cy,zr)]
    f = [(0,1,5,4),(2,3,4,5),(4,3,0),(1,2,5)]
    obj = make_mesh_from_data(name, v, f)
    if mat: assign_mat(obj, mat)
    return obj
```

### Barra diagonale XZ (X-frame, croce di Sant'Andrea)
```python
def diag_bar_xz(name, x1, z1, x2, z2, y_wall,
                thickness=0.048, depth=0.07, mat=None):
    """
    Barra diagonale piatta su parete frontale (piano XZ).
    NON usare box ruotati — proiettano la lunghezza in Y.
    """
    dx=x2-x1; dz=z2-z1; ln=math.sqrt(dx*dx+dz*dz)
    if ln  **Blender 5.x API notes:**
> - `blend_method` è **DEPRECATO** → usa `surface_render_method = "BLENDED"` (trasparenza colorata) o `"DITHERED"` (compatibile con passes)
> - `use_nodes` setter è deprecated (5.0+), ma la proprietà esiste ancora
> - Principled BSDF ora usa modello **OpenPBR**: ha layer Coat (clearcoat), Sheen, Subsurface migliorato
> - Input sicuro: controlla `if 'Nome' in [i.name for i in bsdf.inputs]` per versione-safety
> - **`ShaderNodeMixRGB` è DEPRECATO** in Blender 5.x → usa `ShaderNodeMix` con `node.data_type = 'RGBA'`. Gli input cambiano: `inputs[0]` = Factor, `inputs[6]` = Color A, `inputs[7]` = Color B, `outputs[2]` = Color. Con MixRGB il nodo esiste ma restituisce valori di default (giallo) invece del mix corretto — bug silenzioso!
>
> ```python
> # SBAGLIATO (Blender 5.x):
> mix = nt.nodes.new('ShaderNodeMixRGB')
> mix.inputs[1].default_value = (1,0,0,1)  # → restituisce giallo default
>
> # CORRETTO (Blender 4.x+):
> mix = nt.nodes.new('ShaderNodeMix')
> mix.data_type = 'RGBA'
> mix.blend_type = 'MIX'
> mix.inputs[6].default_value = (1,0,0,1)   # Color A
> mix.inputs[7].default_value = (0,1,0,1)   # Color B
> nt.links.new(factor_socket, mix.inputs[0])
> nt.links.new(mix.outputs[2], bsdf.inputs['Base Color'])
> ```

### Helper: accesso input sicuro
```python
def bsdf_set(bsdf, input_name, value):
    """Setta input BSDF solo se esiste (version-safe)."""
    input_names = [i.name for i in bsdf.inputs]
    if input_name in input_names:
        bsdf.inputs[input_name].default_value = value
```

### PBR base
```python
def mat_pbr(name, color, roughness=0.5, metallic=0.0, alpha=1.0):
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; m.node_tree.nodes.clear()
    bsdf = m.node_tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = m.node_tree.nodes.new('ShaderNodeOutputMaterial')
    m.node_tree.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    bsdf.inputs['Base Color'].default_value = (*color, 1.0)
    bsdf.inputs['Roughness'].default_value  = roughness
    bsdf.inputs['Metallic'].default_value   = metallic
    if alpha  0:
        if 'Coat Weight' in inp:        # Blender 5.x
            bsdf.inputs['Coat Weight'].default_value    = clearcoat
            bsdf.inputs['Coat Roughness'].default_value = 0.05
            bsdf.inputs['Coat IOR'].default_value       = 1.50
        elif 'Clearcoat' in inp:        # Blender 4.x
            bsdf.inputs['Clearcoat'].default_value          = clearcoat
            bsdf.inputs['Clearcoat Roughness'].default_value = 0.05
    tree.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    return m

# Esempi:
# mat_metal("Acciaio",    (0.80,0.80,0.82), roughness=0.15)
# mat_metal("Cromo",      (0.95,0.95,0.96), roughness=0.04)
# mat_metal("CarPaint",   (0.05,0.08,0.65), roughness=0.20, clearcoat=1.0)
# mat_metal("Oro",        (1.00,0.78,0.34), roughness=0.08, anisotropic=0.5)
```

### Subsurface scattering (pelle, cera, cibo, marmo)
```python
def mat_subsurface(name, color, subsurface_color=None, roughness=0.6,
                   radius=(1.0, 0.2, 0.1), scale=0.01, method='RANDOM_WALK'):
    """
    Subsurface scattering per materiali traslucenti.
    Blender 5.x OpenPBR: 'Subsurface Weight' + 'subsurface_method'.

    method:
      'RANDOM_WALK'      → pelle, cera, marmo (più preciso)
      'RANDOM_WALK_SKIN' → pelle umana con epidermide
      'BURLEY'           → veloce, meno preciso

    radius: (R, G, B) scattering — sangue/pelle: (1.0, 0.2, 0.1)
    scale:  0.005=pelle sottile, 0.02=cera, 0.05=marmo
    """
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; tree = m.node_tree; tree.nodes.clear()
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    inp  = [i.name for i in bsdf.inputs]
    bsdf.inputs['Base Color'].default_value = (*color, 1.0)
    bsdf.inputs['Roughness'].default_value  = roughness
    # Subsurface Weight (Blender 5.x) o Subsurface (4.x)
    for sname in ['Subsurface Weight', 'Subsurface']:
        if sname in inp:
            bsdf.inputs[sname].default_value = 0.8
            break
    if 'Subsurface Radius' in inp:
        bsdf.inputs['Subsurface Radius'].default_value = radius
    if 'Subsurface Scale' in inp:
        bsdf.inputs['Subsurface Scale'].default_value  = scale
    if subsurface_color and 'Subsurface Color' in inp:
        bsdf.inputs['Subsurface Color'].default_value  = (*subsurface_color, 1.0)
    # Metodo subsurface
    try: bsdf.subsurface_method = method
    except: pass
    tree.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    return m

# Esempi:
# mat_subsurface("Skin",  (0.84,0.61,0.50), method='RANDOM_WALK_SKIN', scale=0.006)
# mat_subsurface("Wax",   (0.98,0.94,0.82), method='RANDOM_WALK', scale=0.025)
# mat_subsurface("Marble",(0.94,0.92,0.90), method='RANDOM_WALK', radius=(0.8,0.6,0.5), scale=0.04)
```

### Tessuto / velluto (Sheen layer)
```python
def mat_fabric(name, color, roughness=0.85, sheen=0.8, sheen_tint=(1,1,1)):
    """
    Materiale tessuto con Sheen layer (Blender 5.x OpenPBR).
    Sheen dà l'effetto vellutato caratteristico dei tessuti.

    Blender 5.x: 'Sheen Weight' + 'Sheen Roughness' + 'Sheen Tint'
    Blender 4.x: 'Sheen' + 'Sheen Tint'
    """
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True; tree = m.node_tree; tree.nodes.clear()
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    inp  = [i.name for i i

…

## 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-arch
- 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%.
