---
name: blender
description: |
  Drive Blender 5.1 on this machine — operate the running scene through the
  blender-mcp tools, and render multi-angle image sets headlessly. Use this
  skill for any Blender task: "出图 / 渲染 / 出多角度图 / 产品图 / asset sheet /
  出一组图", "把模型导入场景", "改材质 / 调灯光 / 看一眼场景", or turning a
  .blend or a 3D model file into images. It also covers building a model from
  a reference photo when no asset library has it. It carries the operating rules for the
  MCP bridge, including the one that wedges the bridge if ignored. Do NOT use
  it for AI 3D generation (that is the generate_3d tool) or for rendering
  work that does not involve Blender.
---

# Blender

## Operating rules

### Never render inside the GUI Blender process

This is the rule that breaks the setup if ignored, so it comes first.

The MCP for Blender add-on queues commands from its socket thread and drains
them through a single `bpy.app.timers` callback on Blender's main thread
(`BlenderMCPServer._drain_command_queue`). `bpy.ops.render.render()` is a
blocking modal operator; running it from that timer callback blocks the main
event loop for the whole render. The MCP client gives up first and returns a
`-32001` timeout, and the bridge then stops answering every later request —
while the Blender process stays alive and port 9876 stays open, so it looks
healthy while every call times out.

Recovery is `pkill -f "Blender.app/Contents/MacOS/Blender"` then
`open -a /Applications/Blender.app`; the add-on re-starts its server on
launch, so there is no manual "Start MCP Server" click to remember.

`look` is safe — it takes a viewport screenshot, not a full render, and the
bridge keeps answering afterwards. Use it for visual checks.

### How the bridge is wired

```
MCode  /Users/chen/.minimax/mcp.json
         blender: { type: stdio, command: uvx, args: [blender-mcp] }
  → blender-mcp server (stdio, 9 tools)
  → raw TCP 127.0.0.1:9876
  → add-on ~/Library/Application Support/Blender/5.1/scripts/addons/blender_mcp.py (v1.8, protocol 13)
  → command_queue → bpy.app.timers callback on the main thread
  → running Blender GUI
```

### Other gotchas worth knowing before you script

- **The add-on's integration switches are scene properties.** They reset to
  their default (off) whenever a new file is opened. A startup script at
  `~/Library/Application Support/Blender/5.1/scripts/startup/enable_asset_libraries.py`
  keeps Poly Haven, Poly Pizza and Sketchfab on. If `search_assets` reports a
  library as off, check that file exists and restart Blender.
- **The ground level comes from the largest flat object, never from an average
  over all of them.** A subject with small flat decals — an iris disc on a
  face, a blush mark — would otherwise drag the ground level up to eye height
  and the framing would crop the subject's legs off the shot. The render
  script takes the biggest excluded flat object as the ground.
- **Only transform the roots of an imported hierarchy, and transform their world
  matrix.** Two separate traps: moving parent and child separately scales and
  shifts the child twice, which leaves the subject floating above the ground;
  and touching only `scale` leaves objects whose *position* lives in the
  object transform (a small part placed with `location`) at the old position,
  so the part flies off next to the rest of the model. Apply
  `Matrix.Scale(s, 4) @ obj.matrix_world` and write it back.
- **Blender 5.1 localises node names.** `nodes.get("Principled BSDF")` returns
  None in a Chinese UI. Match on `n.type == 'BSDF_PRINCIPLED'` instead. Same
  for `BACKGROUND`, `TEX_ENVIRONMENT`, `OUTPUT_WORLD`.
- **Cycles is registered dynamically, so `RenderSettings.engine.enum_items`
  does not list it.** That list showing only `BLENDER_EEVEE` does not mean
  Cycles is missing — set `sc.render.engine = 'CYCLES'` and it works.
- **The add-on auto-starts its server** (`blendermcp_auto_start_server`
  defaults to True), so a plain `open -a /Applications/Blender.app` is enough.

### Which tool for which job

| Tool | Use it for |
|---|---|
| `get_scene_info` | what objects exist, and their transforms |
| `get_world_state_snapshot` | the broader scene state, not just objects |
| `look` | a rendered/solid viewport picture to check the result |
| `execute_blender_code` | building scenes, importing models, editing materials |
| `search_assets` / `import_asset` | Poly Haven, Poly Pizza and Sketchfab models and HDRIs |
| `generate_3d` | AI text/image-to-3D — needs a paid account, usually off |

