mcpField
B
blender-mcp

blender-mcp

github.com/vladmdgolam创意与媒体92.4k 次安装2 周前更新
代码仓库

关于 blender-mcp

blender-mcp 是用于检查 Blender 场景并准备 GLTF 导出的工作流技能,指导用户进行 MCP 健康检查、场景与材质分析、无头命令行导出、优化和验证。

主要功能

  1. 01MCP 连接与工具健康检查
  2. 02场景层级和材质检查
  3. 03无头命令行 GLTF 导出指引
  4. 04纹理与网格优化流程
  5. 05资产及动画导出验证

适用场景

  1. 01诊断 Blender MCP 连接
  2. 02查找场景或材质导出问题
  3. 03准备适用于 Web 的 GLB 资产
  4. 04检查导出的纹理、网格和动画

MCP 相关

该技能指导使用 get_scene_info、execute_python 和 screenshot 等 Blender MCP 工具检查场景及连接状态。由于 MCP 服务导出 GLTF 会超时,导出应使用 Blender 无头命令行。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vladmdgolam/agent-skills --skill blender-mcp

安装后在 Claude Code 中输入/blender-mcp确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
asset-integrations.md
errors.md
texture-optimization.md

详细介绍

Blender MCP

工具选择

快速检查时,请使用结构化 MCP 工具(get_scene_info、screenshot)。

对于任何非简单任务,请使用 execute_python:遍历层级、提取材质、烘焙动画、批量操作。它可以完整访问 bpy API,避免工具架构限制。

导出 GLTF 时,请使用无头 CLI——MCP 服务会在导出操作期间超时。

健康检查(始终先执行)

  1. get_scene_info — 确认连接正常(默认端口为 9876)
  2. 使用 execute_python 执行 print("ok") — 确认 Python 可正常运行
  3. screenshot — 确认视口捕获正常

如果 MCP 无响应,请检查 Blender MCP 插件是否已启用,以及套接字服务是否正在运行。

完整导出工作流

这是端到端的线性流程。请按顺序完成以下步骤,不要跳过。

步骤 1:健康检查

在执行其他操作前,确认 MCP 正常运行:

# In MCP tool call:
get_scene_info
execute_python: print("ok")
screenshot

如果任何步骤失败,请先停止并修复 MCP 连接。请参阅已知错误与解决方法。

步骤 2:检查场景

运行完整的层级提取,了解当前场景:

import bpy, json

def extract_hierarchy(obj, depth=0):
    data = {
        "name": obj.name,
        "type": obj.type,
        "location": list(obj.location),
        "rotation": list(obj.rotation_euler),
        "scale": list(obj.scale),
        "visible": not obj.hide_viewport,
        "children": [],
    }
    if obj.type == 'MESH' and obj.data:
        data["vertices"] = len(obj.data.vertices)
        data["faces"] = len(obj.data.polygons)
        data["materials"] = [slot.material.name for slot in obj.material_slots if slot.material]
    if obj.type == 'LIGHT':
        data["light_type"] = obj.data.type
        data["energy"] = obj.data.energy
        data["color"] = list(obj.data.color)
    for mod in obj.modifiers:
        if mod.type == 'ARRAY':
            data.setdefault("modifiers", []).append({
                "type": "ARRAY",
                "count": mod.count,
                "offset_object": mod.offset_object.name if mod.offset_object else None,
            })
    for child in obj.children:
        data["children"].append(extract_hierarchy(child, depth + 1))
    return data

scene_data = {
    "name": bpy.context.scene.name,
    "fps": bpy.context.scene.render.fps,
    "frame_start": bpy.context.scene.frame_start,
    "frame_end": bpy.context.scene.frame_end,
    "objects": [],
}
for obj in bpy.context.scene.objects:
    if obj.parent is None:
        scene_data["objects"].append(extract_hierarchy(obj))

print(json.dumps(scene_data, indent=2))

请留意:

  • 阵列修改器(烘焙后会大幅增加文件体积,必须在运行时重新生成)
  • 顶点数量较多的对象(可能导致导出缓慢或 GLB 文件过大)
  • 可能需要或不需要导出的隐藏对象
  • 缺少材质的对象(material_slots 为空)
