Install
openclaw skills install @emergencescience/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 @emergencescience/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 dynamically using quaternions (mathutils)
direction = mathutils.Vector(target) - cam.location
rot_quat = direction.to_track_quat('-Z', 'Y')
cam.rotation_euler = rot_quat.to_euler()
bpy.context.scene.camera = cam
return cam
def render(output_path="/tmp/render.png", engine='CYCLES',
width=1080, height=1080, samples=64):
scene = bpy.context.scene
scene.render.engine = engine
scene.render.resolution_x = width
scene.render.resolution_y = height
scene.render.filepath = output_path
scene.render.image_settings.file_format = 'PNG'
if engine == 'CYCLES':
scene.cycles.samples = samples
scene.cycles.use_denoising = True
# Configure Metal GPU on macOS if available
try:
bpy.context.preferences.addons['cycles'].preferences.compute_device_type = 'METAL'
bpy.context.preferences.addons['cycles'].preferences.get_devices()
for d in bpy.context.preferences.addons['cycles'].preferences.devices:
if d.type == 'METAL':
d.use = True
except Exception:
pass
bpy.ops.render.render(write_still=True)
def create_glass_material(name="Glass", color=(0.9, 0.95, 1.0, 1.0), roughness=0.1, ior=1.309):
"""Create refractive glass or ice material. IOR: 1.309 (ice), 1.5 (glass)"""
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
output = nodes.new(type='ShaderNodeOutputMaterial')
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
bsdf.inputs['Base Color'].default_value = color
bsdf.inputs['Roughness'].default_value = roughness
bsdf.inputs['IOR'].default_value = ior
# Enable transparency (Transmission)
if 'Transmission Weight' in bsdf.inputs:
bsdf.inputs['Transmission Weight'].default_value = 1.0
elif 'Transmission' in bsdf.inputs:
bsdf.inputs['Transmission'].default_value = 1.0
return mat
When carving grooves or features into a mesh:
EXACT solver delete the geometry (vertex count becomes 0). Carve individual loops one-by-one instead.EXACT solver to fail and skip subtraction.curve_data.use_fill_caps = True before conversion. Combine with a Subdivision Surface modifier for clean rounded tips.For perfect geometry and flawless renderings on regular solids (like spheres), define seams directly in the shader nodes using object coordinates instead of modifying geometry:
SMOOTHERSTEP mask from $[w, w+s]$ to $[0, 1]$. Use this mask to mix Colors (Black vs. Orange), Roughness, and normal Height inputs into a single Bump node.When creating models intended for physical 3D printing (e.g. FDM/SLA):
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.01 # 1 unit = 1 cm (Pop Mart standard)
Solidify modifier to give surfaces a physical thickness (typically 1.2mm to 2.0mm):
solid = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
solid.thickness = 0.12 # 1.2 mm (if 1 unit = 1 cm)
solid.offset = -1.0 # Solidify inward
Decimate modifier to reduce poly count prior to export:
decimate = obj.modifiers.new(name="Decimate", type='DECIMATE')
decimate.ratio = 0.15 # Keep 15% of faces
# Blender 4.0+ uses standard wm operators for STL export
bpy.ops.wm.stl_export(filepath="model.stl", export_selected=True)
# Older Blender (<4.0):
# bpy.ops.export_mesh.stl(filepath="model.stl", use_selection=True)
To align procedural elements to concept art (front/side/top views), programmatically load images as background planes:
def load_reference_image(filepath, name="ReferenceImage", location=(0, 0, 0), rotation=(1.5708, 0, 0)):
# Create an empty object of type IMAGE
bpy.ops.object.empty_add(type='IMAGE', location=location, rotation=rotation)
empty = bpy.context.object
empty.name = name
# Load and assign the image data
try:
img = bpy.data.images.load(filepath)
empty.data = img
empty.empty_display_size = 5.0 # scale size
# Optional: set opacity
empty.use_empty_image_alpha = True
empty.empty_image_depth = 'BACK' # display behind mesh
except Exception as e:
print(f"Error loading reference image: {e}")
| Engine | Best For | Notes |
|---|---|---|
BLENDER_EEVEE | Fast preview, real-time | No denoiser needed, good for quick checks |
CYCLES | Photorealistic | Essential for glass reflection and refraction. Use samples=64 or 128 with Denoising. |
'FLOAT' (called 'FAST' in Blender <4.0), ensure cutter overlapping boundaries, or perform Boolean carving one-by-one.curve_data.use_fill_caps = True.Roughness=0.3, Metallic=0.85), a high-contrast rim/back light, and use the CYCLES render engine.use_nodes will be removed as nodes are always enabled. Check compatibility but safely use it for current versions.