## Asset libraries

`search_assets` and `import_asset` take a `source`: `polyhaven`, `polypizza`
or `sketchfab`. Poly Haven needs no key; the other two do.

**Where the keys live.** The add-on stores them in its own preferences
(`BLENDERMCP_AddonPreferences`), which Blender writes into `userpref.blend`, so
they survive file loads and restarts and never appear on a command line:

```python
prefs = bpy.context.preferences.addons["blender_mcp"].preferences
prefs.polypizza_api_key = "<key>"   # and .sketchfab_api_key
bpy.ops.wm.save_userpref()
```

The read order is **add-on preferences → scene property → environment variable**
(`BLENDERMCP_POLYPIZZA_API_KEY`, `BLENDERMCP_SKETCHFAB_API_KEY`), so the
preferences are the right place to put a key.

**Getting a key.**

- Poly Pizza: sign up at `poly.pizza`, confirm the emailed link, then
  `poly.pizza/settings/api` → *Create New App* → tick the API terms → the key
  is shown once. The free plan allows 3000 requests a month.
- Sketchfab: *My Settings → Password & API* at `sketchfab.com/settings/password`
  (a 32-character token). New Sketchfab accounts can only be created through an
  Epic Games account, and the email form on `sketchfab.com/signup` logs in
  rather than registering — a fresh address there just returns
  "The e-mail or the password is incorrect". Google sign-in only works for
  accounts that already exist.

**Poly Pizza in the headless pipeline.** `--polypizza-model <id>` fetches the
model inside Blender through the add-on, so the key stays in the preferences
and the script never sees it. That path needs Blender started with the user's
preferences, so the script drops `--factory-startup` for that run only.

```bash
node scripts/blender-render.js --build studio \
  --polypizza-model iMNqRzPwwe --polyhaven studio_small_09 \
  --out-dir out/ --angles three_quarter,front
```

## Modelling from a reference image

When there is no asset to import, build the object in Blender from a photo and
render it. This is a **clean parametric model**, not a scan: it gives tidy
topology, real materials and a small file, which is usually what a render
needs.

The reliable primitive for turned objects (lamps, vases, bottles, table legs)
is a **lathe**: describe the silhouette as a list of `(radius, height)` points
and spin it.

```python
def lathe(name, profile, segments=28, smooth=True):
    """profile: (radius, height) points. Ends on the axis -> a solid body;
    ends back at the start -> a shell with thickness."""
    bm = bmesh.new()
    vs = [bm.verts.new((r, 0.0, z)) for (r, z) in profile]
    edges = [bm.edges.new((a, b)) for a, b in zip(vs, vs[1:])]
    bmesh.ops.spin(bm, geom=vs + edges, cent=(0, 0, 0), axis=(0, 0, 1),
                   dvec=(0, 0, 0), angle=2 * math.pi, steps=segments,
                   use_merge=True)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new(name); bm.to_mesh(me); bm.free()
    if smooth:
        for p in me.polygons:
            p.use_smooth = True
    ob = bpy.data.objects.new(name, me)
    bpy.context.collection.objects.link(ob)
    return ob
```

Other useful primitives: `bpy.ops.mesh.primitive_cube_add` plus `bmesh.ops.extrude_face_region`
for boxes and frames, `bmesh.ops.inset_region` for a rim, `bmesh.ops.bevel` for
softened edges.

**Procedure.** Read the image, split the object into parts, write one profile
per part, build them in the live session through
`mcp__blender__execute_blender_code`, then export and render:

```python
for ob in bpy.data.objects:
    ob.select_set(True)
bpy.ops.export_scene.gltf(filepath="/tmp/rebuilt.glb", export_format='GLB',
                          use_selection=True)
```

```bash
node scripts/blender-render.js --build studio --model /tmp/rebuilt.glb \
  --polyhaven studio_small_09 --out-dir out/ --angles three_quarter,side
```

