← Back to Skills
Verified
Emergence Blender Bpy
Automate Blender 3D modeling, lighting, materials, and photo-realistic rendering using Python bpy scripting.
Version
1.0.0
Published
Jun 3, 2026
Last Updated
Jun 3, 2026
Installation
Copy to OpenClaw
Install agent skill from https://github.com/emergencescience/emergence-blender-bpy
Documentation
Technical Manifest
slug
emergence-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.2.0
homepage
https://emergence.science/skills/emergence-blender-bpy
repository
https://github.com/emergencescience/emergence-blender-bpy
tags
blender3dmodelingrenderingbpyproceduralautomation3d-printing
requires
{"bins":["blender"]}
Blender Python Automation (bpy) — v2.2.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 & Dynamic Look-At
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
7. Rendering
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)
8. Solid Glass / Ice Material
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
9. Advanced Boolean Modeling (One-by-One Carving & Overlaps)
When carving grooves or features into a mesh:
- Avoid Bulk Cuts: Subtracting a complex, self-intersecting mesh (e.g. intersecting tubes) all at once makes the
EXACTsolver delete the geometry (vertex count becomes 0). Carve individual loops one-by-one instead. - Overlapping Geometry: Always make cutter objects overlap the boundaries of the base object (e.g. extending slightly outside and deeper inside). Co-incident boundaries cause the
EXACTsolver to fail and skip subtraction. - Capping Curves: When converting curves to meshes for cutting, set
curve_data.use_fill_caps = Truebefore conversion. Combine with aSubdivision Surfacemodifier for clean rounded tips.
10. 100% Procedural Coordinate Shader Seams (No Mesh Cuts)
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:
- Basketball Curved Seams Formula: $$|x| = \sqrt{1 - z^2} \cos\left(\theta_d \sqrt{1 - z^2}\right)$$
- Map Range & Masking: Map the resulting distance to a
SMOOTHERSTEPmask 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.
11. 3D Printing & Watertight Export (Solidify/Decimate)
When creating models intended for physical 3D printing (e.g. FDM/SLA):
- Real-World Units: Explicitly set the scene scale. Usually, 1 Blender unit represents 1 cm or 1 mm:
scene = bpy.context.scene scene.unit_settings.system = 'METRIC' scene.unit_settings.scale_length = 0.01 # 1 unit = 1 cm (Pop Mart standard) - Hollow/Wall Thickness: 3D models cannot have zero-thickness surfaces. Use the
Solidifymodifier 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 - Poly Count Optimization: Large vertex counts slow down slicers. Use a
Decimatemodifier to reduce poly count prior to export:decimate = obj.modifiers.new(name="Decimate", type='DECIMATE') decimate.ratio = 0.15 # Keep 15% of faces - Watertight STL Export: Ensure geometry is manifold, then export using standard operators:
# 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)
12. Loading Background Reference Image Planes
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}")
Render Engines
| 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. |
Common Pitfalls
- Boolean Exact solver deletes mesh (Vertices = 0) → Check for self-intersections or co-incident surfaces in the cutter. Switch to
'FLOAT'(called'FAST'in Blender <4.0), ensure cutter overlapping boundaries, or perform Boolean carving one-by-one. - Hollow tubes when converting curves to mesh → Enable caps before converting:
curve_data.use_fill_caps = True. - Glass rendering looks flat or dark → Glass requires light reflections to look realistic. Add a dark reflective metallic ground plane (
Roughness=0.3, Metallic=0.85), a high-contrast rim/back light, and use theCYCLESrender engine. - Deprecation warning on Material.use_nodes → In Blender 6.0+,
use_nodeswill be removed as nodes are always enabled. Check compatibility but safely use it for current versions.
Proof of Verifiability
This skill has been analyzed and verified by the Emergence Science clearinghouse. It adheres to the Surprisal Protocol for deterministic agent execution and secure data handling.