Comprehensive Blender automation via Python bpy API. Procedural modeling, material nodes, lighting, rendering, and asset generation. Includes verified gear/m...
run this clawhubskill inside any MCP-capable agent (Claude Code, Codex, or Cursor). paste the command below and Implexa's MCP server recognizes it, applying the SKILL.md inline via the apply_recommended_skilltool call — the skill executes in your session, no separate install step. you can also invoke it by name in natural language (e.g. "implexa, run blender-bpy-enhanced").
implexa run clawhub/blender-bpy-enhanced
---
name: blender-bpy
title: Blender 3D Automation — Python bpy Scripting
description: Comprehensive Blender automation via Python bpy API. Procedural modeling, material nodes, lighting, rendering, and asset generation. Includes verified gear/mechanical parts demo.
version: 2.0.0
author: Approxima (via skillhub.cn)
tags: [blender, 3d, modeling, rendering, bpy, procedural, automation]
requires:
bins: [blender]
---
# Blender Python Automation (bpy) — v2.0.0
## When to Use This Skill
Invoke when the user wants to:
- Create 3D objects procedurally (gears, mechanical parts, architectural elements)
- Set up materials with node-based textures (metal, brushed, procedural)
- Configure 3-point lighting and camera
- Render still images or animations in headless mode
- Batch process or automate Blender workflows
- Export to GLB/glTF for web or game engines
## Prerequisites
```bash
# Install Blender
apt-get install blender # Linux (Debian/Ubuntu)
# Or: brew install blender # macOS
# Verify
blender --version
```
## Core Patterns
### 1. Headless Execution
```bash
blender --background --python script.py
```
### 2. Scene Setup
```python
import bpy, math
# Clear scene
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for mat in list(bpy.data.materials):
bpy.data.materials.remove(mat)
```
### 3. Procedural Gear Creation
```python
def create_gear(name, radius=2.0, teeth=16, thickness=0.8):
"""Create a gear with teeth and center hole"""
# Base cylinder
bpy.ops.mesh.primitive_cylinder_add(
vertices=teeth * 4,
radius=radius,
depth=thickness,
location=(0, 0, 0)
)
gear = bpy.context.object
gear.name = name
# Edit mode: select vertices at tooth positions
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
for v in gear.data.vertices:
angle = math.atan2(v.co.y, v.co.x)
tooth_angle = 2 * math.pi / teeth
angle_diff = abs((angle % tooth_angle) - tooth_angle / 2)
if angle_diff < tooth_angle * 0.35:
v.select = True
# Extrude and scale for teeth
bpy.ops.mesh.extrude_region_move(
TRANSFORM_OT_translate={"value": (0, 0, 0)}
)
bpy.ops.transform.resize(
value=((radius + 0.4) / radius,) * 2 + (1,),
orient_type='GLOBAL'
)
bpy.ops.object.mode_set(mode='OBJECT')
# Center hole via Boolean
bpy.ops.mesh.primitive_cylinder_add(
vertices=32, radius=0.5,
depth=thickness * 1.5, location=(0, 0, 0)
)
cutter = bpy.context.object
bool_mod = gear.modifiers.new(name="Hole", type='BOOLEAN')
bool_mod.operation = 'DIFFERENCE'
bool_mod.object = cutter
bpy.context.view_layer.objects.active = gear
gear.select_set(True)
bpy.ops.object.modifier_apply(modifier="Hole")
bpy.data.objects.remove(cutter, do_unlink=True)
# Modifier stack
bevel = gear.modifiers.new(name="Bevel", type='BEVEL')
bevel.width = 0.05; bevel.segments = 2; bevel.limit_method = 'ANGLE'
subdiv = gear.modifiers.new(name="Subdivision", type='SUBSURF')
subdiv.levels = 1; subdiv.render_levels = 2
return gear
```
### 4. Procedural Metal Material (Node-based)
```python
def create_metal_material(name, base_color, metallic=0.85, roughness=0.25,
noise_scale=30.0, use_brushed=True):
"""Create a procedural metal material with optional brushed effect"""
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (400, 0)
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf.location = (0, 0)
bsdf.inputs['Base Color'].default_value = base_color
bsdf.inputs['Metallic'].default_value = metallic
bsdf.inputs['Roughness'].default_value = roughness
if use_brushed:
tex = nodes.new(type='ShaderNodeTexCoord')
tex.location = (-400, 100)
noise = nodes.new(type='ShaderNodeTexNoise')
noise.location = (-200, 0)
noise.inputs['Scale'].default_value = noise_scale
noise.inputs['Detail'].default_value = 2.0
ramp = nodes.new(type='ShaderNodeValToRGB')
ramp.location = (0, 100)
ramp.color_ramp.elements[0].color = (
base_color[0]*0.8, base_color[1]*0.8, base_color[2]*0.8, 1.0)
ramp.color_ramp.elements[1].color = (
base_color[0]*1.1, base_color[1]*1.1, base_color[2]*1.1, 1.0)
links.new(tex.outputs['Object'], noise.inputs['Vector'])
links.new(noise.outputs['Fac'], ramp.inputs['Fac'])
links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
return mat
```
### 5. 3-Point Lighting Setup
```python
def setup_lighting(base_intensity=600):
"""Standard 3-point lighting: key, fill, rim"""
# Key light (main)
key = bpy.ops.object.light_add(
type='AREA', location=(5, -4, 6),
rotation=(0.8, 0, 0.7))
key = bpy.context.object
key.data.energy = base_intensity
key.data.size = 4
# Fill light
fill = bpy.ops.object.light_add(
type='AREA', location=(-4, 3, 3),
rotation=(0.5, 0, -1.0))
fill = bpy.context.object
fill.data.energy = base_intensity * 0.5
fill.data.size = 3
# Rim/back light
rim = bpy.ops.object.light_add(
type='AREA', location=(0, 5, 5),
rotation=(0.5, 0, 1.57))
rim = bpy.context.object
rim.data.energy = base_intensity * 0.4
rim.data.size = 2
```
### 6. Camera Setup
```python
def setup_camera(location=(5.5, -4.5, 3.5), target=(0, 0, 0)):
bpy.ops.object.camera_add(location=location)
cam = bpy.context.object
# Point camera at target
direction = (target[0] - location[0],
target[1] - location[1],
target[2] - location[2])
cam.rotation_euler = (
math.asin(direction[2] / math.sqrt(sum(d**2 for d in direction))),
0,
math.atan2(direction[1], direction[0]) + math.pi/2
)
bpy.context.scene.camera = cam
return cam
```
### 7. Rendering
```python
def render(output_path="/tmp/render.png", engine='BLENDER_EEVEE',
width=1920, height=1080, percentage=50):
scene = bpy.context.scene
scene.render.engine = engine
scene.render.resolution_x = width
scene.render.resolution_y = height
scene.render.resolution_percentage = percentage
scene.render.filepath = output_path
scene.render.image_settings.file_format = 'PNG'
bpy.ops.render.render(write_still=True)
print(f"✅ Rendered: {scene.render.filepath}")
```
## Complete Demo: Procedural Gear
See `scripts/demo_gear.py` for the full working demo.
```bash
blender --background --python scripts/demo_gear.py
```
Output: `/tmp/blender_demo_gear.png` and `/tmp/blender_demo_gear.blend`
## Render Engines
| Engine | Best For | Notes |
|--------|----------|-------|
| `BLENDER_EEVEE` | Fast preview, real-time | No denoiser needed, good for demos |
| `CYCLES` | Photorealistic | Needs OpenImageDenoiser; use `samples=128` for quick tests |
## Common Pitfalls
- **Render fails with "Build without OpenImageDenoiser"** → Switch to Eevee: `scene.render.engine = 'BLENDER_EEVEE'`
- **Boolean modifier not applying** → Set `gear.select_set(True)` and `bpy.context.view_layer.objects.active = gear` first
- **Bmesh edit mode errors** → Always call `bpy.ops.object.mode_set(mode='EDIT')` before `bmesh.from_edit_mesh()`
- **Material not showing on export** → Must assign to object's face data: `obj.data.materials.append(mat)`
- **Grid primitive fails** → In Blender 4.0+, use `x_subdivisions=N` and `y_subdivisions=N` instead of `subdivisions=N`
- **Headless EGL warnings** → These are harmless; Eevee falls back to surfaceless rendering automatically
don't have the plugin yet? install it then click "run inline in claude" again.