Install
openclaw skills install blender-bpy-enhancedComprehensive Blender automation via Python bpy API. Procedural modeling, material nodes, lighting, rendering, and asset generation. Includes verified gear/mechanical parts demo.
openclaw skills install blender-bpy-enhancedInvoke when the user wants to:
# Install Blender
apt-get install blender # Linux (Debian/Ubuntu)
# Or: brew install blender # macOS
# Verify
blender --version
blender --background --python script.py
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)
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
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
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
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
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}")
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
| 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 |
scene.render.engine = 'BLENDER_EEVEE'gear.select_set(True) and bpy.context.view_layer.objects.active = gear firstbpy.ops.object.mode_set(mode='EDIT') before bmesh.from_edit_mesh()obj.data.materials.append(mat)x_subdivisions=N and y_subdivisions=N instead of subdivisions=N