mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-19 15:18:03 +00:00
feat(skills): enhance blender-mcp with comprehensive references and recipes
- Rewrite SKILL.md with full setup guide, MCP config, typed commands, object types table - Add references/bpy-api.md: scene, transforms, bmesh, materials, modifiers, camera, rendering, animation - Add references/pitfalls.md: 19 real-session pitfalls (connection, Python exec, rendering, version quirks) - Add references/recipes.md: 5 copy-paste recipes (landscape, glass sphere, donut, turntable, render) - Preserve original blender-mcp name and credit to alireza78a - Tested with Blender 5.1 on macOS
This commit is contained in:
parent
bd7e480236
commit
5601f24449
3 changed files with 593 additions and 0 deletions
264
optional-skills/creative/blender-mcp/references/bpy-api.md
Normal file
264
optional-skills/creative/blender-mcp/references/bpy-api.md
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
# Essential bpy API Reference
|
||||
|
||||
## Scene & Objects
|
||||
|
||||
```python
|
||||
# List all objects
|
||||
[obj.name for obj in bpy.data.objects]
|
||||
|
||||
# Get active object
|
||||
obj = bpy.context.active_object
|
||||
|
||||
# Select object by name
|
||||
bpy.data.objects['Cube'].select_set(True)
|
||||
|
||||
# Delete object by name (no context needed)
|
||||
bpy.data.objects.remove(bpy.data.objects['Cube'], do_unlink=True)
|
||||
|
||||
# Delete all objects
|
||||
for obj in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# Duplicate object
|
||||
import bpy
|
||||
src = bpy.data.objects['Cube']
|
||||
new_obj = src.copy()
|
||||
new_obj.data = src.data.copy()
|
||||
new_obj.name = 'Cube_Copy'
|
||||
bpy.context.collection.objects.link(new_obj)
|
||||
|
||||
# Parent objects
|
||||
child = bpy.data.objects['Child']
|
||||
parent = bpy.data.objects['Parent']
|
||||
child.parent = parent
|
||||
```
|
||||
|
||||
## Transforms
|
||||
|
||||
```python
|
||||
import math, mathutils
|
||||
|
||||
obj = bpy.data.objects['Cube']
|
||||
|
||||
# Location (world coordinates)
|
||||
obj.location = (1.0, 2.0, 3.0)
|
||||
obj.location.x += 0.5
|
||||
|
||||
# Rotation (radians)
|
||||
obj.rotation_euler = (math.radians(45), 0, 0)
|
||||
|
||||
# Scale
|
||||
obj.scale = (2.0, 2.0, 2.0)
|
||||
|
||||
# Apply transforms (bake into mesh)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
```
|
||||
|
||||
## Mesh Creation (bmesh)
|
||||
|
||||
```python
|
||||
import bmesh
|
||||
|
||||
# Create mesh from scratch
|
||||
mesh = bpy.data.meshes.new('CustomMesh')
|
||||
obj = bpy.data.objects.new('CustomObject', mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
|
||||
bm = bmesh.new()
|
||||
# Add vertices
|
||||
v1 = bm.verts.new((0, 0, 0))
|
||||
v2 = bm.verts.new((1, 0, 0))
|
||||
v3 = bm.verts.new((0.5, 1, 0))
|
||||
bm.faces.new((v1, v2, v3))
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
```
|
||||
|
||||
## Materials (Principled BSDF)
|
||||
|
||||
```python
|
||||
# Create material
|
||||
mat = bpy.data.materials.new('MyMat')
|
||||
mat.use_nodes = True
|
||||
nodes = mat.node_tree.nodes
|
||||
links = mat.node_tree.links
|
||||
|
||||
principled = nodes['Principled BSDF']
|
||||
principled.inputs['Base Color'].default_value = (0.8, 0.1, 0.1, 1.0)
|
||||
principled.inputs['Metallic'].default_value = 0.9
|
||||
principled.inputs['Roughness'].default_value = 0.1
|
||||
|
||||
# Assign to object
|
||||
obj = bpy.data.objects['Cube']
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
# Emission material
|
||||
mat = bpy.data.materials.new('GlowMat')
|
||||
mat.use_nodes = True
|
||||
nodes = mat.node_tree.nodes
|
||||
links = mat.node_tree.links
|
||||
nodes.clear()
|
||||
output = nodes.new('ShaderNodeOutputMaterial')
|
||||
emission = nodes.new('ShaderNodeEmission')
|
||||
emission.inputs['Color'].default_value = (0.0, 1.0, 0.5, 1.0)
|
||||
emission.inputs['Strength'].default_value = 5.0
|
||||
links.new(emission.outputs[0], output.inputs[0])
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
# Glass material
|
||||
mat = bpy.data.materials.new('GlassMat')
|
||||
mat.use_nodes = True
|
||||
principled = mat.node_tree.nodes['Principled BSDF']
|
||||
principled.inputs['Transmission Weight'].default_value = 1.0 # Blender 4.x
|
||||
principled.inputs['IOR'].default_value = 1.45
|
||||
principled.inputs['Roughness'].default_value = 0.0
|
||||
```
|
||||
|
||||
## Modifiers
|
||||
|
||||
```python
|
||||
obj = bpy.data.objects['Cube']
|
||||
|
||||
# Subdivision Surface
|
||||
mod = obj.modifiers.new('Subdiv', 'SUBSURF')
|
||||
mod.levels = 2
|
||||
mod.render_levels = 3
|
||||
|
||||
# Solidify
|
||||
mod = obj.modifiers.new('Solidify', 'SOLIDIFY')
|
||||
mod.thickness = 0.05
|
||||
|
||||
# Boolean
|
||||
mod = obj.modifiers.new('Bool', 'BOOLEAN')
|
||||
mod.operation = 'DIFFERENCE'
|
||||
mod.object = bpy.data.objects['BoolCutter']
|
||||
|
||||
# Array
|
||||
mod = obj.modifiers.new('Array', 'ARRAY')
|
||||
mod.count = 5
|
||||
mod.relative_offset_displace = (1.2, 0, 0)
|
||||
|
||||
# Apply modifier
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.modifier_apply(modifier='Subdiv')
|
||||
```
|
||||
|
||||
## Camera & Lighting
|
||||
|
||||
```python
|
||||
# Create camera
|
||||
bpy.ops.object.camera_add(location=(7, -7, 5))
|
||||
cam = bpy.context.active_object
|
||||
cam.rotation_euler = (math.radians(63), 0, math.radians(45))
|
||||
bpy.context.scene.camera = cam
|
||||
|
||||
# Camera settings
|
||||
cam.data.lens = 50 # focal length mm
|
||||
cam.data.clip_start = 0.1
|
||||
cam.data.clip_end = 1000
|
||||
|
||||
# Point light
|
||||
bpy.ops.object.light_add(type='POINT', location=(3, 3, 5))
|
||||
light = bpy.context.active_object
|
||||
light.data.energy = 1000 # watts
|
||||
light.data.color = (1.0, 0.9, 0.8)
|
||||
|
||||
# Sun light
|
||||
bpy.ops.object.light_add(type='SUN', rotation=(math.radians(45), 0, 0))
|
||||
sun = bpy.context.active_object
|
||||
sun.data.energy = 3
|
||||
|
||||
# HDRI world lighting
|
||||
world = bpy.context.scene.world
|
||||
if not world:
|
||||
world = bpy.data.worlds.new('World')
|
||||
bpy.context.scene.world = world
|
||||
world.use_nodes = True
|
||||
nodes = world.node_tree.nodes
|
||||
links = world.node_tree.links
|
||||
nodes.clear()
|
||||
output = nodes.new('ShaderNodeOutputWorld')
|
||||
bg = nodes.new('ShaderNodeBackground')
|
||||
env = nodes.new('ShaderNodeTexEnvironment')
|
||||
env.image = bpy.data.images.load('/path/to/hdri.hdr')
|
||||
links.new(env.outputs[0], bg.inputs[0])
|
||||
links.new(bg.outputs[0], output.inputs[0])
|
||||
```
|
||||
|
||||
## Rendering
|
||||
|
||||
```python
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Resolution
|
||||
scene.render.resolution_x = 1920
|
||||
scene.render.resolution_y = 1080
|
||||
scene.render.resolution_percentage = 100
|
||||
|
||||
# Output
|
||||
scene.render.filepath = '/tmp/render.png'
|
||||
scene.render.image_settings.file_format = 'PNG' # PNG, JPEG, OPEN_EXR
|
||||
|
||||
# Engine
|
||||
scene.render.engine = 'CYCLES' # or 'BLENDER_EEVEE' (5.x) / 'BLENDER_EEVEE_NEXT' (4.x)
|
||||
|
||||
# Cycles settings
|
||||
scene.cycles.samples = 128
|
||||
scene.cycles.use_denoising = True
|
||||
|
||||
# Render
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
# Animation render
|
||||
scene.frame_start = 1
|
||||
scene.frame_end = 250
|
||||
scene.render.filepath = '/tmp/anim_'
|
||||
scene.render.image_settings.file_format = 'PNG'
|
||||
bpy.ops.render.render(animation=True)
|
||||
```
|
||||
|
||||
## Animation (Keyframes)
|
||||
|
||||
```python
|
||||
obj = bpy.data.objects['Cube']
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Set keyframe at frame 1
|
||||
scene.frame_set(1)
|
||||
obj.location = (0, 0, 0)
|
||||
obj.keyframe_insert(data_path='location', frame=1)
|
||||
|
||||
# Set keyframe at frame 60
|
||||
scene.frame_set(60)
|
||||
obj.location = (5, 0, 3)
|
||||
obj.keyframe_insert(data_path='location', frame=60)
|
||||
|
||||
# Rotation keyframe
|
||||
obj.rotation_euler = (0, 0, math.radians(360))
|
||||
obj.keyframe_insert(data_path='rotation_euler', frame=60)
|
||||
|
||||
# Material keyframe
|
||||
mat = obj.data.materials[0]
|
||||
principled = mat.node_tree.nodes['Principled BSDF']
|
||||
principled.inputs['Base Color'].default_value = (1, 0, 0, 1)
|
||||
principled.inputs['Base Color'].keyframe_insert(data_path='default_value', frame=1)
|
||||
principled.inputs['Base Color'].default_value = (0, 0, 1, 1)
|
||||
principled.inputs['Base Color'].keyframe_insert(data_path='default_value', frame=60)
|
||||
```
|
||||
|
||||
## Collections
|
||||
|
||||
```python
|
||||
# Create collection
|
||||
col = bpy.data.collections.new('MyCollection')
|
||||
bpy.context.scene.collection.children.link(col)
|
||||
|
||||
# Move object to collection
|
||||
col.objects.link(obj)
|
||||
bpy.context.scene.collection.objects.unlink(obj)
|
||||
|
||||
# Hide collection
|
||||
col.hide_viewport = True
|
||||
col.hide_render = True
|
||||
```
|
||||
106
optional-skills/creative/blender-mcp/references/pitfalls.md
Normal file
106
optional-skills/creative/blender-mcp/references/pitfalls.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Blender MCP — Pitfalls & Lessons Learned
|
||||
|
||||
## Setup & Connection
|
||||
|
||||
### 1. Addon must be started BEFORE connecting
|
||||
|
||||
The Blender MCP addon creates the socket server only when you click "Start MCP Server" in the BlenderMCP sidebar tab. If the agent tries to connect before this, you get ConnectionRefusedError on port 9876.
|
||||
|
||||
**Verify with:** `lsof -i :9876 -P -n | grep LISTEN`
|
||||
|
||||
### 2. Port 9876 is the default — check for conflicts
|
||||
|
||||
Other services may use 9876. If connection fails but Blender is running with the addon started, check with lsof. Change the port in the BlenderMCP addon UI panel if needed, and update blender_exec() accordingly.
|
||||
|
||||
### 3. The MCP server (uvx blender-mcp) is OPTIONAL for Hermes
|
||||
|
||||
The uvx blender-mcp MCP server is a subprocess bridge designed for Claude Desktop. Hermes can talk directly to the addon's socket using blender_exec() — no MCP subprocess needed. The MCP config is optional and adds a layer of indirection.
|
||||
|
||||
### 4. Addon installation requires user interaction
|
||||
|
||||
Unlike TouchDesigner (where we can paste a script into Textport), Blender addon installation requires the GUI: Edit > Preferences > Add-ons > Install. The agent cannot automate this. Provide the file path and let the user install it.
|
||||
|
||||
## Python Execution
|
||||
|
||||
### 5. Only bpy and math are in the namespace
|
||||
|
||||
The execute_code command provides a namespace with only bpy and math. If you need os, json, bmesh, mathutils, etc., import them inside the code string:
|
||||
|
||||
```python
|
||||
blender_exec("import bmesh; bm = bmesh.new(); ...")
|
||||
```
|
||||
|
||||
### 6. execute_code result is always empty in Blender 5.x addon
|
||||
|
||||
The blender-mcp addon's execute_code returns `{"result": {"executed": true, "result": ""}}` for ALL code — both eval and exec. The eval result is not captured in Blender 5.x. To get values:
|
||||
- Use `get_scene_info` or `get_object_info` for queries
|
||||
- Write results to a temp file and read back: `blender_exec("import json; open('/tmp/result.json','w').write(json.dumps([o.name for o in bpy.data.objects]))")`
|
||||
|
||||
### 7. Errors in execute_code return {"error": "..."} — always check
|
||||
|
||||
Always check for the error key in the response. The addon catches exceptions and returns them as error strings rather than crashing.
|
||||
|
||||
### 8. bpy.ops require correct context
|
||||
|
||||
Many bpy.ops functions require the right context. When executing via the socket, context may differ from the interactive UI. Prefer direct data manipulation:
|
||||
|
||||
```python
|
||||
# Prefer data API over ops
|
||||
blender_exec("bpy.data.objects.remove(bpy.data.objects['Cube'], do_unlink=True)")
|
||||
```
|
||||
|
||||
## Objects & Scene
|
||||
|
||||
### 9. Default scene has Cube, Light, Camera
|
||||
|
||||
New Blender files start with a Cube at (0,0,0), a Light, and a Camera. Clear them before building.
|
||||
|
||||
### 10. Object names are unique — Blender auto-renames duplicates
|
||||
|
||||
Creating an object with name "Cube" when one already exists results in "Cube.001". Always check the returned name.
|
||||
|
||||
### 11. Rotation in create_object is in DEGREES, not radians
|
||||
|
||||
The addon's create_object command converts degrees to radians internally. But in execute_code, bpy uses radians. Be careful about the distinction.
|
||||
|
||||
## Materials
|
||||
|
||||
### 12. Principled BSDF is the default shader
|
||||
|
||||
All materials created by the addon use Principled BSDF. For other shader types, use execute_code to build the node tree manually.
|
||||
|
||||
### 13. Color is RGBA 0-1, not RGB 0-255
|
||||
|
||||
Material colors use floating-point RGBA in 0.0-1.0 range.
|
||||
|
||||
## Rendering
|
||||
|
||||
### 14. Render blocks the connection — set timeout high
|
||||
|
||||
Rendering is synchronous. For the agent, the command will take longer to respond. Set timeout to 120s+ for render operations.
|
||||
|
||||
### 15. Engine name varies by Blender version
|
||||
|
||||
In Blender 5.x, EEVEE is `'BLENDER_EEVEE'` (not `'BLENDER_EEVEE_NEXT'` which was Blender 4.x). Always discover available engines:
|
||||
```python
|
||||
blender_exec("import json; open('/tmp/engines.json','w').write(json.dumps(list(bpy.types.RenderSettings.bl_rna.properties['engine'].enum_items.keys())))")
|
||||
```
|
||||
Known engine names: `BLENDER_EEVEE`, `BLENDER_WORKBENCH`, `CYCLES`
|
||||
|
||||
### 16. GPU rendering requires explicit setup on macOS
|
||||
|
||||
Use METAL compute device type on Apple Silicon. Use CUDA or OPTIX on NVIDIA.
|
||||
|
||||
## Connection Reliability
|
||||
|
||||
### 17. Each command creates a new TCP connection
|
||||
|
||||
blender_exec() opens and closes a TCP connection per call. All state lives in Blender's scene data, not the socket.
|
||||
|
||||
### 18. Large responses may need multiple recv() calls
|
||||
|
||||
The blender_exec() helper loops on recv(65536) until valid JSON is parsed. The 30s timeout handles edge cases.
|
||||
|
||||
### 19. Blender crash loses the socket server
|
||||
|
||||
If Blender crashes, relaunch it, re-enable the addon, click "Start MCP Server" again. Save frequently.
|
||||
223
optional-skills/creative/blender-mcp/references/recipes.md
Normal file
223
optional-skills/creative/blender-mcp/references/recipes.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# Blender Recipes
|
||||
|
||||
Common workflows assembled from the bpy API building blocks.
|
||||
|
||||
## Recipe 1: Low-Poly Landscape
|
||||
|
||||
```python
|
||||
import bpy, bmesh, math, random
|
||||
|
||||
# Clear scene
|
||||
for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# Create subdivided plane
|
||||
bpy.ops.mesh.primitive_plane_add(size=20)
|
||||
plane = bpy.context.active_object
|
||||
plane.name = 'Terrain'
|
||||
|
||||
# Subdivide
|
||||
mod = plane.modifiers.new('Subdiv', 'SUBSURF')
|
||||
mod.levels = 5
|
||||
mod.subdivision_type = 'SIMPLE'
|
||||
bpy.context.view_layer.objects.active = plane
|
||||
bpy.ops.object.modifier_apply(modifier='Subdiv')
|
||||
|
||||
# Displace vertices for terrain
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bm = bmesh.from_edit_mesh(plane.data)
|
||||
random.seed(42)
|
||||
for v in bm.verts:
|
||||
v.co.z = random.gauss(0, 0.5) * (1 - abs(v.co.x)/10) * (1 - abs(v.co.y)/10)
|
||||
bmesh.update_edit_mesh(plane.data)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Green material
|
||||
mat = bpy.data.materials.new('Grass')
|
||||
mat.use_nodes = True
|
||||
mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (0.15, 0.4, 0.1, 1)
|
||||
mat.node_tree.nodes['Principled BSDF'].inputs['Roughness'].default_value = 0.9
|
||||
plane.data.materials.append(mat)
|
||||
|
||||
# Sun light
|
||||
bpy.ops.object.light_add(type='SUN', rotation=(math.radians(45), 0, math.radians(30)))
|
||||
bpy.context.active_object.data.energy = 3
|
||||
|
||||
# Camera
|
||||
bpy.ops.object.camera_add(location=(12, -12, 8))
|
||||
cam = bpy.context.active_object
|
||||
cam.rotation_euler = (math.radians(55), 0, math.radians(45))
|
||||
bpy.context.scene.camera = cam
|
||||
```
|
||||
|
||||
## Recipe 2: Glass Sphere on Reflective Plane
|
||||
|
||||
```python
|
||||
import bpy, math
|
||||
|
||||
for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# Reflective floor
|
||||
bpy.ops.mesh.primitive_plane_add(size=20)
|
||||
floor = bpy.context.active_object
|
||||
mat_floor = bpy.data.materials.new('Floor')
|
||||
mat_floor.use_nodes = True
|
||||
p = mat_floor.node_tree.nodes['Principled BSDF']
|
||||
p.inputs['Base Color'].default_value = (0.02, 0.02, 0.02, 1)
|
||||
p.inputs['Metallic'].default_value = 1.0
|
||||
p.inputs['Roughness'].default_value = 0.05
|
||||
floor.data.materials.append(mat_floor)
|
||||
|
||||
# Glass sphere
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.5, location=(0, 0, 1.5), segments=64, ring_count=32)
|
||||
sphere = bpy.context.active_object
|
||||
mod = sphere.modifiers.new('Smooth', 'SUBSURF')
|
||||
mod.levels = 2
|
||||
mat_glass = bpy.data.materials.new('Glass')
|
||||
mat_glass.use_nodes = True
|
||||
p = mat_glass.node_tree.nodes['Principled BSDF']
|
||||
p.inputs['Transmission Weight'].default_value = 1.0
|
||||
p.inputs['IOR'].default_value = 1.45
|
||||
p.inputs['Roughness'].default_value = 0.0
|
||||
sphere.data.materials.append(mat_glass)
|
||||
|
||||
# Three-point lighting
|
||||
bpy.ops.object.light_add(type='AREA', location=(4, -3, 5))
|
||||
bpy.context.active_object.data.energy = 500
|
||||
bpy.context.active_object.data.size = 3
|
||||
|
||||
bpy.ops.object.light_add(type='AREA', location=(-4, -2, 3))
|
||||
bpy.context.active_object.data.energy = 200
|
||||
bpy.context.active_object.data.size = 2
|
||||
|
||||
bpy.ops.object.light_add(type='AREA', location=(0, 4, 2))
|
||||
bpy.context.active_object.data.energy = 100
|
||||
bpy.context.active_object.data.size = 4
|
||||
|
||||
# Camera
|
||||
bpy.ops.object.camera_add(location=(5, -5, 3))
|
||||
cam = bpy.context.active_object
|
||||
cam.rotation_euler = (math.radians(70), 0, math.radians(45))
|
||||
bpy.context.scene.camera = cam
|
||||
|
||||
# Cycles for glass
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
bpy.context.scene.cycles.samples = 256
|
||||
```
|
||||
|
||||
## Recipe 3: Procedural Donut (Simplified)
|
||||
|
||||
```python
|
||||
import bpy, math
|
||||
|
||||
for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# Torus (donut body)
|
||||
bpy.ops.mesh.primitive_torus_add(
|
||||
major_radius=1.0, minor_radius=0.4,
|
||||
major_segments=48, minor_segments=24
|
||||
)
|
||||
donut = bpy.context.active_object
|
||||
donut.name = 'Donut'
|
||||
|
||||
# Subdivision for smoothness
|
||||
mod = donut.modifiers.new('Subdiv', 'SUBSURF')
|
||||
mod.levels = 2
|
||||
|
||||
# Donut material (warm brown)
|
||||
mat = bpy.data.materials.new('DonutMat')
|
||||
mat.use_nodes = True
|
||||
p = mat.node_tree.nodes['Principled BSDF']
|
||||
p.inputs['Base Color'].default_value = (0.45, 0.22, 0.08, 1.0)
|
||||
p.inputs['Roughness'].default_value = 0.7
|
||||
p.inputs['Subsurface Weight'].default_value = 0.3
|
||||
donut.data.materials.append(mat)
|
||||
|
||||
# Icing (duplicate top half, scale up slightly)
|
||||
bpy.ops.mesh.primitive_torus_add(
|
||||
major_radius=1.02, minor_radius=0.42,
|
||||
major_segments=48, minor_segments=24
|
||||
)
|
||||
icing = bpy.context.active_object
|
||||
icing.name = 'Icing'
|
||||
|
||||
# Pink icing material
|
||||
mat_icing = bpy.data.materials.new('IcingMat')
|
||||
mat_icing.use_nodes = True
|
||||
p = mat_icing.node_tree.nodes['Principled BSDF']
|
||||
p.inputs['Base Color'].default_value = (0.9, 0.4, 0.5, 1.0)
|
||||
p.inputs['Roughness'].default_value = 0.3
|
||||
p.inputs['Coat Weight'].default_value = 0.5
|
||||
icing.data.materials.append(mat_icing)
|
||||
```
|
||||
|
||||
## Recipe 4: Turntable Animation
|
||||
|
||||
```python
|
||||
import bpy, math
|
||||
|
||||
# Assume scene already has objects
|
||||
|
||||
# Create empty as rotation center
|
||||
bpy.ops.object.empty_add(location=(0, 0, 0))
|
||||
pivot = bpy.context.active_object
|
||||
pivot.name = 'TurntablePivot'
|
||||
|
||||
# Parent camera to pivot
|
||||
cam = bpy.data.objects.get('Camera')
|
||||
if cam:
|
||||
cam.parent = pivot
|
||||
cam.location = (7, 0, 3)
|
||||
cam.rotation_euler = (math.radians(75), 0, math.radians(90))
|
||||
|
||||
# Animate rotation
|
||||
scene = bpy.context.scene
|
||||
scene.frame_start = 1
|
||||
scene.frame_end = 120 # 5 seconds at 24fps
|
||||
|
||||
pivot.rotation_euler = (0, 0, 0)
|
||||
pivot.keyframe_insert(data_path='rotation_euler', frame=1)
|
||||
pivot.rotation_euler = (0, 0, math.radians(360))
|
||||
pivot.keyframe_insert(data_path='rotation_euler', frame=121)
|
||||
|
||||
# Make rotation linear (not eased)
|
||||
for fc in pivot.animation_data.action.fcurves:
|
||||
for kp in fc.keyframe_points:
|
||||
kp.interpolation = 'LINEAR'
|
||||
|
||||
# Render settings
|
||||
scene.render.filepath = '/tmp/turntable_'
|
||||
scene.render.image_settings.file_format = 'PNG'
|
||||
scene.render.resolution_x = 1080
|
||||
scene.render.resolution_y = 1080
|
||||
```
|
||||
|
||||
## Recipe 5: Render to File and Verify
|
||||
|
||||
```python
|
||||
import bpy, os
|
||||
|
||||
scene = bpy.context.scene
|
||||
scene.render.resolution_x = 1920
|
||||
scene.render.resolution_y = 1080
|
||||
scene.render.filepath = '/tmp/blender_render.png'
|
||||
scene.render.image_settings.file_format = 'PNG'
|
||||
|
||||
# Use Cycles for quality
|
||||
scene.render.engine = 'CYCLES'
|
||||
scene.cycles.samples = 128
|
||||
scene.cycles.use_denoising = True
|
||||
|
||||
# Render
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
# Verify
|
||||
result = os.path.exists('/tmp/blender_render.png')
|
||||
```
|
||||
|
||||
Then from the agent, view the render:
|
||||
```python
|
||||
# After blender_exec returns, verify and view
|
||||
from hermes_tools import terminal
|
||||
terminal("ls -la /tmp/blender_render.png")
|
||||
# Use vision_analyze to inspect the render
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue