Blender 3D — Python Manipulation Guide
SkillWeb & browsingHow to programmatically create, modify, and verify Blender 3D scenes using Python bpy (headless background mode, .blend files, render output). For setup-gen and reward-gen agents.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Blender 3D — Python Manipulation Guide skill
What this skill tells your AI
The instructions your AI receives, as published by xlang-ai/cua-gym in .claude/skills/blender/SKILL.md and read by ahel’s review.
This skill teaches setup-gen (create scenes, objects, materials, animations, render setups) and reward-gen (verify scene state, compare render output) how to work with Blender using Python.
- Libraries:
bpy(Blender-embedded),json,subprocess,cv2,numpy,Pillow,imagehash,scikit-image - Install:
sudo apt install blender(VM);pip3 install opencv-python numpy Pillow imagehash scikit-image(verification) - Blender version: 3.0.1 (VM), Python 3.9 embedded
- Headless:
blender --background --python script.py - File format:
.blend(binary, must use bpy to read/write)
0. GUI Startup on VM (for setup-gen)
After preparing the .blend scene file, setup-gen should launch Blender with the scene loaded for the GUI agent.
CRITICAL VM LIMIT: GUI launches must set DISPLAY=:0.
import os
import shlex
import subprocess
import time
def launch_gui(command: str, delay_sec: float = 1.0):
env = os.environ.copy()
env["DISPLAY"] = ":0"
subprocess.Popen(
shlex.split(command),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
time.sleep(delay_sec)
# Launch Blender with a pre-built scene
launch_gui('blender "/home/user/Desktop/task.blend"', delay_sec=3.0)
# Launch Blender with default empty scene
launch_gui('blender', delay_sec=3.0)
Guidelines:
- Blender opens
.blendfiles passed as arguments. - Use non-blocking launch (
Popen) so script exits cleanly. - Blender is heavy (3D viewport init) — use
delay_sec=3.0or higher. - Open initial scene, never golden scene.
1. Headless Script Execution (setup-gen & reward-gen)
All programmatic Blender operations run via --background mode. Scripts use the embedded bpy module.
Running Scripts
import subprocess
def run_blender_script(script_path: str, blend_file: str = None,
timeout: int = 60) -> subprocess.CompletedProcess:
"""Run a Python script inside Blender's background mode."""
cmd = ["blender", "--background"]
if blend_file:
cmd.append(blend_file)
cmd.extend(["--python", script_path])
env = dict(os.environ, DISPLAY=":0",
XAUTHORITY="/run/user/1000/gdm/Xauthority")
return subprocess.run(cmd, capture_output=True, text=True,
timeout=timeout, env=env)
# Run a setup script on a new scene
result = run_blender_script("/home/user/Desktop/setup.py")
# Run a verification script on an existing .blend
result = run_blender_script("/home/user/Desktop/verify.py",
blend_file="/home/user/Desktop/task.blend")
CRITICAL: Rendering requires DISPLAY=:0 and XAUTHORITY=/run/user/1000/gdm/Xauthority even in background mode on this VM.
Script Template (setup-gen)
#!/usr/bin/env python3
"""Blender setup script — run with: blender --background --python this_script.py"""
import bpy
import os
import json
# --- Clear default scene ---
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
# ... create scene contents ...
# --- Save ---
bpy.ops.wm.save_as_mainfile(filepath="/home/user/Desktop/task.blend")
Script Template (reward-gen / verification)
#!/usr/bin/env python3
"""Blender verify script — run with: blender --background task.blend --python this_script.py"""
import bpy
import json
import sys
results = {}
# ... inspect bpy.data.objects, materials, etc. ...
# Write results as JSON for the reward script to parse
with open("/tmp/blender_verify_result.json", "w") as f:
json.dump(results, f)
# Exit with code based on pass/fail
sys.exit(0 if all(results.values()) else 1)
2. Scene & Object Creation (setup-gen)
Clearing Defaults
import bpy
# Remove all default objects (Cube, Camera, Light)
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
# Or remove specific objects
for name in ["Cube", "Camera", "Light"]:
obj = bpy.data.objects.get(name)
if obj:
bpy.data.objects.remove(obj, do_unlink=True)
# Clean orphan data blocks
bpy.ops.outliner.orphans_purge(do_recursive=True)
Mesh Primitives
import bpy
from math import radians
# All primitive operators — each creates and selects the new object
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=(3, 0, 0))
bpy.ops.mesh.primitive_ico_sphere_add(radius=1, subdivisions=3, location=(6, 0, 0))
bpy.ops.mesh.primitive_cylinder_add(radius=1, depth=2, location=(0, 3, 0))
bpy.ops.mesh.primitive_cone_add(radius1=1, depth=2, location=(3, 3, 0))
bpy.ops.mesh.primitive_torus_add(major_radius=1, minor_radius=0.3, location=(6, 3, 0))
bpy.ops.mesh.primitive_plane_add(size=2, location=(0, 6, 0))
bpy.ops.mesh.primitive_monkey_add(size=1, location=(3, 6, 0)) # Suzanne
bpy.ops.mesh.primitive_circle_add(radius=1, vertices=32, location=(6, 6, 0))
bpy.ops.mesh.primitive_grid_add(x_subdivisions=10, y_subdivisions=10, size=2, location=(0, 9, 0))
# Access the just-created object
obj = bpy.context.active_object
Transforms
obj = bpy.data.objects["Cube"]
# Location (world coordinates)
obj.location = (1.0, 2.0, 3.0)
# Rotation (Euler angles in radians)
obj.rotation_euler = (radians(45), 0, radians(90))
# Scale
obj.scale = (2.0, 1.0, 0.5)
# Rename
obj.name = "MyCube"
# Apply transforms (bake into mesh data)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
Object Duplication
# Duplicate with linked data (instanced)
bpy.ops.object.duplicate(linked=True)
# Duplicate with independent data
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.duplicate(linked=False)
duplicate = bpy.context.active_object
duplicate.location.x += 3 # Offset
3. Materials & Textures (setup-gen)
Principled BSDF Material
import bpy
def create_material(name: str, color: tuple = (0.8, 0.8, 0.8, 1.0),
metallic: float = 0.0, roughness: float = 0.5,
emission_color: tuple = None, emission_strength: float = 0.0,
alpha: float = 1.0) -> bpy.types.Material:
"""Create a Principled BSDF material.
color: (R, G, B, A) in 0.0-1.0 range
"""
mat = bpy.data.materials.new(name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes["Principled BSDF"]
# Blender 3.0 Principled BSDF input names:
# Base Color, Subsurface, Subsurface Radius, Subsurface Color, Subsurface IOR,
# Subsurface Anisotropy, Metallic, Specular, Specular Tint, Roughness,
# Anisotropic, Anisotropic Rotation, Sheen, Sheen Tint, Clearcoat,
# Clearcoat Roughness, IOR, Transmission, Transmission Roughness,
# Emission, Emission Strength, Alpha, Normal, Clearcoat Normal, Tangent
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Metallic"].default_value = metallic
bsdf.inputs["Roughness"].default_value = roughness
bsdf.inputs["Alpha"].default_value = alpha
if emission_color:
bsdf.inputs["Emission"].default_value = emission_color
bsdf.inputs["Emission Strength"].default_value = emission_strength
return mat
def assign_material(obj, mat):
"""Assign material to object."""
if obj.data.materials:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
# Examples
red_mat = create_material("Red", color=(1, 0, 0, 1))
gold_mat = create_material("Gold", color=(1, 0.8, 0, 1), metallic=0.9, roughness=0.2)
glass_mat = create_material("Glass", color=(0.9, 0.9, 1, 1), roughness=0.0, alpha=0.3)
glass_mat.blend_method = 'HASHED' # Enable transparency in EEVEE
assign_material(bpy.data.objects["Cube"], red_mat)
Glass/Transparent Materials
mat = create_material("Glass", alpha=0.1)
mat.blend_method = 'HASHED' # EEVEE transparency: 'OPAQUE', 'CLIP', 'HASHED', 'BLEND'
mat.shadow_method = 'HASHED' # Shadow transparency
bsdf = mat.node_tree.nodes["Principled BSDF"]
bsdf.inputs["Transmission"].default_value = 1.0
bsdf.inputs["IOR"].default_value = 1.45
World Background
world = bpy.data.worlds.new("MyWorld")
bpy.context.scene.world = world
world.use_nodes = True
bg = world.node_tree.nodes["Background"]
bg.inputs["Color"].default_value = (0.05, 0.05, 0.2, 1) # Dark blue
bg.inputs["Strength"].default_value = 1.0
4. Modifiers (setup-gen)
obj = bpy.data.objects["Cube"]
# Subdivision Surface (Blender 3.0 type name: 'SUBSURF', NOT 'SUBDIVISION_SURFACE')
mod = obj.modifiers.new("Subdiv", "SUBSURF")
mod.levels = 2 # Viewport subdivisions
mod.render_levels = 3 # Render subdivisions
# Mirror
mod = obj.modifiers.new("Mirror", "MIRROR")
mod.use_axis[0] = True # Mirror on X
mod.use_axis[1] = False
mod.use_axis[2] = False
# Boolean
mod = obj.modifiers.new("Bool", "BOOLEAN")
mod.operation = 'DIFFERENCE' # 'DIFFERENCE', 'UNION', 'INTERSECT'
mod.object = bpy.data.objects["Sphere"]
mod.solver = 'EXACT'
# Array
mod = obj.modifiers.new("Array", "ARRAY")
mod.count = 5
mod.relative_offset_displace = (1.2, 0, 0)
# Solidify
mod = obj.modifiers.new("Solid", "SOLIDIFY")
mod.thickness = 0.1
# Bevel
mod = obj.modifiers.new("Bevel", "BEVEL")
mod.width = 0.05
mod.segments = 3
# Wireframe
mod = obj.modifiers.new("Wire", "WIREFRAME")
mod.thickness = 0.02
# Screw (lathe)
mod = obj.modifiers.new("Screw", "SCREW")
mod.angle = 6.28318 # 2*pi = full revolution
mod.steps = 64
# Decimate
mod = obj.modifiers.new("Decimate", "DECIMATE")
mod.ratio = 0.5
# Remesh
mod = obj.modifiers.new("Remesh", "REMESH")
mod.mode = 'SMOOTH' # 'BLOCKS', 'SMOOTH', 'SHARP', 'VOXEL'
mod.octree_depth = 6
# Apply modifier (destructive — bakes into mesh)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.modifier_apply(modifier="Subdiv")
Available Modifier Types (Blender 3.0)
Generate: ARRAY, BEVEL, BOOLEAN, BUILD, DECIMATE, EDGE_SPLIT, MASK, MIRROR, MULTIRES, REMESH, SCREW, SKIN, SOLIDIFY, SUBSURF, TRIANGULATE, WELD, WIREFRAME
Deform: ARMATURE, CAST, CURVE, DISPLACE, HOOK, LAPLACIANDEFORM, LATTICE, MESH_DEFORM, SHRINKWRAP, SIMPLE_DEFORM, SMOOTH, CORRECTIVE_SMOOTH, LAPLACIANSMOOTH, SURFACE_DEFORM, WARP, WAVE
Physics: CLOTH, COLLISION, DYNAMIC_PAINT, EXPLODE, FLUID, OCEAN, PARTICLE_INSTANCE, PARTICLE_SYSTEM, SOFT_BODY, SURFACE
Data: DATA_TRANSFER, MESH_CACHE, MESH_SEQUENCE_CACHE, NORMAL_EDIT, WEIGHTED_NORMAL, UV_PROJECT, UV_WARP, VERTEX_WEIGHT_EDIT, VERTEX_WEIGHT_MIX, VERTEX_WEIGHT_PROXIMITY
Other: NODES (Geometry Nodes), MESH_TO_VOLUME, VOLUME_TO_MESH, VOLUME_DISPLACE
5. Camera & Lighting (setup-gen)
Camera
import bpy
from mathutils import Euler
cam_data = bpy.data.cameras.new("Camera")
cam_data.type = 'PERSP' # 'PERSP', 'ORTHO', 'PANO'
cam_data.lens = 50 # Focal length (mm) for PERSP
cam_data.ortho_scale = 6.0 # Orthographic scale (for ORTHO)
cam_data.clip_start = 0.1
cam_data.clip_end = 1000
cam_data.sensor_width = 36 # Sensor size (mm)
cam_obj = bpy.data.objects.new("Camera", cam_data)
bpy.context.collection.objects.link(cam_obj)
cam_obj.location = (7, -6, 5)
cam_obj.rotation_euler = Euler((1.1, 0, 0.8))
# Set as active camera
bpy.context.scene.camera = cam_obj
# Depth of Field
cam_data.dof.use_dof = True
cam_data.dof.focus_distance = 5.0
cam_data.dof.aperture_fstop = 2.8
Lights
# Point light
light_data = bpy.data.lights.new("PointLight", "POINT")
light_data.energy = 100 # Watts
light_data.color = (1, 1, 1)
light_data.shadow_soft_size = 0.25
light_obj = bpy.data.objects.new("PointLight", light_data)
bpy.context.collection.objects.link(light_obj)
light_obj.location = (4, -4, 6)
# Sun light (directional, infinite distance)
sun_data = bpy.data.lights.new("Sun", "SUN")
sun_data.energy = 3
sun_data.angle = 0.00918 # Angular diameter
sun_obj = bpy.data.objects.new("Sun", sun_data)
bpy.context.collection.objects.link(sun_obj)
# Spot light
spot_data = bpy.data.lights.new("Spot", "SPOT")
spot_data.energy = 200
spot_data.spot_size = 0.785 # Cone angle in radians (45 degrees)
spot_data.spot_blend = 0.15 # Edge softness 0-1
spot_obj = bpy.data.objects.new("Spot", spot_data)
bpy.context.collection.objects.link(spot_obj)
# Area light
area_data = bpy.data.lights.new("Area", "AREA")
area_data.energy = 100
area_data.shape = 'RECTANGLE' # 'SQUARE', 'RECTANGLE', 'DISK', 'ELLIPSE'
area_data.size = 2
area_data.size_y = 1 # For RECTANGLE/ELLIPSE
area_obj = bpy.data.objects.new("Area", area_data)
bpy.context.collection.objects.link(area_obj)
6. Animation (setup-gen)
Keyframe Insertion
import bpy
obj = bpy.data.objects["Cube"]
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 120
scene.render.fps = 24
# Location keyframes
obj.location = (0, 0, 0)
obj.keyframe_insert(data_path="location", frame=1)
obj.location = (5, 0, 0)
obj.keyframe_insert(data_path="location", frame=60)
obj.location = (5, 5, 0)
obj.keyframe_insert(data_path="location", frame=120)
# Rotation keyframes
obj.rotation_euler = (0, 0, 0)
obj.keyframe_insert(data_path="rotation_euler", frame=1)
obj.rotation_euler = (0, 0, 6.28318) # Full rotation
obj.keyframe_insert(data_path="rotation_euler", frame=120)
# Scale keyframes
obj.scale = (1, 1, 1)
obj.keyframe_insert(data_path="scale", frame=1)
obj.scale = (2, 2, 2)
obj.keyframe_insert(data_path="scale", frame=60)
# Material property keyframes
mat = obj.data.materials[0]
bsdf = mat.node_tree.nodes["Principled BSDF"]
bsdf.inputs["Alpha"].default_value = 1.0
bsdf.inputs["Alpha"].keyframe_insert("default_value", frame=1)
bsdf.inputs["Alpha"].default_value = 0.0
bsdf.inputs["Alpha"].keyframe_insert("default_value", frame=120)
FCurve Interpolation
# Set interpolation type for keyframes
action = obj.animation_data.action
for fcurve in action.fcurves:
for kp in fcurve.keyframe_points:
kp.interpolation = 'LINEAR' # 'CONSTANT', 'LINEAR', 'BEZIER', 'SINE', 'QUAD', etc.
kp.handle_left_type = 'AUTO_CLAMPED'
kp.handle_right_type = 'AUTO_CLAMPED'
7. Render Settings (setup-gen)
EEVEE (fast, real-time engine)
scene = bpy.context.scene
scene.render.engine = 'BLENDER_EEVEE' # Blender 3.0 name (NOT 'BLENDER_EEVEE_NEXT')
# Resolution
scene.render.resolution_x = 1920
scene.render.resolution_y = 1080
scene.render.resolution_percentage = 100
# Output
scene.render.image_settings.file_format = 'PNG' # 'PNG', 'JPEG', 'BMP', 'TIFF', 'OPEN_EXR'
scene.render.image_settings.color_mode = 'RGBA' # 'BW', 'RGB', 'RGBA'
scene.render.film_transparent = True # Transparent background
# EEVEE-specific (Blender 3.0)
scene.eevee.taa_render_samples = 64
scene.eevee.use_bloom = True # Bloom (removed in Blender 4.0)
scene.eevee.bloom_threshold = 0.8
scene.eevee.use_ssr = True # Screen Space Reflections
scene.eevee.use_ssr_refraction = True
scene.eevee.shadow_cube_size = '1024'
scene.eevee.shadow_cascade_size = '2048'
Cycles (path tracing, photorealistic)
scene.render.engine = 'CYCLES'
scene.cycles.samples = 128
scene.cycles.use_denoising = True
scene.cycles.device = 'CPU' # or 'GPU' if available
Rendering to File
# Single frame render
scene.render.filepath = "/home/user/Desktop/render.png"
bpy.ops.render.render(write_still=True)
# Animation render (all frames)
scene.render.filepath = "/home/user/Desktop/frames/" # Trailing slash for sequence
scene.render.image_settings.file_format = 'PNG'
bpy.ops.render.render(animation=True)
# Render specific frame
scene.frame_set(42)
scene.render.filepath = "/home/user/Desktop/frame_42.png"
bpy.ops.render.render(write_still=True)
8. Text & Curves (setup-gen)
3D Text Objects
# Create text
font_curve = bpy.data.curves.new(type="FONT", name="TextData")
font_curve.body = "Hello World"
font_curve.size = 1.5
font_curve.extrude = 0.05 # 3D depth
font_curve.bevel_depth = 0.02 # Edge bevel
font_curve.bevel_resolution = 4
# Alignment
font_curve.align_x = 'CENTER' # 'LEFT', 'CENTER', 'RIGHT', 'JUSTIFY', 'FLUSH'
font_curve.align_y = 'CENTER' # 'TOP_BASELINE', 'TOP', 'CENTER', 'BOTTOM'
# Spacing
font_curve.space_character = 1.0
font_curve.space_word = 1.0
font_curve.space_line = 1.2
text_obj = bpy.data.objects.new("MyText", font_curve)
bpy.context.collection.objects.link(text_obj)
text_obj.location = (0, 0, 2)
# Load custom font (optional)
# font_curve.font = bpy.data.fonts.load("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
# Convert text to mesh (for modifiers etc.)
bpy.context.view_layer.objects.active = text_obj
text_obj.select_set(True)
bpy.ops.object.convert(target='MESH')
Bezier Curves
curve_data = bpy.data.curves.new("MyCurve", type="CURVE")
curve_data.dimensions = '3D'
curve_data.resolution_u = 12
curve_data.bevel_depth = 0.05 # Tube radius (0 = flat curve)
spline = curve_data.splines.new('BEZIER')
spline.bezier_points.add(2) # Total: 3 points (1 default + 2 added)
spline.bezier_points[0].co = (0, 0, 0)
spline.bezier_points[1].co = (2, 2, 0)
spline.bezier_points[2].co = (4, 0, 0)
for point in spline.bezier_points:
point.handle_left_type = 'AUTO'
point.handle_right_type = 'AUTO'
spline.use_cyclic_u = False # True = closed loop
curve_obj = bpy.data.objects.new("MyCurve", curve_data)
bpy.context.collection.objects.link(curve_obj)
9. Collections & Parenting (setup-gen)
Collections
# Create collection
coll = bpy.data.collections.new("Furniture")
bpy.context.scene.collection.children.link(coll)
# Move object to collection
obj = bpy.data.objects["Cube"]
coll.objects.link(obj)
# Optionally remove from default collection
bpy.context.scene.collection.objects.unlink(obj)
# Nested collections
sub_coll = bpy.data.collections.new("Chairs")
coll.children.link(sub_coll)
# Hide collection
layer_coll = bpy.context.view_layer.layer_collection.children["Furniture"]
layer_coll.exclude = True # Exclude from view layer
Parent-Child Relationships
child = bpy.data.objects["Sphere"]
parent = bpy.data.objects["Cube"]
child.parent = parent
# Parent with transform preservation
child.parent = parent
child.matrix_parent_inverse = parent.matrix_world.inverted()
Constraints
obj = bpy.data.objects["Cube"]
target = bpy.data.objects["Sphere"]
# Track To constraint
c = obj.constraints.new('TRACK_TO')
c.target = target
c.track_axis = 'TRACK_NEGATIVE_Z'
c.up_axis = 'UP_Y'
# Copy Location
c = obj.constraints.new('COPY_LOCATION')
c.target = target
c.use_x = True
c.use_y = True
c.use_z = False # Don't copy Z
# Limit Location
c = obj.constraints.new('LIMIT_LOCATION')
c.use_min_x = True
c.min_x = -5.0
c.use_max_x = True
c.max_x = 5.0
10. Import / Export (setup-gen)
# OBJ
bpy.ops.export_scene.obj(filepath="/home/user/Desktop/model.obj", use_selection=False)
bpy.ops.import_scene.obj(filepath="/home/user/Desktop/model.obj")
# FBX
bpy.ops.export_scene.fbx(filepath="/home/user/Desktop/model.fbx", use_selection=False)
bpy.ops.import_scene.fbx(filepath="/home/user/Desktop/model.fbx")
# glTF / GLB
bpy.ops.export_scene.gltf(filepath="/home/user/Desktop/model.glb",
export_format='GLB') # 'GLB' or 'GLTF_SEPARATE'
bpy.ops.import_scene.gltf(filepath="/home/user/Desktop/model.glb")
# STL
bpy.ops.export_mesh.stl(filepath="/home/user/Desktop/model.stl", use_selection=False)
bpy.ops.import_mesh.stl(filepath="/home/user/Desktop/model.stl")
# Save / Open .blend
bpy.ops.wm.save_as_mainfile(filepath="/home/user/Desktop/scene.blend")
bpy.ops.wm.open_mainfile(filepath="/home/user/Desktop/scene.blend")
11. Complete Setup Example (setup-gen)
#!/usr/bin/env python3
"""Create a complete scene: table with objects, camera, light, and material."""
import bpy
from mathutils import Euler
# Clear defaults
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
# --- Table (scaled cube) ---
bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 0.5))
table = bpy.context.active_object
table.name = "Table"
table.scale = (2, 1, 0.05)
# Table legs
for x, y in [(-0.9, -0.45), (0.9, -0.45), (-0.9, 0.45), (0.9, 0.45)]:
bpy.ops.mesh.primitive_cylinder_add(radius=0.05, depth=0.5, location=(x, y, 0.25))
leg = bpy.context.active_object
leg.name = "Leg"
leg.parent = table
# Wood material for table
wood = bpy.data.materials.new("Wood")
wood.use_nodes = True
bsdf = wood.node_tree.nodes["Principled BSDF"]
bsdf.inputs["Base Color"].default_value = (0.4, 0.25, 0.1, 1)
bsdf.inputs["Roughness"].default_value = 0.7
table.data.materials.append(wood)
# --- Red sphere on table ---
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.2, location=(0.5, 0, 0.73))
sphere = bpy.context.active_object
sphere.name = "RedBall"
red = bpy.data.materials.new("Red")
red.use_nodes = True
red.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (1, 0, 0, 1)
sphere.data.materials.append(red)
# --- Camera ---
cam_data = bpy.data.cameras.new("Camera")
cam_data.lens = 35
cam_obj = bpy.data.objects.new("Camera", cam_data)
bpy.context.collection.objects.link(cam_obj)
cam_obj.location = (3, -3, 2.5)
cam_obj.rotation_euler = Euler((1.1, 0, 0.8))
bpy.context.scene.camera = cam_obj
# --- Sun light ---
sun = bpy.data.lights.new("Sun", "SUN")
sun.energy = 3
sun_obj = bpy.data.objects.new("Sun", sun)
bpy.context.collection.objects.link(sun_obj)
sun_obj.rotation_euler = Euler((0.8, 0.2, -0.5))
# --- Render settings ---
scene = bpy.context.scene
scene.render.engine = 'BLENDER_EEVEE'
scene.render.resolution_x = 1920
scene.render.resolution_y = 1080
scene.eevee.taa_render_samples = 32
# --- Save ---
bpy.ops.wm.save_as_mainfile(filepath="/home/user/Desktop/task.blend")
Golden File Pattern
import shutil
# Method 1: Save expected .blend state
bpy.ops.wm.save_as_mainfile(filepath="/home/user/Desktop/golden.blend")
# Method 2: Render expected output for visual comparison
bpy.context.scene.render.filepath = "/home/user/Desktop/golden_render.png"
bpy.ops.render.render(write_still=True)
# Method 3: Export scene state as JSON (for non-Blender verification)
import json
golden_state = {
"objects": {obj.name: {"type": obj.type, "location": list(obj.location),
"scale": list(obj.scale)}
for obj in bpy.data.objects},
"materials": list(bpy.data.materials.keys()),
"render_engine": bpy.context.scene.render.engine,
}
with open("/home/user/Desktop/golden_state.json", "w") as f:
json.dump(golden_state, f, indent=2)
12. Reading & Verifying (reward-gen)
Scene State Extraction
The primary verification pattern: run a bpy script that dumps scene state to JSON, then parse JSON in the reward script.
#!/usr/bin/env python3
"""Run inside Blender: blender --background task.blend --python extract_state.py"""
import bpy
import json
def extract_scene_state() -> dict:
"""Extract full scene state as JSON-serializable dict."""
state = {
"objects": {},
"materials": {},
"collections": list(bpy.data.collections.keys()),
"render": {
"engine": bpy.context.scene.render.engine,
"resolution_x": bpy.context.scene.render.resolution_x,
"resolution_y": bpy.context.scene.render.resolution_y,
"fps": bpy.context.scene.render.fps,
"frame_start": bpy.context.scene.frame_start,
"frame_end": bpy.context.scene.frame_end,
},
}
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 197
- Forks
- 18
- Last commit
- Aug 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
blender-xlang-ai- Source
- github.com/xlang-ai/cua-gym