步骤 3:验证材质

运行材质提取,在开始导出前发现可能造成信息丢失的设置:

import bpy, json

def extract_materials():
    materials = []
    for mat in bpy.data.materials:
        if not mat.use_nodes:
            continue
        info = {"name": mat.name, "nodes": [], "warnings": []}
        has_principled = False
        for node in mat.node_tree.nodes:
            node_data = {"type": node.type, "name": node.name}
            if node.type == 'BSDF_PRINCIPLED':
                has_principled = True
                for inp in node.inputs:
                    if inp.is_linked:
                        node_data[inp.name] = "linked"
                    elif hasattr(inp, 'default_value'):
                        val = inp.default_value
                        try:
                            node_data[inp.name] = list(val)
                        except TypeError:
                            node_data[inp.name] = float(val)
            if node.type == 'TEX_IMAGE' and node.image:
                node_data["image"] = node.image.filepath
                node_data["size"] = [node.image.size[0], node.image.size[1]]
                if node.image.size[0] > 2048:
                    info["warnings"].append(f"Large texture: {node.image.filepath} ({node.image.size[0]}x{node.image.size[1]})")
            if node.type in ('TEX_NOISE', 'TEX_VORONOI', 'TEX_WAVE', 'TEX_MUSGRAVE'):
                info["warnings"].append(f"Procedural texture node '{node.name}' ({node.type}) will be LOST on GLTF export")
            if node.type == 'VALTORGB':  # Color Ramp
                info["warnings"].append(f"Color Ramp '{node.name}' remapping will be LOST on GLTF export")
        if not has_principled:
            info["warnings"].append("No Principled BSDF found — export result unpredictable")
        info["nodes"].append(node_data)
        materials.append(info)
    return materials

result = extract_materials()
for mat in result:
    if mat["warnings"]:
        print(f"WARN [{mat['name']}]: {'; '.join(mat['warnings'])}")
print(json.dumps(result, indent=2))

继续前请检查所有警告,并决定是现在烘焙程序纹理,还是在导出后于运行时修补材质。

步骤 4:通过无头 CLI 导出

MCP 服务无法处理 GLTF 导出(会超时)。请始终使用无头 CLI:

# Use 'blender' if it's on PATH, otherwise use the platform-specific path:
#   macOS:   /Applications/Blender.app/Contents/MacOS/Blender
#   Windows: "C:\Program Files\Blender Foundation\Blender 4.x\blender.exe"
#   Linux:   /usr/bin/blender
blender \
  --background "/path/to/scene.blend" \
  --python-expr "
import bpy, os
export_path = '/path/to/output.glb'
os.makedirs(os.path.dirname(os.path.abspath(export_path)), exist_ok=True)
bpy.ops.export_scene.gltf(
    filepath=export_path,
    export_format='GLB',
    export_apply=False,
    export_animations=True,
    export_nla_strips=True,
    export_cameras=True,
    export_lights=False,
    export_draco_mesh_compression_enable=False,
)
size_mb = os.path.getsize(export_path) / 1024 / 1024
print(f'Export complete: {export_path} ({size_mb:.1f} MB)')
"

关键标志:

  • export_apply=False — 不要烘焙修改器(阵列修改器会将 1 MB 文件增大到 56 MB)
  • export_draco_mesh_compression_enable=False — 稍后通过 gltf-transform 应用 Draco 压缩
  • 对可能包含空格的路径加引号
步骤 5:使用 gltf-transform 优化

成功导出后运行。请始终逐项执行,不要使用 optimize:

# 1. Inspect raw export first
npx @gltf-transform/cli inspect output.glb

# 2. Resize textures (max 1K for web/mobile)
npx @gltf-transform/cli resize output.glb resized.glb --width 1024 --height 1024

# 3. WebP compression (quality 90 preserves detail)
npx @gltf-transform/cli webp resized.glb webp.glb --quality 90

# 4. Draco mesh compression (LAST — irreversible)
npx @gltf-transform/cli draco webp.glb final.glb

# 5. Inspect final result
npx @gltf-transform/cli inspect final.glb

