From blender-remote
This skill should be used when the user asks to render, set up rendering, enable CUDA, configure Cycles, set render resolution or samples, render an animation, output a PNG or EXR, create an animated GIF, or combine rendered frames into a video or GIF in Blender.
How this skill is triggered — by the user, by Claude, or both
Slash command
/blender-remote:renderingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```python
import bpy
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.device = 'GPU'
cprefs = bpy.context.preferences.addons['cycles'].preferences
cprefs.compute_device_type = 'CUDA'
cprefs.get_devices()
for device in cprefs.devices:
device.use = (device.type == 'CUDA')
enabled = [d.name for d in cprefs.devices if d.use]
print("Rendering on:", enabled)
If nvidia-smi shows the GPU but Cycles doesn't detect it, re-run cprefs.get_devices() after setting compute_device_type.
import bpy
scene = bpy.context.scene
render = scene.render
# Resolution
render.resolution_x = 1920
render.resolution_y = 1080
render.resolution_percentage = 100
# Sampling (lower for drafts, higher for final)
scene.cycles.samples = 128 # final quality
scene.cycles.use_denoising = True # AI denoiser reduces noise at lower samples
# Output format
render.image_settings.file_format = 'PNG'
render.image_settings.color_mode = 'RGBA'
render.filepath = '/tmp/render_output.png'
import bpy
bpy.context.scene.frame_set(1)
bpy.ops.render.render(write_still=True)
print("Rendered:", bpy.context.scene.render.filepath)
import bpy
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 120 # 5 seconds at 24fps
scene.render.fps = 24
scene.render.image_settings.file_format = 'PNG'
scene.render.filepath = '/tmp/frames/frame_' # Blender appends #### suffix
bpy.ops.render.render(animation=True)
print("Animation rendered to:", scene.render.filepath)
Create a smooth 360° orbit around the scene. The camera is parented to an empty at the scene center; rotating the empty sweeps the camera around it.
import bpy, math
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 120
# Empty at the scene center acts as the orbit pivot
bpy.ops.object.empty_add(location=(0, 0, 1))
target = bpy.context.object
target.name = 'OrbitTarget'
# Position camera at orbit radius
cam = scene.camera
if cam is None:
bpy.ops.object.camera_add(location=(8, 0, 3))
cam = bpy.context.object
scene.camera = cam
else:
cam.location = (8, 0, 3)
# Parent camera to the empty so the camera follows its rotation
cam.parent = target
cam.matrix_parent_inverse = target.matrix_world.inverted()
# Aim camera at the empty at all times
constraint = cam.constraints.new('TRACK_TO')
constraint.target = target
constraint.track_axis = 'TRACK_NEGATIVE_Z'
constraint.up_axis = 'UP_Y'
# Keyframe the orbit via empty rotation
target.rotation_euler = (0, 0, 0)
target.keyframe_insert('rotation_euler', frame=1)
target.rotation_euler = (0, 0, math.radians(360))
target.keyframe_insert('rotation_euler', frame=scene.frame_end)
# Linear interpolation for a constant-speed orbit
for fcurve in target.animation_data.action.fcurves:
for kp in fcurve.keyframe_points:
kp.interpolation = 'LINEAR'
After rendering PNG frames, combine them with ImageMagick or Pillow:
convert -delay 4 -loop 0 /tmp/frames/frame_*.png /tmp/output.gif
# -delay 4 = ~25fps; adjust to match render fps
from PIL import Image
import glob, os
frame_paths = sorted(glob.glob('/tmp/frames/frame_*.png'))
frames = [Image.open(f).convert('RGBA') for f in frame_paths]
frames[0].save(
'/tmp/output.gif',
save_all=True,
append_images=frames[1:],
loop=0,
duration=42, # ms per frame (~24fps)
optimize=True
)
print(f"GIF saved: {len(frames)} frames")
Before executing any render script via the MCP, save it to scripts/ in the project directory (e.g. scripts/render_orbit_animation.py). Update CLAUDE.md under ## Custom Settings with the chosen resolution, sample count, and output path so they persist across sessions.
import bpy
bpy.ops.wm.save_as_mainfile(filepath='/tmp/scene.blend')
| Use case | Samples | Denoising | Resolution |
|---|---|---|---|
| Draft / preview | 32 | True | 50% |
| Review | 64 | True | 100% |
| Final | 256 | True | 100% |
| Print/portfolio | 512 | True | 200% |
# Draft preset
scene.cycles.samples = 32
scene.render.resolution_percentage = 50
# Final preset
scene.cycles.samples = 256
scene.render.resolution_percentage = 100
Always render at the lowest quality that answers the question being asked. Each pass exists to validate something specific — don't burn samples until the prior pass has been approved.
| Pass | Purpose | Samples | Resolution | Frames |
|---|---|---|---|---|
composition | Camera framing, layout, basic lighting | 16 | 25% | 1 |
lookdev | Materials, light intensity, color | 32 | 50% | 1 |
motion | Animation timing, frame coverage | 32 | 50% | every 4th frame |
review | Stakeholder approval | 64 | 100% | full |
final | Hand-off / portfolio | 256+ | 100%–200% | full |
Promote one tier at a time. If composition is rejected, fix it and re-render composition — never jump to lookdev to "see what it looks like."
import bpy
PRESETS = {
'composition': {'samples': 16, 'res_pct': 25, 'denoise': True},
'lookdev': {'samples': 32, 'res_pct': 50, 'denoise': True},
'motion': {'samples': 32, 'res_pct': 50, 'denoise': True},
'review': {'samples': 64, 'res_pct': 100, 'denoise': True},
'final': {'samples': 256, 'res_pct': 100, 'denoise': True},
'portfolio': {'samples': 512, 'res_pct': 200, 'denoise': True},
}
def apply_preset(name):
p = PRESETS[name]
scene = bpy.context.scene
scene.cycles.samples = p['samples']
scene.cycles.use_denoising = p['denoise']
scene.render.resolution_percentage = p['res_pct']
# Persistent data: reuse BVH, shaders, geometry between frames in an animation
scene.render.use_persistent_data = True
print(f"[render] preset={name} samples={p['samples']} res={p['res_pct']}%")
Re-running an animation render after a crash should not redo finished frames.
import bpy, os, re
def render_missing_frames(output_dir, prefix='frame_'):
scene = bpy.context.scene
pattern = re.compile(re.escape(prefix) + r'(\d+)\.')
done = {int(m.group(1)) for f in os.listdir(output_dir) if (m := pattern.match(f))}
for frame in range(scene.frame_start, scene.frame_end + 1):
if frame in done:
continue
scene.frame_set(frame)
scene.render.filepath = os.path.join(output_dir, f'{prefix}{frame:04d}.png')
bpy.ops.render.render(write_still=True)
Validate motion timing without rendering 120 frames.
import bpy, os
def render_motion_preview(output_dir, step=4):
scene = bpy.context.scene
for frame in range(scene.frame_start, scene.frame_end + 1, step):
scene.frame_set(frame)
scene.render.filepath = os.path.join(output_dir, f'preview_{frame:04d}.png')
bpy.ops.render.render(write_still=True)
import bpy
scene = bpy.context.scene
# Reuse geometry/shader caches between frames — large speedup for animations
scene.render.use_persistent_data = True
# Adaptive sampling: stop sampling pixels that have already converged
scene.cycles.use_adaptive_sampling = True
scene.cycles.adaptive_threshold = 0.01
# Cap per-tile time so a slow region doesn't dominate
scene.cycles.time_limit = 0 # 0 = no limit; set seconds per frame for hard cap
# Denoiser: OptiX is fastest on NVIDIA, OpenImageDenoise is the highest quality fallback
scene.cycles.denoiser = 'OPTIX'
composition preset → render frame 1 → confirm camera and layout.lookdev preset → render frame 1 → confirm materials and lighting.motion preset → run render_motion_preview(step=4) → confirm timing.review preset → run render_missing_frames(...) for the full range.final preset → re-run render_missing_frames(...) only after review sign-off.npx claudepluginhub boernmaster/blender_skill --plugin blender-remoteCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.