Feeding the export through `--model` puts the rebuild under exactly the same
studio, camera and lighting as an imported asset, so the two are directly
comparable. Compare the renders, then adjust the profiles and repeat — the
loop is cheap (a two-angle Cycles pass takes about 40 s).

**Limits.** This works for objects whose shape can be described in parts and
proportions: furniture, lamps, vessels, vehicles, toys. It does not reproduce
organic or irregular forms (faces, foliage, cloth, creatures) and it is not a
scan — if the asset must match the photo exactly, a generative service is the
right tool instead (`generate_3d`, which needs a paid account).

### Building a character from scratch

The same toolkit, layered onto a body. Each part is a separate object with its
own material, so the model stays editable:

- **Head** — a sphere deformed per vertex: cranium wide and round, chin
  tapered (`taper = 1 - k * t**p` on the lower half), front of the face
  flattened.
- **Hair** — a cap (a sphere scaled ~1.035x with the face region deleted by
  testing each face centre against an ellipse) plus a fringe built as *one
  continuous surface*: top edge on the hairline, bottom edge zigzagged so the
  low points become pointed tips, then a Solidify modifier for thickness.
  Separate strands leave visible gaps between them and read as a picket fence.
- **Eyes** — a flattened sclera, an iris disc with a spherical-gradient
  ColorRamp material (Texture Coordinate → Gradient → ColorRamp → Base Color),
  a pupil disc, emissive glint spheres, and a *flattened* sweep for the lash
  line (a round tube reads as a spike, not a lash).
- **Body** — a lathed torso scaled into an ellipse, a skirt whose radius is
  modulated per segment for pleats, limbs as swept tubes.
- **Materials** — skin with subsurface, hair with low roughness, an emissive
  material for the eye highlights.

Four traps that each cost real iterations:

1. **Do not colour an iris with `TexCoord.Object` + a spherical gradient.** A
   disc's object coordinates only span its own radius, so the gradient is
   nearly constant across it and the iris renders as one flat colour. Compute
   the colour per vertex and pass it through a `Color Attribute` instead.
2. **Flat facial features must hug the face.** A mouth built as a squashed
   sphere protrudes and reads as a snout; build it as a patch whose vertices
   sit on the face surface (solve the head ellipsoid for the front `y` at each
   `x, z`).
3. **Hair clumps need to be wide and flat.** A narrow round tube reads as a
   dagger. Flatten the sweep cross-section to about 0.4 and overlap the clumps
   so no gaps show.
4. **Check colours in a Cycles render, not the viewport.** The material
   preview uses a different environment and makes hair and skin read lighter
   and more saturated than the final image does.

**Loop.** Build in the live session, check with `look` (a viewport screenshot,
safe and fast), adjust the parameters, export to GLB, then render through the
same pipeline. Put the whole character in one script file and re-run it after
each edit; a 500-line build script that takes a minute to re-run is far
cheaper than re-issuing geometry by hand.

## Render pipeline (headless)

Rendering goes through a separate headless Blender process, not the bridge.
It is faster, parallelisable, and leaves the GUI scene untouched.

### Setup

The headless path needs only the Blender binary. The MCP path additionally
needs the add-on running, which it does by default.

### Inputs to collect

- **What to render**: an existing `.blend` path, a model file to import, or a
  scene that must be built
- **Output directory** for the PNGs
- **Angle set**: presets (`front`, `three_quarter`, `side`, `back`, `top`,
  `top_three_quarter`, `plan`, `low`, `detail`) or `orbit:N` for N evenly
  spaced views. `top` is a high angle (elevation 75°) that keeps the subject
  readable; `plan` is a straight-down view.
- **Engine and quality**: `CYCLES` for final output, `EEVEE` for fast previews
- **Environment**: a Poly Haven HDRI id, a local `.hdr`, or an existing world

If the user only says "render this", render the current scene from the live
session at `three_quarter` and mention the other presets rather than asking.

### Procedure