预期体积缩减:原始约 22 MB → WebP 约 3.7 MB → Draco 约 1 MB。详细指标请参阅 references/texture-optimization.md。

步骤 6:验证

发布前,请按照下方完整的导出后验证清单进行检查。

导出后验证清单

每次导出后,在交付 GLB 以供集成前,请验证以下内容:

  • 文件大小合理 — 典型 Web 场景中,原始 GLB 小于 30 MB,优化后的 GLB 小于 5 MB。超过这些阈值的文件都应标记。
  • 使用 gltf-transform CLI 检查 — 运行 npx @gltf-transform/cli inspect final.glb 并检查:网格数量、纹理数量、纹理大小、动画数量、访问器大小。确认没有意外重复。
  • 在 Babylon.js Sandbox 中进行视觉测试 — 将 GLB 拖放到 sandbox.babylonjs.com。验证:网格正确渲染、纹理正常显示、动画正常播放、没有黑色或粉色材质。
  • 没有 Three.js 控制台错误 — 在最简 Three.js GLTFLoader 测试页面中加载,并检查浏览器控制台。常见错误:THREE.GLTFLoader: Unknown extension、纹理文件缺失、Draco 版本不受支持。
  • 抽查材质 — 选择 3–5 个材质,目视确认粗糙度、金属度和基础颜色正确。与 Blender 视口渲染进行比较。标记看起来扁平或过于闪亮的材质。
  • 抽查动画 — 如果场景包含动画,请在 Babylon.js Sandbox 或 Three.js 中确认至少一个动画能正确播放。检查帧数是否符合预期。
  • 已验证名称映射 — 如果运行时代码引用了网格名称,请确认经过 GLTF 导出转换后名称仍匹配(空格 → 下划线,移除点号)。参见关键规则 5。
  • 没有缺失纹理 — 检查 Babylon.js Sandbox 的网络标签页。纹理文件没有 404 错误。所有纹理都应打包在 GLB 内。

示例

示例 1:导出带动画的角色绑定

**场景:**你有一个带骨架、3 个 NLA 动作(idle、walk、run)、PBR 纹理集以及通过父级关系附加武器的人形角色。你需要一个可用于 Three.js 场景的 Web 就绪 GLB。

步骤 1:健康检查和场景检查

# MCP tool calls
get_scene_info
execute_python: print("ok")

步骤 2:检查绑定

import bpy, json

# Check armature and NLA strips
for obj in bpy.data.objects:
    if obj.type == 'ARMATURE':
        print(f"Armature: {obj.name}")
        if obj.animation_data:
            print(f"  Active action: {obj.animation_data.action.name if obj.animation_data.action else 'None'}")
            for track in obj.animation_data.nla_tracks:
                print(f"  NLA track: {track.name}")
                for strip in track.strips:
                    print(f"    Strip: {strip.name}, frames {strip.frame_start}-{strip.frame_end}")

步骤 3:检查材质导出损失

运行上文的材质提取。对于角色,请留意:

  • 程序化皮肤纹理节点(Noise → 颜色变化)——这些内容会丢失
  • 面料粗糙度上的 Color Ramp——会丢失,粗糙度会显得平坦
  • 决策:将程序化变化烘焙到图像纹理中,或在运行时修补粗糙度值

步骤 4:导出

blender \
  --background "/path/to/character.blend" \
  --python-expr "
import bpy, os, tempfile
export_dir = tempfile.gettempdir()
bpy.ops.export_scene.gltf(
    filepath=os.path.join(export_dir, 'character.glb'),
    export_format='GLB',
    export_apply=False,
    export_animations=True,
    export_nla_strips=True,
    export_cameras=False,
    export_lights=False,
    export_draco_mesh_compression_enable=False,
    export_skins=True,
    export_morph=True,
)
print('done:', os.path.getsize(os.path.join(export_dir, 'character.glb')) / 1024 / 1024, 'MB')
"

步骤 5:验证动画已导出

npx @gltf-transform/cli inspect character.glb | grep -i anim

预期输出:3 个动画(Idle、Walk、Run)。如果为 0,请检查 NLA 条带是否已静音,或轨道是否设置为独奏。

步骤 6:优化

