1. 从零搭建3D智慧仓储数字孪生的整体思路
1.1 为什么选Blender做建模源头而不是直接Three.js手搓
做智慧仓储数字孪生,第一步永远是模型从哪来。我见过太多团队一上来就用Three.js的BoxGeometry拼货架、拼叉车,结果做出来的东西像儿童积木,甲方看一眼就摇头。Three.js原生几何体适合做抽象示意,但仓储场景里货架的横梁斜撑、堆垛机的立柱导轨、AGV的轮廓倒角,这些细节靠代码堆顶点是不现实的。
Blender在这个环节的价值就体现出来了。它是免费开源的,社区资源极其丰富,仓储相关的货架、托盘、输送线模型在公开模型库里一抓一大把。更重要的是Blender的导出管线对glTF格式支持非常成熟,而glTF又是Three.js生态里最顺手的格式——体积小、加载快、材质信息保留完整。
我自己的习惯是:静态结构全部在Blender里做完,动态逻辑全部留给Three.js。货架、墙体、地面、月台这些不动的东西,在Blender里建好、展UV、贴好材质、命名规范,一次性导出。叉车、AGV、堆垛机这些要动的,也是在Blender里建好单个模型,导出后在Three.js里通过代码控制位置和旋转。这样分工的好处是美术和程序解耦,改模型不用动代码,调逻辑不用重开Blender。
1.2 MCP协议在整个链路里扮演什么角色
MCP这个词最近在AI工具圈很热,全称是Model Context Protocol。放到Blender场景里理解,它本质上是一套让外部程序(比如AI助手或者你写的前端工具)能够"对话"Blender的桥梁。传统做法是你手动在Blender里建模、手动导出、手动拖到项目目录,每一步都要人操作。MCP的思路是把这个过程协议化、自动化。
具体到智慧仓储项目,MCP能做的事情包括:通过协议指令让Blender批量执行建模脚本、自动导出指定格式、甚至根据参数动态调整模型尺寸。比如你有20种不同规格的货架,传统做法是一个个建,用MCP配合Blender的Python API,可以写一套参数化模板,输入长宽高和层数,自动生成对应模型并导出。
这里要说明的是,MCP本身不是Blender的功能,而是外部工具与Blender之间的通信规范。你在实际项目里落地时,可能是通过Blender的Python脚本接口来实现类似效果,MCP提供的是更上层的抽象。理解这一层,后面配置环境时就不会迷糊。
1.3 前端技术栈的选型逻辑
Three.js + Vue 3 + TypeScript这套组合,在数字孪生类项目里已经算是标准答案了。Three.js负责3D渲染,Vue 3负责UI层和状态管理,TypeScript负责类型安全。为什么不用React?不是React不好,而是Vue 3的响应式系统在数字孪生场景里有一个天然优势:仓库里的设备状态、库存数据、报警信息这些频繁变化的值,用Vue的ref和reactive管理起来非常直观,数据一变UI自动更新,不需要手动去操作DOM。
TypeScript在这个项目里不是可选项而是必选项。数字孪生涉及的数据结构很复杂——货架有位置、尺寸、层数、每层库位状态;AGV有当前位置、目标位置、电量、载货状态;输送线有运行状态、速度、堵塞报警。这些结构如果没有类型约束,写到后面自己都记不清某个字段叫什么。用TypeScript定义好interface,编辑器自动补全,改字段名时全局重构,省下来的调试时间远超学习成本。
1.4 整体架构分层
我把整个项目分成四层来理解,这样后续开发和排查问题都有清晰的边界:
| 层级 | 职责 | 关键技术 |
|---|---|---|
| 模型层 | 静态几何体、材质、贴图 | Blender + glTF导出 |
| 渲染层 | 场景搭建、相机控制、光照 | Three.js |
| 数据层 | 设备状态、库存数据、报警信息 | TypeScript接口 + 模拟数据/WebSocket |
| 交互层 | 面板、按钮、数据看板 | Vue 3 + 组件库 |
分层的好处是每层可以独立替换。比如模型层从Blender换成其他建模工具,只要导出glTF,渲染层完全不用改。数据层从模拟数据换成真实接口,交互层也不用动。这种解耦在项目后期需求频繁变更时能救命。
2. Blender建模与导出的核心细节
2.1 仓储场景的建模规范
在Blender里建仓储场景,最容易犯的错误是"建得好看但导不出去"。我踩过的坑包括:模型面数太高导致glTF文件几十兆、材质用了Blender专有节点导致导出后变黑、模型原点不在几何中心导致Three.js里定位偏移。
先说面数控制。一个标准货架,如果每个横梁都用圆柱体加细分,面数轻松上万。实际在数字孪生场景里,货架在屏幕上可能只占几百像素,根本看不出圆柱和方柱的区别。我的做法是:远距离观察的模型用方柱代替圆柱,近距离交互的模型才保留细节。一个四层货架控制在500面以内,整个仓库场景控制在50万面以内,这样在普通办公电脑上也能流畅跑60帧。
材质方面,Blender的Principled BSDF节点导出glTF支持最好。不要用Emission加复杂节点树,不要用Procedural Texture,这些导出后大概率丢失。贴图用PNG或JPG,分辨率控制在1024以内,太大的贴图加载慢且显存占用高。如果确实需要发光效果,在Three.js里用Emissive材质补,比在Blender里折腾节点靠谱。
原点位置这个细节很多人忽略。Blender里新建物体默认原点在世界中心,但货架的原点应该在底部中心,这样在Three.js里设置position时,y坐标就是货架底部离地高度,直观且不容易错。AGV的原点应该在底盘中心,旋转时绕自身中心转。导出前一定要检查每个物体的原点位置,不然后面在代码里调位置会调到崩溃。
2.2 命名规范与层级组织
Blender里的物体命名会直接带到Three.js里,通过scene.getObjectByName()获取。如果命名是"Cube.001"、"Cube.002"这种,代码里根本没法用。我的命名规范是这样的:
- 货架:
Rack_A01、Rack_A02,字母代表区域,数字代表编号 - 库位:
Rack_A01_L1_S1,L代表层,S代表位 - AGV:
AGV_01、AGV_02 - 输送线:
Conveyor_Main、Conveyor_Branch_01 - 地面:
Ground - 墙体:
Wall_North、Wall_South
层级组织上,用Empty物体做分组。所有货架放在一个叫Racks的Empty下面,所有AGV放在AGVs下面。这样在Three.js里可以整组控制显隐、整组做动画。导出时Empty会变成glTF里的Node,Three.js里对应Object3D,层级关系完整保留。
2.3 glTF导出参数详解
Blender导出glTF的界面参数不少,但真正影响项目的就几个:
- 格式:选glTF Binary(.glb),单文件包含所有数据,比.gltf+.bin+.textures的分离格式省心
- 包含:选"选中的物体"或"可见的物体",不要选"所有物体",否则会把灯光相机也导出去
- 变换:Y向上勾选,这样Blender的Z轴向上会转成glTF的Y轴向上,和Three.js坐标系一致
- 几何数据:应用修改器勾选,UV和法线都勾选
- 材质:导出材质勾选,图像格式选自动
- 压缩:Draco压缩可以大幅减小文件体积,但需要Three.js端加载Draco解码器。如果项目对加载速度要求高,建议开启
导出后一定要用glTF查看器(比如gltf-viewer.donmccurdy.com)检查一遍,确认模型完整、材质正确、层级清晰。我遇到过导出后货架少了一层的情况,原因是那层用了Array修改器但没应用,导出时被忽略了。
2.4 参数化建模的实操思路
仓储项目里货架规格往往不统一,A区是重型货架,B区是轻型货架,C区是流利式货架。一个个建效率太低,用Blender的Python API做参数化模板是正解。
核心思路是写一个函数,输入货架的长、宽、高、层数、每层库位数,自动生成对应的几何体。立柱用方柱,横梁用扁长方体,层板用薄长方体。生成后自动命名、自动设置原点、自动分配到对应Empty下。这样改规格只需要改参数,不用重新建模。
这套脚本配合MCP协议,理论上可以实现"前端传参数→Blender自动建模→自动导出→前端加载"的闭环。实际落地时,Blender的Python脚本可以通过命令行执行:blender --background --python script.py,脚本里读取参数、生成模型、导出glb。前端通过接口触发这个命令,等导出完成后加载新模型。这套流程在需要频繁调整布局的场景里非常实用。
3. Three.js场景搭建与核心交互实现
3.1 场景初始化与性能配置
Three.js场景初始化看起来简单,但配置项直接影响后续性能和效果。渲染器方面,antialias: true开启抗锯齿,powerPreference: "high-performance"让浏览器优先用独显,alpha: false关闭透明通道因为仓储场景不需要。像素比用Math.min(window.devicePixelRatio, 2),高分屏上限制在2倍,再高就是浪费性能。
相机用PerspectiveCamera,视场角选45到60度之间。太小了透视感弱像正交投影,太大了边缘畸变严重。近裁剪面0.1,远裁剪面1000,这个范围覆盖整个仓库足够。初始位置放在仓库斜上方,比如(50, 40, 50),看向仓库中心。
光照是很多新手容易忽略的部分。只用环境光会让场景扁平没有立体感,只用平行光会有死黑的阴影面。我的配置是:环境光强度0.6提供基础照明,平行光强度0.8从斜上方打下来产生明暗对比,再加一个半球光补充环境反射。如果场景有室内感需求,可以在顶部加几个点光源模拟顶灯。阴影方面,平行光开阴影,但shadow map尺寸控制在2048以内,太大了性能吃不消。
3.2 模型加载与进度管理
加载glb模型用GLTFLoader,配合DRACOLoader如果导出时开了Draco压缩。加载是异步的,需要处理加载进度和加载完成两个回调。进度条用Vue的响应式变量控制,加载完成后隐藏。
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader' const loader = new GLTFLoader() const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath('/draco/') loader.setDRACOLoader(dracoLoader) loader.load('/models/warehouse.glb', (gltf) => { scene.add(gltf.scene) // 遍历找到需要交互的物体 gltf.scene.traverse((child) => { if (child.name.startsWith('Rack_')) { rackList.push(child) } }) }, (progress) => { loadPercent.value = (progress.loaded / progress.total) * 100 })加载完成后遍历场景树,把需要交互的物体收集到数组里。这里有个细节:glTF导出后物体名称可能被修改,比如Blender里的Rack_A01导出后可能变成Rack_A01_0。加载后先打印一遍所有物体名称,确认实际名称再写匹配逻辑。
3.3 相机控制与视角切换
数字孪生项目里相机控制是高频操作。OrbitControls提供基础的旋转、平移、缩放,但默认参数需要调整。enableDamping开启阻尼让操作更顺滑,dampingFactor设0.05,maxPolarAngle限制在Math.PI/2.2左右防止相机钻到地面以下,minDistance和maxDistance限制缩放范围。
除了自由视角,仓储项目通常需要几个预设视角:全局俯视、A区特写、B区特写、AGV跟随。预设视角的实现是记录目标相机位置和lookAt点,用Tween.js或手动lerp做平滑过渡。过渡时间设1到1.5秒,太快了看不清,太慢了拖沓。
function flyToCamera(targetPos: Vector3, targetLookAt: Vector3) { const startPos = camera.position.clone() const startLookAt = controls.target.clone() const duration = 1200 const startTime = performance.now() function update() { const elapsed = performance.now() - startTime const t = Math.min(elapsed / duration, 1) const ease = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2 camera.position.lerpVectors(startPos, targetPos, ease) controls.target.lerpVectors(startLookAt, targetLookAt, ease) controls.update() if (t < 1) requestAnimationFrame(update) } update() }3.4 射线拾取与物体交互
点击货架显示库存信息,这个功能靠Raycaster实现。核心逻辑是鼠标点击时,把屏幕坐标转成归一化坐标,用Raycaster从相机发射射线,检测与哪些物体相交,取第一个相交的物体。
const raycaster = new Raycaster() const mouse = new Vector2() function onMouseClick(event: MouseEvent) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1 mouse.y = -(event.clientY / window.innerHeight) * 2 + 1 raycaster.setFromCamera(mouse, camera) const intersects = raycaster.intersectObjects(rackList, true) if (intersects.length > 0) { const hitObject = intersects[0].object // 向上查找带Rack_前缀的父物体 let target = hitObject while (target.parent && !target.name.startsWith('Rack_')) { target = target.parent } showRackInfo(target.name) } }这里有个性能优化点:intersectObjects的第二个参数设为true会递归检测子物体,如果货架模型层级很深,检测开销不小。优化做法是只把货架的顶层Object3D放进数组,递归检测时Three.js内部会处理。另外鼠标移动时的hover效果不要每帧都做射线检测,用节流控制在每秒10次左右就够了。
3.5 设备动画与状态驱动
AGV沿路径行驶、堆垛机升降、输送线转动,这些动画分两类:一类是位置旋转的连续变化,一类是状态切换的离散变化。
连续变化用requestAnimationFrame在渲染循环里更新。AGV沿预设路径点移动,每帧计算当前位置到目标点的方向,按速度前进,到达后切换到下一个路径点。路径点用CatmullRomCurve3做平滑曲线,AGV沿曲线移动比直线拐弯自然得多。
离散变化用Vue的响应式数据驱动。设备状态从"空闲"变"运行",对应Three.js里改变材质颜色或播放动画。这里的关键是建立数据到视觉的映射关系:状态值变了,对应的视觉表现自动更新。我通常写一个updateDeviceVisual(deviceId, status)函数,内部根据status切换材质、显隐动画、更新标签文字。
4. Vue 3与Three.js的工程化整合
4.1 组件结构设计
Vue 3和Three.js整合的核心问题是:Three.js的场景、相机、渲染器这些对象放在哪里?我的做法是封装一个useThreeScene组合式函数,内部管理所有Three.js对象,对外暴露初始化、销毁、添加物体、控制相机等方法。Vue组件只负责调用这些方法和管理UI状态。
组件结构上,WarehouseView.vue是3D视图容器,内部有一个canvas元素。ControlPanel.vue是右侧控制面板,包含视角切换按钮、设备列表、数据看板。DeviceDetail.vue是点击设备后弹出的详情面板。AlarmList.vue是报警信息列表。这些组件通过Pinia或provide/inject共享状态。
4.2 响应式数据与3D场景的同步
Vue的响应式数据和Three.js对象是两套系统,需要建立同步机制。我的做法是用watch监听关键数据变化,变化时调用Three.js的更新方法。
// 监听AGV位置变化 watch(() => agvStore.agvList, (newList) => { newList.forEach(agv => { const mesh = scene.getObjectByName(agv.id) if (mesh) { mesh.position.set(agv.x, 0, agv.z) mesh.rotation.y = agv.rotation } }) }, { deep: true })注意deep: true在数据量大时性能开销明显。优化做法是给每个AGV单独建watch,或者用shallowRef配合手动触发更新。如果数据来自WebSocket高频推送,建议在Three.js渲染循环里直接读取最新数据,而不是靠Vue的watch触发。
4.3 内存管理与销毁
单页应用里切换路由时,Three.js场景如果不销毁,内存会持续增长。销毁需要做几件事:停止requestAnimationFrame循环、移除所有事件监听、dispose几何体和材质、dispose渲染器、清空场景。
function disposeScene() { cancelAnimationFrame(animationId) window.removeEventListener('resize', onResize) renderer.domElement.removeEventListener('click', onMouseClick) scene.traverse((child) => { if (child instanceof Mesh) { child.geometry.dispose() if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()) } else { child.material.dispose() } } }) renderer.dispose() scene.clear() }这段代码在组件onUnmounted时调用。我踩过的坑是忘记dispose纹理,纹理占显存很大,不释放的话切换几次路由浏览器就卡了。纹理的dispose在材质的dispose里会连带处理,但如果有单独加载的纹理需要手动dispose。
4.4 性能监控与优化手段
数字孪生项目上线后最常见的反馈是"卡"。性能优化要从几个维度入手:
渲染层面,用renderer.info查看drawcall数量,控制在100以内。超了就用InstancedMesh合并相同几何体的物体,比如100个托盘用InstancedMesh渲染,drawcall从100降到1。材质尽量复用,不要每个物体都新建Material。
数据层面,WebSocket推送频率控制在每秒10次以内,前端做节流合并。如果设备数量多,不要每个设备都触发Vue更新,批量更新减少响应式开销。
代码层面,渲染循环里避免创建新对象,Vector3、Matrix4这些提前创建好复用。requestAnimationFrame的回调里不要做复杂计算,计算逻辑放到Web Worker里。
5. 常见问题排查与避坑经验
5.1 模型加载后位置不对
这是最高频的问题。原因通常有三个:Blender导出时坐标系没转对、Three.js里父级变换影响了子物体、模型原点不在预期位置。
排查步骤:先在Three.js里打印模型的position、rotation、scale,确认数值是否符合预期。如果position是(0,0,0)但模型显示在别处,说明模型原点不在几何中心。如果rotation有非零值,说明导出时坐标系转换有问题。如果scale不是1,检查Blender里是否对物体做了缩放但没应用。
解决方法:Blender里选中物体,Object > Apply > All Transforms应用所有变换,确保scale为1、rotation为0。导出时勾选Y向上。Three.js里加载后如果需要调整,用model.position.set()和model.rotation.set()显式设置。
5.2 材质显示异常
导出后材质变黑、变透明、贴图丢失,原因通常是Blender用了glTF不支持的节点。Principled BSDF是唯一保证兼容的节点。如果用了Mix Shader、Emission加复杂节点树、Procedural Texture,导出后大概率出问题。
排查方法:在Blender里切换到Material Preview模式,确认材质显示正常。导出后用glTF查看器打开,如果查看器里就不对,说明是导出问题。如果查看器里对但Three.js里不对,说明是Three.js的材质配置问题。
Three.js端常见问题是色彩空间。glTF的贴图是sRGB,Three.js的renderer.outputColorSpace要设为SRGBColorSpace,贴图的colorSpace也要设为SRGBColorSpace。不设置的话颜色会偏暗或偏亮。
5.3 交互点击不响应
Raycaster检测不到物体,原因可能是:物体不在raycaster的检测列表里、物体的visible为false、物体被其他透明物体遮挡、鼠标坐标计算错误。
排查步骤:先确认intersectObjects的数组里有目标物体。然后检查物体的visible属性。再检查是否有透明物体挡在前面,比如玻璃墙。最后检查鼠标坐标计算,event.clientX要除以canvas的宽度而不是window的宽度,如果canvas不是全屏的话。
5.4 性能问题速查表
| 现象 | 可能原因 | 排查方法 | 解决手段 |
|---|---|---|---|
| 帧率低于30 | drawcall过多 | renderer.info.render.calls | InstancedMesh合并、材质复用 |
| 加载后卡顿 | 模型面数过高 | 查看glb文件大小 | Blender减面、Draco压缩 |
| 内存持续增长 | 未dispose | 浏览器内存面板 | 组件卸载时dispose几何体材质 |
| 交互延迟 | 射线检测频繁 | 打印检测次数 | 节流、限制检测对象 |
| 贴图模糊 | 纹理尺寸小 | 检查贴图分辨率 | 提高分辨率但不超过2048 |
5.5 实操心得与避坑清单
做了几个仓储数字孪生项目后,我总结了几条文档里不会写的经验:
- 模型面数宁少勿多。甲方不会因为你货架横梁是圆柱体就多付钱,但会因为卡顿扣钱。远距离物体用方柱,近距离交互物体才保留细节。
- 命名规范从第一天就定好。后期改名字比前期定规范痛苦十倍。用
区域_编号_层级的格式,代码里好匹配。 - 原点位置导出前逐个检查。货架原点在底部中心,AGV原点在底盘中心,门原点在底部一侧。原点错了后面调位置调到怀疑人生。
- 材质用最简单的Principled BSDF。不要炫技用复杂节点,导出兼容性第一。
- 加载进度条必须有。模型大的时候加载要好几秒,没有进度条用户以为卡死了。
- 预设视角至少三个。全局、分区、设备特写。用户不会用鼠标慢慢转,给按钮一键切换。
- 报警信息要能定位到3D位置。点击报警列表,相机飞到对应设备,这个功能甲方特别买账。
- 移动端适配要提前考虑。触屏的旋转缩放和鼠标不一样,OrbitControls要开
enablePan和touches配置。
6. 数据层设计与模拟数据生成
6.1 仓储数据模型定义
数字孪生的"孪生"二字,核心在于数据映射。3D场景里的每个物体,都要有对应的数据实体。用TypeScript定义清楚这些接口,是整个项目的地基。
interface Rack { id: string name: string position: { x: number; y: number; z: number } size: { width: number; height: number; depth: number } levels: number slotsPerLevel: number slots: Slot[] } interface Slot { id: string level: number index: number status: 'empty' | 'occupied' | 'locked' | 'error' palletId?: string weight?: number } interface AGV { id: string position: { x: number; z: number } rotation: number status: 'idle' | 'moving' | 'loading' | 'charging' | 'error' battery: number targetNode?: string path: { x: number; z: number }[] }这些接口定义好后,模拟数据生成、接口对接、状态更新都有类型约束,不会出现字段名写错、类型不匹配的低级错误。
6.2 模拟数据生成策略
项目初期没有真实数据接口时,需要生成模拟数据让场景"活"起来。模拟数据的核心要求是:看起来真实、变化有规律、能触发各种状态。
货架库位状态用随机加权重的方式生成。比如70%概率是occupied,20%是empty,8%是locked,2%是error。这样场景里大部分库位有货,少量空位和异常,视觉上真实。
AGV运动模拟用路径点加状态机。每个AGV分配一条路径,沿路径循环移动。到达装卸点时状态切到loading,停留几秒后切回moving。电量随时间递减,低于20%自动去充电桩。这套逻辑用setInterval每500毫秒更新一次数据,Vue的响应式系统自动驱动3D场景更新。
6.3 实时数据接入的预留设计
模拟数据终究要换成真实数据。设计之初就要预留接口层,把数据来源抽象成可替换的模块。
interface DataSource { getRacks(): Promise<Rack[]> getAGVs(): Promise<AGV[]> subscribe(callback: (data: any) => void): void unsubscribe(): void } class MockDataSource implements DataSource { /* 模拟实现 */ } class WebSocketDataSource implements DataSource { /* 真实实现 */ }组件里依赖DataSource接口而不是具体实现,切换数据源只需要换一个实例。WebSocket接入时,收到消息后解析成对应的数据模型,更新到Pinia store,Vue的watch触发Three.js更新。这套流程跑通后,从模拟到真实的切换成本极低。
6.4 数据可视化与3D的联动
数据看板上的数字和3D场景里的物体要能互相定位。点击看板上的"异常库位:12个",3D场景里对应的库位要高亮闪烁。点击3D场景里的货架,看板要显示该货架的详细数据。
实现方式是维护一个id -> Object3D的映射表。数据变化时通过id找到对应的3D物体,更新其视觉表现。3D物体被点击时,通过物体名称反查id,更新看板数据。这个映射表在模型加载完成后建立,后续所有联动都靠它。
高亮效果的实现:保存物体原始材质,高亮时替换成高亮材质,取消高亮时恢复。高亮材质用Emissive发光加透明度变化,配合sin函数做呼吸闪烁效果。多个物体同时高亮时,用数组管理,避免状态混乱。
7. 项目扩展方向与个人体会
7.1 从单仓库到多仓库园区
当前做的是单仓库内部数字孪生,扩展方向是园区级多仓库。技术上需要解决的是场景切换和LOD(Level of Detail)。园区视角下,每个仓库用简化模型表示,点击进入后才加载详细模型。Three.js的LOD对象可以根据相机距离自动切换模型精度,远距离用低模,近距离用高模。
多仓库的数据聚合也是个挑战。每个仓库有独立的设备列表和库存数据,园区层面需要汇总统计。数据层要支持按仓库维度查询和全园区汇总查询两种模式。
7.2 接入真实IoT数据的注意事项
从模拟数据切到真实IoT数据,最大的变化是数据频率和异常处理。真实设备可能每秒推送多次数据,也可能几分钟不推送。前端要做数据缓冲和超时检测,超过一定时间没收到数据要标记设备离线。
数据格式的兼容也是问题。不同厂商的设备数据格式不同,需要在接入层做适配转换,统一成前端定义的数据模型。这个适配层的工作量往往比预期大,要提前预留时间。
7.3 我个人在实际操作中的体会
做数字孪生项目,技术只占一半,另一半是对业务的理解。我见过技术很炫但甲方不买账的项目,也见过技术朴素但甲方很满意的项目。区别在于是否解决了实际问题。
仓储场景里,甲方最关心的三个问题是:我的货在哪、我的设备在干嘛、有没有异常。数字孪生要把这三个问题回答清楚。货的位置通过点击货架显示库位状态,设备状态通过动画和标签实时展示,异常通过高亮和报警列表突出。把这三点做好,项目就成功了一大半。
另外一个小技巧:给甲方演示时,不要一上来就展示全局视角。先点一个货架,弹出库存详情;再点一个AGV,显示它正在执行的任务;最后触发一个模拟报警,相机自动飞到异常位置。这套流程走下来,甲方立刻能理解数字孪生的价值。全局视角留到最后再展示,作为"整体掌控"的收尾。
最后再分享一个性能上的经验:如果项目要在低配电脑上演示,提前准备一个"性能模式"。开启后关闭阴影、降低像素比、用简化模型、减少动画帧率。演示时如果卡顿,一键切换性能模式,流畅度立刻提升。这个功能花不了多少开发时间,但能在关键时刻救场。