1. **Decide where the subject comes from.**
   - An existing `.blend` on disk → go to step 3.
   - A model file (`.glb` `.gltf` `.fbx` `.obj` `.stl` `.usd`) → pass it as
     `--model`. The script imports it, scales it to a sensible height, sits it
     on the ground, and frames it.
   - A Poly Haven CC0 model → pass `--polyhaven-model <id>`; the script
     downloads the bundle (main file + `.bin` + textures) and imports it.
   - A Poly Pizza model → pass `--polypizza-model <id>`; the add-on downloads it
     with the stored API key. Needs the key to be configured first.
   - It has to be modelled → build it in the live Blender through
     `mcp__blender__execute_blender_code`, then save the `.blend` with
     `bpy.ops.wm.save_as_mainfile(...)`. Build it live because the scene is
     interactive state the bridge owns; a headless process starts from a
     factory-empty file and would lose it.

2. **Get the environment.**
   - The scene needs a Poly Haven HDRI and you are working in the live session:
     use the MCP `import_asset` tool with `source: "polyhaven"`. It sets the
     world *and packs the HDR into the .blend*, so the headless render needs no
     extra files.
   - A fully self-contained run instead: pass `--polyhaven <id>` to the render
     script; it resolves and downloads the `.hdr` itself.
   - The `.blend` already has a world → nothing to do.

3. **Render headless.** From the skill directory:

   ```bash
   node scripts/blender-render.js \
     --blend /path/to/scene.blend \
     --out-dir /path/to/output \
     --angles three_quarter,front,side \
     --engine CYCLES --samples 64 --res 1200x900
   ```

   To build the reference studio scene and light it in one self-contained run:

   ```bash
   node scripts/blender-render.js \
     --build studio --polyhaven studio_small_09 \
     --out-dir /path/to/output \
     --angles three_quarter,front,side,top
   ```

   To put a model on that pedestal:

   ```bash
   node scripts/blender-render.js \
     --build studio --polyhaven-model ArmChair_01 --polyhaven studio_small_09 \
     --out-dir /path/to/output --angles three_quarter,front
   ```

   Run `node scripts/blender-render.js --help` for every option.

4. **Read the JSON report** the script prints on stdout. It lists each angle,
   its path, render time and size, plus an `ok` flag. Report the file paths and
   total time to the user. Check the `ok` flag before declaring success.

### Output contract

- One PNG per angle in `--out-dir`, named after the angle (`front.png`,
  `orbit_02.png`, …)
- `render_report.json` in the same directory with an `ok` flag, per-angle
  timings and sizes, the framing diagnostics (`framed_objects`,
  `excluded_flat`, `subject_center`, `subject_radius`, `ground_z`) and the
  Blender log lines
- `ndc_bounds` per angle gives the subject's projected extent in normalised
  device coordinates — `x`/`y` in [0,1] over the frame. Values near
  `[0.08, 0.92]` mean the subject fills about 84% of the frame; anything
  hugging 0 or 1 means it is about to be cropped
- The script prints the same report on stdout and exits non-zero if any angle
  failed. Add `--verbose` to also stream Blender's framing diagnostics to
  stderr as they happen.

### Failure handling

- **MCP calls start timing out** → the bridge is wedged, usually after a render
  was triggered through `execute_blender_code`. Restart Blender; the add-on
  re-starts its server on launch:
  `pkill -f "Blender.app/Contents/MacOS/Blender"; open -a /Applications/Blender.app`
- **`enum "EEVEE" not found`** → pass `CYCLES` or `EEVEE`; the script maps them
  to Blender's internal ids (`BLENDER_EEVEE`).
- **Subject cropped or lost in the frame** → the script fits the camera to the
  subject's bounding-box corners against the frustum, so the subject lands
  inside the frame with a margin. A large ground plane is excluded
  automatically; if the framing is still wrong, pass `--subject <name>` to
  frame only matching objects, or lower `--margin`.
- **The subject is itself a thin sheet** → the planar-exclusion rule drops it.
  Pass `--include-flat` to keep planar objects in the framing. When the subject
  is flat and nothing else survives, the script already falls back to treating
  the largest flat object as the ground and framing the rest.
- **Model import fails with "Missing resource"** → a `.gltf` references its
  `.bin` and textures by relative path. Keep them next to the `.gltf` in the
  same layout. Poly Haven bundles downloaded through `--polyhaven-model` are
  laid out correctly.
- **Imported model is the wrong size** → `--model-height <metres>` sets the
  height explicitly; the default is 0.8 m on the pedestal and 1.0 m on the
  floor.
