Blender 3D Automation Enhanced

Data & APIs

Comprehensive Blender automation via Python bpy API. Procedural modeling, material nodes, lighting, rendering, and asset generation. Includes verified gear/mechanical parts demo.

Install

openclaw skills install blender-bpy-enhanced

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

# Install Blender
apt-get install blender   # Linux (Debian/Ubuntu)
# Or: brew install blender  # macOS

# Verify
blender --version

Core Patterns

1. Headless Execution

blender --background --python script.py

2. Scene Setup

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

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)

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

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

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

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.

blender --background --python scripts/demo_gear.py

Output: /tmp/blender_demo_gear.png and /tmp/blender_demo_gear.blend

Render Engines

EngineBest ForNotes
BLENDER_EEVEEFast preview, real-timeNo denoiser needed, good for demos
CYCLESPhotorealisticNeeds 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