blender-mcp by vladmdgolam/agent-skills
npx skills add https://github.com/vladmdgolam/agent-skills --skill blender-mcp使用结构化 MCP 工具(get_scene_info、screenshot)进行快速检查。
对于任何非简单任务:层次结构遍历、材质提取、动画烘焙、批量操作,请使用**execute_python**。它提供完整的 bpy API 访问权限,并避免了工具模式限制。
对于 GLTF 导出,请使用无头 CLI —— MCP 服务器在导出操作时会超时。
get_scene_info —— 验证连接(默认端口 9876)print("ok") 执行 execute_python —— 验证 Python 工作正常screenshot —— 验证视口捕获工作正常广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
如果 MCP 无响应,请检查 Blender MCP 插件是否已启用以及套接字服务器是否正在运行。
这是端到端的线性叙述。请按顺序遵循这些步骤。不要跳过任何步骤。
在操作任何其他内容之前,确认 MCP 处于活动状态:
# 在 MCP 工具调用中:
get_scene_info
execute_python: print("ok")
screenshot
如果任何步骤失败,请先停止并修复 MCP 连接。请参阅已知错误。
运行完整的层次结构提取以了解您正在处理的内容:
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))
查找:
material_slots)运行材质提取,以便在提交导出之前捕获可能导致导出失真的设置:
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))
在继续之前,请查看所有警告。决定:是现在烘焙程序化纹理,还是在导出后在运行时修补材质。
MCP 服务器无法处理 GLTF 导出(会超时)。始终使用无头 CLI:
# 如果 'blender' 在 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在成功导出后运行。始终使用单独的步骤,切勿使用 optimize:
# 1. 首先检查原始导出
npx @gltf-transform/cli inspect output.glb
# 2. 调整纹理大小(Web/移动端最大 1K)
npx @gltf-transform/cli resize output.glb resized.glb --width 1024 --height 1024
# 3. WebP 压缩(质量 90 保留细节)
npx @gltf-transform/cli webp resized.glb webp.glb --quality 90
# 4. Draco 网格压缩(最后一步 —— 不可逆)
npx @gltf-transform/cli draco webp.glb final.glb
# 5. 检查最终结果
npx @gltf-transform/cli inspect final.glb
预期的大小缩减:~22 MB 原始文件 → ~3.7 MB (WebP) → ~1 MB (Draco)。有关详细指标,请参阅 references/texture-optimization.md。
在交付之前,运行下面完整的导出后验证清单。
每次导出后,在将 GLB 文件移交进行集成之前,请验证以下内容:
npx @gltf-transform/cli inspect final.glb 并检查:网格数量、纹理数量、纹理大小、动画数量、访问器大小。没有意外的重复。THREE.GLTFLoader: Unknown extension、缺少纹理文件、不支持的 Draco 版本。场景: 您有一个带有人体骨骼的角色,包含 3 个 NLA 动作(空闲、行走、奔跑)、PBR 纹理集,以及通过父级关系附加的武器。您需要一个可用于 Three.js 场景的 Web 就绪 GLB 文件。
步骤 1:健康检查和场景检查
# MCP 工具调用
get_scene_info
execute_python: print("ok")
步骤 2:检查绑定
import bpy, json
# 检查骨骼和 NLA 条带
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:检查可能导致导出失真的材质
运行上面的材质提取。对于角色,请注意:
步骤 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();
// 在渲染循环中更新混合器:mixer.update(delta)
});
场景: 导出后,金属面板材质在 Three.js 中看起来均匀平坦且闪亮。在 Blender 中,它通过噪波纹理 → 颜色渐变 → 粗糙度输入具有有趣的粗糙度变化。
步骤 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 <-- 这不会被导出
Node: TEX_NOISE - Noise Texture <-- 这不会被导出
步骤 2:了解 GLTF 接收到了什么
导出会导出 Principled BSDF 的粗糙度输入。当链接到颜色渐变时,GLTF 导出器会采用输入插槽的 default_value(备用值),该值通常为 0.5 —— 完全平坦。
步骤 3A:通过在 Blender 中烘焙来修复(最佳质量)
import bpy
# 选择对象
obj = bpy.data.objects["MetalPanelMesh"]
bpy.context.view_layer.objects.active = obj
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
# 创建新图像用于烘焙
bake_img = bpy.data.images.new("MetalPanel_roughness_baked", width=1024, height=1024)
bake_img.colorspace_settings.name = 'Non-Color'
# 向材质添加图像纹理节点
mat = obj.active_material
nodes = mat.node_tree.nodes
img_node = nodes.new('ShaderNodeTexImage')
img_node.image = bake_img
nodes.active = img_node
# 烘焙粗糙度(使用 ROUGHNESS 通道或 EMIT 技巧)
bpy.context.scene.cycles.bake_type = 'ROUGHNESS'
bpy.ops.object.bake(type='ROUGHNESS', save_mode='INTERNAL')
# 保存烘焙的图像
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}")
然后将新的图像纹理节点连接到粗糙度输入并重新导出。
步骤 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') {
// 使用带纹理的粗糙度或变化的值,而不是平坦的 0.5
mat.roughness = 0.3; // 调整以匹配预期的外观
mat.metalness = 0.9;
mat.needsUpdate = true;
}
});
}
});
});
步骤 4:验证修复
重新导出并运行验证清单。在 Babylon.js Sandbox 中,将金属面板材质与 Blender 视口截图进行比较,以确认粗糙度变化已保留。
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')
"
设置 export_apply=False。阵列修改器(圆形图案、线性重复)在烘焙时会使文件大小急剧膨胀。应在运行时复制它们。
示例:通过阵列修改器的 16 个滚轮实例 ≈ 1 MB GLB。烘焙后 ≈ 56 MB GLB。
如果您计划使用 gltf-transform 进行优化,请在不使用 Draco 压缩的情况下导出。重新编码现有的 Draco 会损坏网格。将 Draco 应用为最后一步。
这些 Blender 节点设置在导出时会丢失:
| 节点设置 | 丢失的内容 | 解决方法 |
|---|---|---|
| 噪波纹理 → 粗糙度 | 整个程序化链 | 烘焙到纹理,或在运行时进行着色器修补 |
| 粗糙度纹理上的颜色渐变 | 值重映射范围 | 手动设置粗糙度值,或在运行时重映射 |
| 程序化凹凸(噪波 → 凹凸) | 凹凸细节 | 在 Blender 中烘焙法线贴图 |
| 具有复杂因子的混合着色器 | 混合逻辑 | 在导出前简化为单个 BSDF |
确实会导出的内容: 平坦的粗糙度/金属度值、图像纹理(无颜色渐变重映射)、烘焙的法线贴图、PBR 纹理集(baseColor, metallicRoughness, normal)。
Blender 名称在 GLTF 中会进行转换:
| Blender | GLTF |
|---|---|
RINGS ball L | RINGS_ball_L |
Sphere.003 | Sphere003 |
RINGS L.001 | RINGS_L001 |
RINGS S (尾随空格) | RINGS_S_ |
在代码中引用网格时,请始终检查导出的 GLB 中的名称,而不是 Blender 中的名称。
optimizeoptimize 命令包含 simplify,这会破坏网格几何体。请改用单独的步骤:
# 调整纹理大小(最大 1024x1024)
npx @gltf-transform/cli resize input.glb resized.glb --width 1024 --height 1024
# WebP 纹理压缩
npx @gltf-transform/cli webp resized.glb webp.glb --quality 90
# Draco 网格压缩(最后一步)
npx @gltf-transform/cli draco webp.glb output.glb
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
# 阵列修改器(对于运行时复制很重要)
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))
| 设置 | 值 | 原因 |
|---|---|---|
export_format | 'GLB' | 单个二进制文件 |
export_apply | False | 不烘焙修改器(阵列等) |
export_animations | True | 包含动画数据 |
export_nla_strips | True | 将 NLA 条带烘焙到动作中 |
export_cameras | True | 包含相机绑定 |
export_lights | False | 在运行时处理灯光 (Three.js/R3F) |
export_draco_mesh_compression_enable | False | 稍后通过 gltf-transform 应用 Draco |
目标:在可接受的视觉质量下获得最小的 GLB 文件。
Blender 导出 (无 Draco) → 调整大小 (最大 1K) → WebP (质量 90) → Draco
~22 MB ~3.7 MB ~3.7 MB ~1 MB
关键见解:
npx @gltf-transform/cli inspect model.glb有关具体命令和质量指标,请参阅 references/texture-optimization.md。
配置后可通过 Blender MCP 使用:
| 集成 | 功能 |
|---|---|
| PolyHaven | 搜索、下载、导入免费的 HDRIs、纹理和 3D 模型,并自动设置材质 |
| Sketchfab | 搜索和下载模型(需要访问令牌) |
| Hyper3D Rodin | 根据文本描述或参考图像生成 3D 模型 |
| Hunyuan3D | 根据文本提示、图像或两者创建 3D 资产 |
有关使用示例和工作流模式,请参阅 references/asset-integrations.md。
完整的错误表请参阅 references/errors.md。
print() + json.dumps()tempfile.gettempdir()每周安装次数
257
代码仓库
GitHub 星标数
2
首次出现
2026年2月17日
安全审计
安装于
opencode251
codex250
github-copilot249
gemini-cli248
cursor244
kimi-cli240
Use structured MCP tools (get_scene_info, screenshot) for quick inspection.
Use execute_python for anything non-trivial: hierarchy traversal, material extraction, animation baking, bulk operations. It gives full bpy API access and avoids tool schema limitations.
Use headless CLI for GLTF exports — the MCP server times out on export operations.
get_scene_info — verify connection (default port 9876)execute_python with print("ok") — verify Python worksscreenshot — verify viewport capture worksIf MCP is unresponsive, check that the Blender MCP addon is enabled and the socket server is running.
This is the end-to-end linear narrative. Follow these steps in order. Do not skip steps.
Confirm MCP is alive before touching anything else:
# In MCP tool call:
get_scene_info
execute_python: print("ok")
screenshot
If any step fails, stop and fix MCP connectivity first. See Known Errors.
Run the full hierarchy extraction to understand what you're working with:
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))
Look for:
material_slots)Run the material extraction to catch export-lossy setups before committing to an export:
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))
Review all warnings before proceeding. Decide: bake procedural textures now, or patch materials at runtime after export.
The MCP server cannot handle GLTF exports (timeout). Always use headless 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)')
"
Critical flags:
export_apply=False — do not bake modifiers (Array modifier turns 1 MB into 56 MB)export_draco_mesh_compression_enable=False — apply Draco later via gltf-transformRun after a successful export. Always use individual steps, never 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
Expected size reduction: ~22 MB raw → ~3.7 MB (WebP) → ~1 MB (Draco). See references/texture-optimization.md for detailed metrics.
Run the full Post-Export Validation checklist below before shipping.
After every export, verify the following before handing off the GLB for integration:
npx @gltf-transform/cli inspect final.glb and check: mesh count, texture count, texture sizes, animation count, accessor sizes. No unexpected duplication.THREE.GLTFLoader: Unknown extension, missing texture files, unsupported Draco version.Scenario: You have a humanoid character with armature, 3 NLA actions (idle, walk, run), PBR texture set, and a weapon attached via parenting. You need a web-ready GLB for a Three.js scene.
Step 1: Health check and scene inspection
# MCP tool calls
get_scene_info
execute_python: print("ok")
Step 2: Inspect the rig
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}")
Step 3: Check materials for export losses
Run the material extraction above. For a character, watch for:
Step 4: Export
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')
"
Step 5: Verify animations exported
npx @gltf-transform/cli inspect character.glb | grep -i anim
Expected output: 3 animations (Idle, Walk, Run). If 0, check that NLA strips are muted or the tracks are set to solo.
Step 6: Optimize
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
Step 7: Runtime animation setup (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)
});
Scenario: After export, a metal panel material looks uniformly flat and shiny in Three.js. In Blender it had interesting roughness variation from a Noise Texture → Color Ramp → roughness input.
Step 1: Confirm the problem in 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")
Expected output reveals:
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
Step 2: Understand what GLTF received
The export exports the Principled BSDF's roughness input. When linked to a Color Ramp, GLTF exporter takes the default_value of the input socket (fallback), which is typically 0.5 — perfectly flat.
Step 3A: Fix by baking in Blender (best quality)
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}")
Then connect the new image texture node to the Roughness input and re-export.
Step 3B: Fix at runtime in Three.js (quick patch)
If you cannot bake, override the material roughness after load:
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;
}
});
}
});
});
Step 4: Verify fix
Re-export and run validation checklist. In Babylon.js Sandbox, compare the metal panel material against a Blender viewport screenshot to confirm roughness variation is preserved.
The Blender MCP server cannot handle GLTF exports — they exceed the timeout. Always use headless 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')
"
Set export_apply=False. Array modifiers (circular patterns, linear repeats) balloon file size when baked. Replicate them at runtime instead.
Example: 16 roller instances via Array modifier = ~1 MB GLB. Baked = ~56 MB GLB.
If you plan to optimize with gltf-transform, export without Draco compression. Re-encoding existing Draco corrupts meshes. Apply Draco as the final step.
These Blender node setups are lost on export:
| Node Setup | What's Lost | Workaround |
|---|---|---|
| Noise Texture → roughness | Entire procedural chain | Bake to texture, or shader patch at runtime |
| Color Ramp on roughness texture | Value remapping range | Manual roughness values, or runtime remap |
| Procedural bump (Noise → Bump) | Bump detail | Bake normal map in Blender |
| Mix Shader with complex factor | Blend logic | Simplify to single BSDF before export |
What DOES export: flat roughness/metallic values, image textures (without Color Ramp remapping), baked normal maps, PBR texture sets (baseColor, metallicRoughness, normal).
Blender names are transformed in GLTF:
| Blender | GLTF |
|---|---|
RINGS ball L | RINGS_ball_L |
Sphere.003 | Sphere003 |
RINGS L.001 | RINGS_L001 |
RINGS S (trailing space) | RINGS_S_ |
Always check names in the exported GLB, not Blender, when referencing meshes in code.
optimizeThe optimize command includes simplify which destroys mesh geometry. Use individual steps instead:
# 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
Blender project paths often contain spaces. Always double-quote:
blender --background "$HOME/Downloads/blend 3/scene.blend" ...
Full hierarchy with materials, transforms, and modifiers:
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))
| Setting | Value | Why |
|---|---|---|
export_format | 'GLB' | Single binary file |
export_apply | False | Don't bake modifiers (Array, etc.) |
export_animations | True | Include animation data |
export_nla_strips |
Target: smallest GLB with acceptable visual quality.
Blender export (no Draco) → resize (1K max) → WebP (q90) → Draco
~22 MB ~3.7 MB ~3.7 MB ~1 MB
Key insights:
npx @gltf-transform/cli inspect model.glbSee references/texture-optimization.md for concrete commands and quality metrics.
Available through Blender MCP when configured:
| Integration | Capabilities |
|---|---|
| PolyHaven | Search, download, import free HDRIs, textures, and 3D models with auto material setup |
| Sketchfab | Search and download models (requires access token) |
| Hyper3D Rodin | Generate 3D models from text descriptions or reference images |
| Hunyuan3D | Create 3D assets from text prompts, images, or both |
See references/asset-integrations.md for usage examples and workflow patterns.
See references/errors.md for complete error tables.
print() + json.dumps() for small results (scene info, single object)tempfile.gettempdir() for large extraction results (full hierarchy, animation data, material reports)Weekly Installs
257
Repository
GitHub Stars
2
First Seen
Feb 17, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode251
codex250
github-copilot249
gemini-cli248
cursor244
kimi-cli240
shadcn/ui 框架:React 组件库与 UI 设计系统,Tailwind CSS 最佳实践
51,500 周安装
True |
| Bake NLA strips into actions |
export_cameras | True | Include camera rigs |
export_lights | False | Handle lights in runtime (Three.js/R3F) |
export_draco_mesh_compression_enable | False | Apply Draco later via gltf-transform |