- **Render too slow** → drop `--samples`, lower `--res`, or switch to
  `--engine EEVEE`. Measured on an Apple M5 Pro (Metal GPU): Cycles at
  1200×900 / 64 samples takes roughly 23–35 s per angle, EEVEE at the same
  resolution roughly 0.5–4 s.
- **Poly Haven download fails** → `--polyhaven` needs network access; fall back
  to a local `--env <file.hdr>`.
- **"Sketchfab API key is not configured"** → the MCP server refuses the call
  before the add-on is even asked. Put the token in the add-on preferences
  (see *Asset libraries*); a made-up token does not work, because the add-on
  always sends an `Authorization: Token …` header and the API rejects an
  invalid one with 401 instead of falling back to anonymous search.
- **"Poly Pizza API key is not configured"** → same, for `polypizza_api_key`.

## Examples

Input: "帮我把这个 Blender 场景的材质调一下"
Output: inspect with `get_scene_info`, edit through
`mcp__blender__execute_blender_code`, then check with `look` (not a render).

Input: "把我这个 Blender 场景出 4 个角度的图"
Output: save the live scene to a `.blend`, then

```bash
node scripts/blender-render.js --blend scene.blend --out-dir out/ \
  --angles front,three_quarter,side,top --engine CYCLES --samples 64
```

Input: "用 Poly Haven 的影棚 HDRI 给个产品图"
Output:

```bash
node scripts/blender-render.js --build studio --polyhaven studio_small_09 \
  --out-dir out/ --angles three_quarter --engine CYCLES
```

Input: "给我的 glb 模型出 4 个角度的产品图"
Output:

```bash
node scripts/blender-render.js --build studio --model /path/to/model.glb \
  --polyhaven studio_small_09 --out-dir out/ \
  --angles front,three_quarter,side,top --engine CYCLES
```

Input: "用 Poly Pizza 的椅子出一张产品图"
Output: find the model id with `search_assets(source="polypizza")`, then

```bash
node scripts/blender-render.js --build studio --polypizza-model <id> \
  --polyhaven studio_small_09 --out-dir out/ --angles three_quarter --engine CYCLES
```

## Windows (win32) platform notes

The render script is Node.js and runs unchanged, but the Blender binary and
paths differ:

```powershell
# Blender location
node scripts/blender-render.js --blender "C:\Program Files\Blender Foundation\Blender 5.1\blender.exe" ...

# or via environment variable
$env:BLENDER_BIN = "C:\Program Files\Blender Foundation\Blender 5.1\blender.exe"
```

Paths passed to `--blend`, `--out-dir` and `--env` must be Windows paths. The
Poly Haven download and the angle/auto-framing logic are platform-independent.

Everything else that is spelled with a macOS path in this document moves:

| What | macOS | Windows |
|---|---|---|
| Add-on file | `~/Library/Application Support/Blender/5.1/scripts/addons/blender_mcp.py` | `%APPDATA%\Blender Foundation\Blender\5.1\scripts\addons\blender_mcp.py` |
| Startup script | `~/Library/Application Support/Blender/5.1/scripts/startup/` | `%APPDATA%\Blender Foundation\Blender\5.1\scripts\startup\` |
| Skill | `~/.minimax/skills/blender/` | `%USERPROFILE%\.minimax\skills\blender\` (also copy to `%USERPROFILE%\.codex\skills\blender\`) |
| MCP config | `~/.minimax/mcp.json` | `%USERPROFILE%\.minimax\mcp.json` (key: `mcpServers`) |
| Recover a wedged bridge | `pkill -f "Blender.app/Contents/MacOS/Blender"` | `taskkill /F /IM blender.exe` |

Two extra Windows-only traps:

- **The MCP stdio child processes pile up.** Every call can spawn another
  `uvx blender-mcp` that the runtime never reaps; a long session leaves
  hundreds of them holding memory. `taskkill /F /IM blender.exe` does not
  touch them — clean them up explicitly when wrapping up.
- **`mavis` is not a standalone binary.** The MCP server list is managed
  through the app's own `mcp` tool (or by editing `mcp.json` directly), not
  by a `mavis` command on `PATH`.