npx @gltf-transform/cli resize character.glb char_resized.glb --width 1024 --height 1024
npx @gltf-transform/cli webp char_resized.glb char_webp.glb --quality 90
npx @gltf-transform/cli draco char_webp.glb character_final.glb

步骤 7:设置运行时动画(Three.js)

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import * as THREE from 'three';

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');

const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);

loader.load('/character_final.glb', (gltf) => {
    const mixer = new THREE.AnimationMixer(gltf.scene);
    const clips = gltf.animations; // [Idle, Walk, Run]
    const idleAction = mixer.clipAction(clips.find(c => c.name === 'Idle'));
    idleAction.play();
    // Animate mixer in render loop: mixer.update(delta)
});

示例 2:调试材质导出损失(粗糙度显得平坦)

**场景:**导出后,金属面板材质在 Three.js 中显得均匀、平坦且闪亮。在 Blender 中,它通过 Noise Texture → Color Ramp → 粗糙度输入具有有趣的粗糙度变化。

步骤 1:在 Blender 中确认问题

import bpy, json

mat = bpy.data.materials.get("MetalPanel")
if mat and mat.use_nodes:
    for node in mat.node_tree.nodes:
        print(f"Node: {node.type} - {node.name}")
        for inp in node.inputs:
            if inp.is_linked:
                print(f"  Input '{inp.name}': linked to something")

预期输出显示:

Node: BSDF_PRINCIPLED - Principled BSDF
  Input 'Roughness': linked to something
Node: VALTORGB - Color Ramp         <-- this will NOT export
Node: TEX_NOISE - Noise Texture     <-- this will NOT export

步骤 2:了解 GLTF 接收到的内容

导出过程会导出 Principled BSDF 的粗糙度输入。连接到 Color Ramp 时,GLTF 导出器会采用输入插槽的default_value(回退值),通常为 0.5——因此表面完全平坦。

步骤 3A:在 Blender 中通过烘焙修复(质量最佳)

import bpy

# Select the object
obj = bpy.data.objects["MetalPanelMesh"]
bpy.context.view_layer.objects.active = obj
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)

# Create a new image to bake into
bake_img = bpy.data.images.new("MetalPanel_roughness_baked", width=1024, height=1024)
bake_img.colorspace_settings.name = 'Non-Color'

# Add image texture node to material
mat = obj.active_material
nodes = mat.node_tree.nodes
img_node = nodes.new('ShaderNodeTexImage')
img_node.image = bake_img
nodes.active = img_node

# Bake roughness (use ROUGHNESS pass or EMIT trick)
bpy.context.scene.cycles.bake_type = 'ROUGHNESS'
bpy.ops.object.bake(type='ROUGHNESS', save_mode='INTERNAL')

# Save baked image
import tempfile, os
bake_path = os.path.join(tempfile.gettempdir(), 'MetalPanel_roughness_baked.png')
bake_img.filepath_raw = bake_path
bake_img.file_format = 'PNG'
bake_img.save()
print(f"Baked roughness to {bake_path}")

然后将新的图像纹理节点连接到 Roughness 输入,并重新导出。

步骤 3B:在 Three.js 运行时修复(快速修补)

如果无法烘焙,请在加载后覆盖材质粗糙度:

loader.load('/metal_panel.glb', (gltf) => {
    gltf.scene.traverse((child) => {
        if (child.isMesh && child.material) {
            const mats = Array.isArray(child.material) ? child.material : [child.material];
            mats.forEach(mat => {
                if (mat.name === 'MetalPanel') {
                    // Instead of flat 0.5, set a textured roughness or varied value
                    mat.roughness = 0.3;  // adjust to match intended look
                    mat.metalness = 0.9;
                    mat.needsUpdate = true;
                }
            });
        }
    });
});

步骤 4:验证修复

重新导出并运行验证清单。在 Babylon.js Sandbox 中,将金属面板材质与 Blender 视口截图进行比较,确认粗糙度变化得以保留。

关键规则

1. 导出时 MCP 服务超时

Blender MCP 服务无法处理 GLTF 导出——这类操作会超出超时时间。请始终使用无头 CLI:

blender --background "scene.blend" --python-expr "
import bpy, os
export_path = 'output.glb'
os.makedirs(os.path.dirname(export_path), exist_ok=True)
bpy.ops.export_scene.gltf(
    filepath=export_path,
    export_format='GLB',
    export_apply=False,
    export_animations=True,
    export_nla_strips=True,
    export_cameras=True,
    export_lights=False,
    export_draco_mesh_compression_enable=False,
)
print(f'Size: {os.path.getsize(export_path)/1024/1024:.1f} MB')
"
2. 导出时不要应用修改器

设置 export_apply=False。数组修改器(圆形阵列、线性重复)烘焙后会使文件体积大幅膨胀。应在运行时复制它们。

示例:通过 Array 修改器创建 16 个滚轮实例,导出的 GLB 约为 1 MB;烘焙后则约为 56 MB。

3. 首次导出时不要使用 Draco

如果计划使用 gltf-transform 优化,请不要使用 Draco 压缩导出。对已有 Draco 压缩的数据重新编码会损坏网格。最后一步再应用 Draco。

4. 程序化纹理无法导出到 GLTF

这些 Blender 节点设置在导出时会丢失:

节点设置丢失内容解决方法
Noise Texture → roughness整个程序化节点链烘焙为纹理,或在运行时进行着色器补丁
粗糙度纹理上的 Color Ramp数值重映射范围手动设置粗糙度值,或在运行时重映射
程序化凹凸(Noise → Bump)凹凸细节在 Blender 中烘焙法线贴图
带有复杂因子的 Mix Shader混合逻辑导出前简化为单个 BSDF

可以导出的内容: 固定的粗糙度/金属度值、图像纹理(不包含 Color Ramp 重映射)、烘焙的法线贴图、PBR 纹理集(baseColor、metallicRoughness、normal)。

5. GLTF 名称映射

Blender 中的名称在 GLTF 中会转换:

  • 空格 → 下划线
  • 点号 → 删除
  • 末尾空格 → 末尾下划线
BlenderGLTF
RINGS ball LRINGS_ball_L
Sphere.003Sphere003
RINGS L.001RINGS_L001
RINGS S (末尾有空格)RINGS_S_

在代码中引用网格时,务必检查导出的 GLB 中的名称,而不是 Blender 中的名称。

6. 切勿使用 gltf-transform optimize

optimize 命令包含会破坏网格几何的 simplify。请改用单独的步骤:

# Resize textures (max 1024x1024)
npx @gltf-transform/cli resize input.glb resized.glb --width 1024 --height 1024

# WebP texture compression
npx @gltf-transform/cli webp resized.glb webp.glb --quality 90

# Draco mesh compression (LAST step)
npx @gltf-transform/cli draco webp.glb output.glb
7. 为包含空格的路径加引号

Blender 项目路径通常包含空格。务必使用双引号:

blender --background "$HOME/Downloads/blend 3/scene.blend" ...

场景提取模式

提取完整层级结构,包括材质、变换和修改器:

import bpy, json

def extract_hierarchy(obj, depth=0):
    data = {
        "name": obj.name,
        "type": obj.type,
        "location": list(obj.location),
        "rotation": list(obj.rotation_euler),
        "scale": list(obj.scale),
        "visible": not obj.hide_viewport,
        "children": [],
    }
    if obj.type == 'MESH' and obj.data:
        data["vertices"] = len(obj.data.vertices)
        data["faces"] = len(obj.data.polygons)
        data["materials"] = [slot.material.name for slot in obj.material_slots if slot.material]
    if obj.type == 'LIGHT':
        data["light_type"] = obj.data.type
        data["energy"] = obj.data.energy
        data["color"] = list(obj.data.color)
        if obj.data.type == 'AREA':
            data["size"] = obj.data.size
            data["size_y"] = obj.data.size_y
    # Array modifiers (important for runtime replication)
    for mod in obj.modifiers:
        if mod.type == 'ARRAY':
            data.setdefault("modifiers", []).append({
                "type": "ARRAY",
                "count": mod.count,
                "offset_object": mod.offset_object.name if mod.offset_object else None,
            })
    for child in obj.children:
        data["children"].append(extract_hierarchy(child, depth + 1))
    return data

