Install
$ agentstack add skill-max-786-claude-3d-harness-blender-arch ✓ 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 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:
# 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:
# 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
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 > ``
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
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)
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)
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
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)
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)
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.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.