Blender MCP + Three.js 仓储数字孪生实战:从建模到交互
1. 从零拆解为什么选 Blender MCP 做仓储数字孪生1.1 这个项目到底在解决什么问题做过仓储可视化的人都知道传统做法无非两条路要么用 Three.js 手搓几何体货架、叉车、传送带全靠BoxGeometry拼要么直接上 Unity/UE效果是好了但打包体积、加载速度、前端集成成本全都上去了。我最早做第一个仓储看板的时候就是用 Three.js 硬拼了 200 多个立方体当货架结果客户看了一眼说“像乐高”当场社死。这个项目的核心思路是把建模这件事交回给 Blender然后通过MCPModel Context Protocol把 Blender 里的场景数据、材质信息、层级结构以一种结构化的方式喂给前端 Three.js 渲染层。说白了就是让专业的人用专业的工具建模前端只负责“接数据、渲染、交互”各司其职。那 Antigravity 在这里扮演什么角色它是一个 AI 驱动的开发代理环境可以理解成一个能帮你写代码、调工具、跑流程的智能工作台。你把 Blender MCP 挂上去之后它就能读取 Blender 场景里的对象树、材质、变换矩阵甚至能根据你的自然语言描述去操作 Blender。这个组合的想象空间在于你描述一个仓储场景AI 帮你生成 Blender 脚本Blender 执行建模MCP 把结果同步到前端项目里。适合谁来参考这篇内容三类人一是做智慧园区、智慧仓储可视化的前端工程师二是想从传统 Three.js 手搓转型到“建模工具 数据管线”模式的 3D 开发者三是对 MCP 协议感兴趣、想看看它在 3D 领域怎么落地的人。不需要你是 Blender 大神但至少得知道 Blender 的基本操作不然连场景都搭不出来。1.2 为什么不用 Unity 数字孪生那套Unity 做数字孪生确实成熟资源商店里现成的仓储模型一抓一大把光照、后处理、物理模拟都是开箱即用。但问题也很明显WebGL 导出后的包体动辄几十兆首屏加载在弱网环境下直接劝退而且 Unity 的 WebGL 渲染管线和 Three.js 是两套东西你想在同一个页面里混用 Vue 3 的 UI 组件和 Unity 画布通信成本很高。Three.js 的优势在于轻、灵活、和前端生态无缝衔接。Vue 3 管 UI 和状态Three.js 管 3D 场景两者通过响应式数据桥接改一个货架的状态UI 和 3D 场景同时更新这种体验是 Unity 很难做到的。代价就是建模和场景管理得自己扛而 Blender MCP 正好补上了这块短板。1.3 Blender MCP 到底传了什么数据很多人以为 MCP 就是把.glb文件传过去完事其实远不止。Blender MCP 暴露的是一套场景图Scene Graph的语义描述包括对象层级关系哪个货架属于哪个区域每个对象的变换矩阵位置、旋转、缩放材质与贴图引用自定义属性比如你在 Blender 里给货架打的shelf_id、capacity标签这些数据到了前端不是直接扔给 Three.js 就完事而是要先经过一层数据适配把 Blender 的 Z-up 坐标系转成 Three.js 的 Y-up把材质引用映射成 Three.js 的MeshStandardMaterial把自定义属性挂到对应的Object3D.userData上。这一步做得好不好直接决定了后面交互逻辑写起来是顺滑还是痛苦。注意Blender 默认 Z 轴向上Three.js 默认 Y 轴向上。如果你不做坐标转换模型导进来要么躺平要么倒立这个坑我踩过不止一次。2. 环境搭建Antigravity 接入 Blender MCP 的完整流程2.1 前置准备与版本选择先把家伙什备齐。你需要的东西清单如下组件推荐版本说明Blender4.2 LTSLTS 版本稳定MCP 插件兼容性好Antigravity最新稳定版确保 MCP 协议支持完整Node.js20.x LTS前端构建和 MCP 服务端都依赖Vue 33.4组合式 API 写起来更顺手Three.jsr160新版本对 WebGL2 支持更好Blender 版本这块我要多说一句。4.0 之后 Blender 的 Python API 有一些破坏性变更特别是bpy.ops里的一些操作符参数变了。如果你跟着网上老教程走很可能在 MCP 插件初始化那一步就报错。我建议直接用 4.2 LTS这个版本在插件生态和 API 稳定性之间平衡得最好。Antigravity 这边安装完之后第一件事是确认 MCP 功能是否可用。在设置里找 MCP Servers 相关的配置项如果找不到说明你的版本可能不支持需要更新。这里有个常见问题有些人安装后打开发现提示账号验证之类的信息这种情况一般是登录态没同步重新走一遍登录流程即可不要反复重装。2.2 Blender 侧 MCP 插件安装Blender 的 MCP 插件安装有两种方式我推荐用命令行装因为图形界面装插件有时候会静默失败你都不知道问题出在哪。打开终端找到 Blender 的 Python 环境。macOS 下通常在/Applications/Blender.app/Contents/Resources/4.2/python/bin/python3.11Windows 下在 Blender 安装目录的4.2/python/bin/里。用这个 Python 去装 MCP 相关的包/path/to/blender/python -m pip install blender-mcp装完之后在 Blender 的脚本编辑器里跑一段初始化代码把 MCP 服务注册进去import bpy from blender_mcp import register_mcp_server # 注册 MCP 服务指定端口和场景同步范围 register_mcp_server( port9876, sync_scope[objects, materials, custom_props], auto_startTrue ) print(MCP server registered on port 9876)这段代码做了三件事监听 9876 端口、声明同步范围对象、材质、自定义属性、设置 Blender 启动时自动开启服务。sync_scope这个参数很关键如果你只做静态展示可以不加custom_props减少数据传输量但如果要做交互比如点击货架显示库存那custom_props必须开。实操心得Blender 的脚本编辑器里跑完代码后记得把这段脚本保存成.py文件然后在Edit Preferences Add-ons里注册成启动脚本。不然每次重启 Blender 都要手动跑一遍烦得很。2.3 Antigravity 侧连接配置Blender 那边服务起来了接下来在 Antigravity 里配置连接。打开 Antigravity 的 MCP 配置文件通常在用户目录的.antigravity/mcp.json加入 Blender 的服务地址{ mcpServers: { blender-warehouse: { url: http://localhost:9876, type: blender, options: { coordinateSystem: z-up, unitScale: 1.0, syncInterval: 2000 } } } }coordinateSystem告诉 Antigravity 源数据是 Z-up它会在转发给前端时自动做转换。syncInterval是同步间隔单位毫秒2000 表示每 2 秒拉一次场景变更。如果你做的是实时性要求高的场景比如 AGV 小车位置更新可以调到 500 甚至更低但要注意 Blender 那边的性能开销。配置写完后重启 Antigravity在 MCP 面板里应该能看到blender-warehouse的状态变成 connected。如果显示 error先检查 Blender 那边服务是否在跑再检查端口是否被占用。2.4 前端项目初始化前端这块我用 Vue 3 TypeScript Three.js 的组合。先用 Vite 起项目npm create vitelatest warehouse-digital-twin -- --template vue-ts cd warehouse-digital-twin npm install three types/three然后装一个关键的库three-stdlib它提供了很多 Three.js 官方示例里的实用工具比如GLTFLoader、OrbitControls的 TypeScript 版本npm install three-stdlib项目结构我建议这样组织src/ components/ TwinViewer.vue # 3D 场景容器 WarehouseUI.vue # 仓储信息面板 composables/ useThreeScene.ts # Three.js 场景管理 useMCPBridge.ts # MCP 数据桥接 types/ warehouse.ts # 仓储数据类型定义这个结构的好处是Three.js 的场景逻辑和 Vue 的 UI 逻辑彻底解耦。useThreeScene只管渲染useMCPBridge只管数据同步Vue 组件只负责把两者粘起来。后面你要换渲染引擎或者换数据源改动范围都可控。3. 核心实现从 Blender 场景到 Three.js 渲染3.1 仓储场景的 Blender 建模要点在 Blender 里搭仓储场景不是随便摆几个立方体就完事。为了让 MCP 同步过来的数据能直接用于交互建模时就要有意识地把语义信息埋进去。货架建模用Array Modifier复制货架层板但复制出来的对象要Apply成独立对象不然 MCP 同步过去只有一个对象前端没法单独控制每一层。每个货架对象在Object Properties Custom Properties里加三个属性shelf_id货架唯一标识字符串capacity总库位数整数zone所属区域字符串如 A区地面和墙体单独建一个Ground对象和一个Walls对象不要和货架合并。前端渲染时地面和墙体的材质可以单独替换比如根据昼夜切换颜色合并了就不好处理。AGV 小车和传送带这些动态对象要单独命名前缀统一用dynamic_比如dynamic_agv_01、dynamic_conveyor_main。MCP 同步时可以根据前缀过滤动态对象走单独的更新通道静态对象只在初始化时拉一次。注意Blender 里的对象名不要用中文虽然 Blender 支持但传到前端后编码问题会让你怀疑人生。用英文 下划线清晰又安全。3.2 MCP 数据桥接层的设计useMCPBridge.ts是整个项目的核心枢纽它要做的事情包括连接 MCP 服务、拉取场景数据、转换坐标系、映射材质、挂载自定义属性、监听变更。先定义数据类型interface MCPObject { name: string; type: mesh | empty | light; transform: { position: [number, number, number]; rotation: [number, number, number]; scale: [number, number, number]; }; material?: string; customProps?: Recordstring, any; children?: MCPObject[]; } interface WarehouseScene { objects: MCPObject[]; materials: Recordstring, MaterialDef; timestamp: number; }然后是坐标转换函数。Blender 的 Z-up 转 Three.js 的 Y-up本质上是把(x, y, z)变成(x, z, -y)function convertCoordinate(pos: [number, number, number]): [number, number, number] { return [pos[0], pos[2], -pos[1]]; } function convertRotation(rot: [number, number, number]): [number, number, number] { // Blender 用欧拉角 XYZ 顺序Three.js 默认也是 XYZ // 但坐标轴映射后旋转需要重新计算 return [rot[0], rot[2], -rot[1]]; }这个转换看着简单但实际做的时候很容易搞混。我的经验是拿一个在 Blender 里位于(1, 0, 0)的立方体做测试转换后应该在 Three.js 里位于(1, 0, 0)再拿一个位于(0, 1, 0)的转换后应该在(0, 0, -1)。用这两个基准点验证基本不会错。材质映射这块Blender 的 Principled BSDF 和 Three.js 的MeshStandardMaterial参数对应关系如下Blender 参数Three.js 参数说明Base Colorcolor基础色Metallicmetalness金属度Roughnessroughness粗糙度Emissionemissive自发光Alphaopacity透明度写一个映射函数把 MCP 传来的材质定义转成 Three.js 材质实例。注意color需要从 sRGB 转到线性空间不然颜色会偏亮。3.3 Three.js 场景初始化与对象挂载useThreeScene.ts负责创建场景、相机、渲染器以及把 MCP 数据实例化成 Three.js 对象。import * as THREE from three; import { OrbitControls } from three-stdlib; export function useThreeScene(container: HTMLElement) { const scene new THREE.Scene(); scene.background new THREE.Color(0x1a1a2e); const camera new THREE.PerspectiveCamera( 45, container.clientWidth / container.clientHeight, 0.1, 1000 ); camera.position.set(20, 15, 20); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(container.clientWidth, container.clientHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.shadowMap.enabled true; container.appendChild(renderer.domElement); const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.dampingFactor 0.05; controls.maxPolarAngle Math.PI / 2.2; // 灯光 const ambient new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambient); const directional new THREE.DirectionalLight(0xffffff, 1.2); directional.position.set(10, 20, 10); directional.castShadow true; directional.shadow.mapSize.set(2048, 2048); scene.add(directional); // 渲染循环 function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); return { scene, camera, renderer, controls }; }setPixelRatio那里限制最大为 2是因为有些高分屏设备devicePixelRatio能到 3 甚至 4渲染压力翻倍但肉眼几乎看不出差别。限制在 2 是性能和画质的平衡点。对象挂载的逻辑遍历 MCP 传来的对象树根据type创建对应的 Three.js 对象。mesh类型用BoxGeometry或从 Blender 导出的几何数据创建BufferGeometryempty类型创建Object3D作为容器。每个对象创建后把customProps挂到userData上function mountObject(mcpObj: MCPObject, parent: THREE.Object3D) { let threeObj: THREE.Object3D; if (mcpObj.type mesh) { const geometry new THREE.BoxGeometry(1, 1, 1); const material getMaterial(mcpObj.material); threeObj new THREE.Mesh(geometry, material); threeObj.castShadow true; threeObj.receiveShadow true; } else { threeObj new THREE.Object3D(); } threeObj.name mcpObj.name; threeObj.position.fromArray(convertCoordinate(mcpObj.transform.position)); threeObj.rotation.fromArray(convertRotation(mcpObj.transform.rotation)); threeObj.scale.fromArray(mcpObj.transform.scale); threeObj.userData { ...mcpObj.customProps }; parent.add(threeObj); if (mcpObj.children) { mcpObj.children.forEach(child mountObject(child, threeObj)); } }这里有个性能优化点如果货架数量很多比如 500 个以上每个货架都创建独立的Mesh会导致 draw call 爆炸。这时候要用InstancedMesh把相同几何体和材质的货架合并成一个实例化网格。但实例化之后单独控制某个货架的高亮就麻烦了需要用instanceId来区分。我的建议是静态货架用实例化动态对象AGV、传送带用独立 Mesh兼顾性能和灵活性。3.4 交互逻辑点击货架显示库存信息数字孪生和纯展示的区别就在交互。点击货架弹出库存面板这个功能看着简单但实现起来有几个坑。射线检测Raycaster是标准做法const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); renderer.domElement.addEventListener(click, (event) { const rect renderer.domElement.getBoundingClientRect(); mouse.x ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y -((event.clientY - rect.top) / rect.height) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(scene.children, true); if (intersects.length 0) { const hit intersects[0].object; // 向上查找带 shelf_id 的父对象 let target hit; while (target !target.userData.shelf_id) { target target.parent; } if (target) { showShelfInfo(target.userData); } } });第一个坑intersectObjects的第二个参数recursive要设为true否则只会检测顶层对象货架的子网格点不到。第二个坑点击到的是子网格但shelf_id挂在父对象上所以要向上遍历查找。第三个坑如果场景里有透明对象比如玻璃隔断射线会穿透需要根据material.transparent过滤。高亮效果用emissive实现function highlightShelf(obj: THREE.Object3D) { obj.traverse(child { if (child instanceof THREE.Mesh) { const mat child.material as THREE.MeshStandardMaterial; mat.emissive new THREE.Color(0x00ff88); mat.emissiveIntensity 0.3; } }); }记得在点击其他货架时取消上一个的高亮不然整个场景会变成“绿灯大会”。4. 常见问题与排查技巧实录4.1 MCP 连接失败排查表现象可能原因排查方法Antigravity 显示 MCP 未连接Blender 服务未启动在 Blender 脚本编辑器重跑注册代码连接成功但拉不到数据端口被占用或防火墙拦截lsof -i :9876检查端口数据拉到了但对象位置错乱坐标系未转换检查coordinateSystem配置材质显示为纯黑颜色空间未转换确认 sRGB 转线性空间逻辑场景加载后卡顿draw call 过多用renderer.info.render.calls查看这个表里的问题我基本都遇到过。最坑的是“材质显示为纯黑”排查了半天以为是 MCP 传的数据有问题最后发现是 Three.js r152 之后颜色管理默认开启Blender 传来的 sRGB 颜色值没有转换导致颜色计算错误。解决办法是在renderer初始化时设置THREE.ColorManagement.enabled true然后在材质创建时用new THREE.Color().setStyle(hex, THREE.SRGBColorSpace)。4.2 性能优化的几个关键点Three.js 场景卡顿90% 的情况是 draw call 太多或者几何体面数太高。用renderer.info可以实时查看console.log(Draw calls:, renderer.info.render.calls); console.log(Triangles:, renderer.info.render.triangles); console.log(Geometries:, renderer.info.memory.geometries);仓储场景的合理指标draw call 控制在 100 以内三角形数量控制在 50 万以内。超了就要优化。优化手段按优先级排序合并静态几何体用BufferGeometryUtils.mergeGeometries把相同材质的静态对象合并成一个几何体draw call 直接降一个数量级。实例化重复对象货架、托盘这些重复出现的对象用InstancedMesh。LODLevel of Detail远处的货架用低面数模型近处用高面数。Three.js 的LOD类可以直接用。视锥剔除Three.js 默认开启但如果你手动设置了frustumCulled false记得改回来。纹理压缩用 KTX2 格式的压缩纹理显存占用能降 70% 以上。实操心得优化之前先用 Chrome DevTools 的 Performance 面板录一段看看时间到底花在 JS 执行、渲染还是 GPU 上。盲目优化容易做无用功。我见过有人花两天优化几何体最后发现瓶颈在 Vue 的响应式更新上。4.3 数据同步的实时性取舍MCP 的syncInterval设多少合适这个没有标准答案取决于你的场景需求纯静态展示不需要轮询初始化拉一次就行syncInterval设为 0。货架状态更新入库/出库2-5 秒一次足够人眼对库存数字的变化不敏感。AGV 位置追踪需要 200-500 毫秒级但这时候建议走 WebSocket 推送而不是轮询。轮询的代价是 Blender 那边每次都要遍历场景树、序列化数据。如果场景对象超过 1000 个2 秒一次的轮询会让 Blender 明显卡顿。解决办法是增量同步MCP 只传变更的对象而不是全量场景。这需要在 Blender 侧维护一个脏标记dirty flag对象变换或属性变更时打标记同步时只传带标记的对象。4.4 前端渲染的常见视觉问题模型导入后“躺平”或“倒立”前面说过是坐标系问题。但还有一种情况模型位置对了但旋转不对。这通常是因为 Blender 里的旋转是欧拉角而 Three.js 的rotation设置顺序可能不一致。Blender 默认 XYZ 顺序Three.js 也是 XYZ但坐标轴映射后旋转的符号需要调整。我的经验是位置转换用(x, z, -y)旋转转换用(rx, rz, -ry)缩放不变。如果旋转还是不对用四元数Quaternion代替欧拉角能避免万向节死锁和顺序问题。阴影闪烁Shadow Acne是另一个常见问题。表现为物体表面出现条纹状阴影。解决办法是调整directionalLight.shadow.bias通常设为-0.0001到-0.001之间。如果还不行增大shadow.mapSize或者调整shadow.camera的near/far范围。4.5 Antigravity 使用中的几个坑Antigravity 作为开发代理环境在跑 MCP 相关任务时偶尔会出现执行中断的情况。我遇到过的原因主要有两个一是网络请求超时MCP 服务响应慢导致代理认为任务失败二是 Blender 那边脚本报错异常没有正确捕获导致整个链路断掉。应对方法在 Blender 的 MCP 服务代码里加全局异常捕获任何操作失败都返回结构化的错误信息而不是直接抛异常。Antigravity 这边把 MCP 请求的超时时间调大默认可能是 30 秒调到 60 秒能覆盖大部分场景。还有一个账号验证的问题有些人用着用着突然提示需要验证。这种情况一般是登录态过期重新登录即可。不要频繁切换账号容易触发风控。5. 从静态孪生到动态交互的扩展思路5.1 接入实时数据源现在的方案是 Blender 建模 MCP 同步 Three.js 渲染数据是静态的。要变成真正的数字孪生需要接入实时数据。常见的做法是在 MCP 桥接层加一个 WebSocket 客户端后端推送库存变更、AGV 位置、温湿度等数据前端收到后更新对应对象的userData和视觉表现。比如 AGV 位置更新ws.onmessage (event) { const data JSON.parse(event.data); if (data.type agv_position) { const agv scene.getObjectByName(dynamic_agv_${data.id}); if (agv) { // 用插值平滑移动避免跳变 gsap.to(agv.position, { x: data.x, y: 0, z: data.z, duration: 0.5, ease: power2.out }); } } };用 GSAP 做插值是因为 AGV 上报的位置是离散的直接设置会导致小车“瞬移”。插值让移动看起来连续体验好很多。5.2 货架状态的可视化编码库存状态用颜色编码是最直观的绿色表示正常黄色表示低库存红色表示缺货。实现方式是在userData里加一个stockLevel字段然后写一个更新函数function updateShelfColor(shelf: THREE.Object3D, level: number) { const color level 0.5 ? 0x00ff88 : level 0.2 ? 0xffaa00 : 0xff4444; shelf.traverse(child { if (child instanceof THREE.Mesh) { const mat child.material as THREE.MeshStandardMaterial; mat.color.setHex(color); } }); }但这里有个问题如果货架用了实例化网格单独改一个实例的颜色需要用setColorAt方法而不是改材质。这是实例化的代价用之前要想清楚。5.3 后续可以扩展的方向这个项目目前做的是“上篇”核心是把 Blender MCP 到 Three.js 的管线跑通。后面可以扩展的方向很多接入路径规划算法让 AGV 自动寻路、用着色器实现货架的热力图效果、加一个时间轴回放历史库存变化、甚至用 AI 根据仓库布局自动生成最优货架排列方案。我个人最感兴趣的是用 Antigravity 的 AI 能力做场景的自动生成。你描述“一个 50 米乘 30 米的仓库A 区放 10 排货架每排 5 层B 区放传送带”Antigravity 生成 Blender 脚本Blender 执行建模MCP 同步到前端。整个流程跑通之后做仓储可视化就像写一段描述那么简单。这个方向我还在摸索等跑通了再写一篇分享。最后分享一个小技巧Blender 里给对象加自定义属性的时候属性名用蛇形命名shelf_id而不是shelfId因为 MCP 序列化后传到前端蛇形命名和大多数后端 API 的风格一致省得来回转换。这个细节看着小但能省不少事。