scene_data = {
    "name": bpy.context.scene.name,
    "fps": bpy.context.scene.render.fps,
    "frame_start": bpy.context.scene.frame_start,
    "frame_end": bpy.context.scene.frame_end,
    "objects": [],
}

for obj in bpy.context.scene.objects:
    if obj.parent is None:
        scene_data["objects"].append(extract_hierarchy(obj))

print(json.dumps(scene_data, indent=2))

材质提取模式

import bpy, json

def extract_materials():
    materials = []
    for mat in bpy.data.materials:
        if not mat.use_nodes:
            continue
        info = {"name": mat.name, "nodes": []}
        for node in mat.node_tree.nodes:
            node_data = {"type": node.type, "name": node.name}
            if node.type == 'BSDF_PRINCIPLED':
                for inp in node.inputs:
                    if inp.is_linked:
                        node_data[inp.name] = "linked"
                    elif hasattr(inp, 'default_value'):
                        val = inp.default_value
                        try:
                            node_data[inp.name] = list(val)
                        except TypeError:
                            node_data[inp.name] = float(val)
            if node.type == 'TEX_IMAGE' and node.image:
                node_data["image"] = node.image.filepath
                node_data["size"] = [node.image.size[0], node.image.size[1]]
            info["nodes"].append(node_data)
        materials.append(info)
    return materials

print(json.dumps(extract_materials(), indent=2))

动画关键帧提取

import bpy, json

def extract_animation(obj):
    if not obj.animation_data or not obj.animation_data.action:
        return None
    tracks = []
    for fc in obj.animation_data.action.fcurves:
        keyframes = []
        for kp in fc.keyframe_points:
            keyframes.append({
                "frame": int(kp.co[0]),
                "value": float(kp.co[1]),
                "interpolation": kp.interpolation,
            })
        tracks.append({
            "data_path": fc.data_path,
            "index": fc.array_index,
            "keyframes": keyframes,
        })
    return {"object": obj.name, "tracks": tracks}

animations = []
for obj in bpy.data.objects:
    anim = extract_animation(obj)
    if anim:
        animations.append(anim)

print(json.dumps(animations, indent=2))

GLTF 导出设置参考

设置值原因
export_format'GLB'单个二进制文件
export_applyFalse不烘焙修改器(Array 等)
export_animationsTrue包含动画数据
export_nla_stripsTrue将 NLA 条带烘焙到动作中
export_camerasTrue包含摄像机装置
export_lightsFalse在运行时处理灯光(Three.js/R3F)
export_draco_mesh_compression_enableFalse稍后通过 gltf-transform 应用 Draco

纹理优化流程

目标:在保持可接受视觉质量的前提下,尽可能减小 GLB 体积。

Blender export (no Draco) → resize (1K max) → WebP (q90) → Draco
   ~22 MB                    ~3.7 MB           ~3.7 MB      ~1 MB

要点:

  • 4K 纹理(4096x4096)每张约占 89 MB GPU 内存。1K 纹理约占 5.6 MB,减少 16 倍。
  • PNG 金属度粗糙度纹理使用质量为 85-90 的 WebP 压缩,效果良好。
  • 纹理降采样对移动 GPU(Adreno、Mali)的提升最明显。
  • 使用以下命令检查:npx @gltf-transform/cli inspect model.glb

具体命令和质量指标请参阅 references/texture-optimization.md。

资源集成

配置后,可通过 Blender MCP 使用:

集成功能
PolyHaven搜索、下载并导入免费的 HDRI、纹理和 3D 模型,自动设置材质
Sketchfab搜索并下载模型(需要访问令牌)
Hyper3D Rodin根据文本描述或参考图像生成 3D 模型
Hunyuan3D根据文本提示词、图像或两者创建 3D 资源

用法示例和工作流模式请参阅 references/asset-integrations.md。

已知错误及解决方法

完整的错误表请参阅 references/errors.md。

数据输出

  • 对于小型结果(场景信息、单个对象),使用 print() + json.dumps()
  • 对于大型提取结果(完整层级结构、动画数据、材质报告),使用 tempfile.gettempdir()
  • 始终包含元数据:场景名称、fps、帧范围、Blender 版本

评论

支持 Markdown