新网站怎样做推广,彭阳网站建设多少钱,南山网站建设哪家效益快,百度大全一、开篇衔接
大家好#xff01;第五篇我们实现了空间分析功能#xff0c;让三维场景具备了 “决策支持” 能力。而在数字孪生、工业可视化等核心场景中#xff0c;“三维模型” 是核心载体 —— 比如工厂的设备模型、小区的建筑模型、城市的管网模型。仅仅加载模型远远不…一、开篇衔接
大家好第五篇我们实现了空间分析功能让三维场景具备了 “决策支持” 能力。而在数字孪生、工业可视化等核心场景中“三维模型” 是核心载体 —— 比如工厂的设备模型、小区的建筑模型、城市的管网模型。仅仅加载模型远远不够我们还需要与模型 “对话”点击水泵模型看实时压力数据、电机故障时模型自动变红、点击电梯按钮播放升降动画。
这些 “模型交互” 是区别于 “静态展示” 的关键也是 Cesium 在工业场景落地的核心能力。本篇将基于真实工业场景从 “模型交互原理” 到 “实战代码”逐步实现三大高频交互功能并解决新手最易踩的 “模型交互坑”所有案例均使用开源 glTF 模型附获取渠道确保你能跟着复现。
二、模型交互核心原理先懂底层逻辑
在动手前需先理解 Cesium 中模型交互的底层逻辑 —— 一切交互都围绕glTF 模型的结构和射线检测展开。
1. glTF 模型的核心结构
Cesium 支持的 glTF.gltf/.glb模型包含三个关键部分这是交互的基础 结构 作用 交互关联场景 节点Node 模型的 “骨骼”每个节点对应模型的一个部件如设备的 “电机”“阀门”“底座” 点击指定部件如只点击阀门不响应底座 材质Material 模型的 “皮肤”定义部件的颜色、纹理、光泽如金属质感、塑料质感 故障时修改颜色电机从银色变红色 动画Animation 模型的 “动作”定义节点的运动轨迹如阀门旋转、电梯升降 播放 / 暂停动画开启阀门、电梯上行
2. Cesium 模型交互的核心逻辑
Cesium 通过 **“射线检测 模型结构解析”** 实现交互
射线检测当鼠标点击时生成从相机到点击位置的 “射线”判断射线是否与模型相交获取交互对象若相交获取相交的模型实例Model、节点ModelNode、材质ModelMaterial执行交互逻辑根据需求触发操作如显示节点属性、修改材质、播放动画。
三、实战准备模型与环境
1. 测试模型获取附开源渠道
为确保实战可复现推荐使用以下开源 glTF 模型工业设备 / 建筑类带节点和动画
工业设备模型Google Poly搜索 “pump”“valve”筛选 glTF 格式
建筑动画模型Sketchfab搜索 “animated building elevator”选择免费可商用模型
本地模型处理若模型无节点 / 动画可用Blender免费 3D 工具拆分节点、制作简单动画如旋转阀门导出时选择 “glTF 2.0” 格式。
2. 模型放置路径
将下载的 glTF 模型如industrial-pump.glb放在项目public/models目录下确保路径正确如/models/industrial-pump.glb。
四、实战场景从需求到落地
以下场景均在CesiumViewer.vue中实现需先引入新增的 Cesium 类
// 在script setup顶部添加
import {Model,ModelAnimationClip,ModelNode,ModelMaterial,Ray,ScreenSpaceEventType,Cartesian3,Color,BillboardGraphics,LabelGraphics
} from cesium
场景 1模型点击交互 —— 查询部件属性
需求说明
“加载一台工业水泵模型点击模型的不同部件如‘电机’‘进水阀’‘出水阀’弹出该部件的实时运行参数如电机转速、阀门开关状态”要求只响应指定部件忽略无关节点如底座。
代码实现
// 1. 加载带节点的水泵模型
const loadPumpModel () {// 模型配置关键开启节点拾取否则无法获取点击的部件const pumpModel viewer.scene.primitives.add(Model.fromGltf({url: /models/industrial-pump.glb, // 模型路径modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(116.404, 39.915, 10) // 模型位置北京高度10米),scale: 20, // 缩放比例根据模型实际大小调整allowPicking: true, // 允许拾取必须开启否则无法点击部件debugShowBoundingVolume: false // 关闭包围盒显示调试时可开启}));// 2. 监听模型加载完成确保节点已初始化pumpModel.readyPromise.then((model) {console.log(水泵模型加载完成节点列表, model.nodeNames); // 打印所有节点名称如Motor InletValve// 3. 创建“属性弹窗”Entity初始隐藏const infoEntity viewer.entities.add({name: 部件属性弹窗,billboard: new BillboardGraphics({image: /images/info-panel.png, // 弹窗背景图public目录下width: 200,height: 120,show: false}),label: new LabelGraphics({text: ,font: 12px sans-serif,fillColor: Color.BLACK,pixelOffset: new Cartesian2(0, -40), // 文本在弹窗上方show: false})});// 4. 监听鼠标左键点击事件const handler new ScreenSpaceEventHandler(viewer.scene.canvas);handler.setInputAction((event) {// 生成射线从相机到点击位置const ray viewer.camera.getPickRay(event.position);if (!ray) return;// 射线检测获取与模型相交的结果const pickResult viewer.scene.pickFromRay(ray);if (!pickResult || !pickResult.node) return; // 未点击到模型节点直接返回// 获取点击的模型节点信息const clickedNode pickResult.node; // 点击的节点ModelNode实例const nodeName clickedNode.name; // 节点名称如Motorconst modelInstance pickResult.primitive; // 点击的模型实例// 5. 模拟不同部件的实时数据真实项目从接口获取const partData {Motor: { speed: 2800 RPM, temperature: 45°C, status: 正常 },InletValve: { openRatio: 100%, pressure: 0.8 MPa, status: 开启 },OutletValve: { openRatio: 80%, pressure: 0.6 MPa, status: 开启 },Base: { status: 无数据 } // 底座无数据不显示弹窗};// 6. 过滤无数据节点显示弹窗if (partData[nodeName] partData[nodeName].status ! 无数据) {const data partData[nodeName];// 组装弹窗文本const infoText ${nodeName}\n转速${data.speed || -}\n压力${data.pressure || -}\n状态${data.status};// 设置弹窗位置在点击节点上方10米处const nodePosition new Cartesian3();clickedNode.computeWorldMatrix(modelInstance.modelMatrix, new Cartesian3());Cartesian3.multiplyByTranslation(clickedNode.worldMatrix,new Cartesian3(0, 0, 10), // 向上偏移10米nodePosition);// 更新弹窗EntityinfoEntity.position nodePosition;infoEntity.billboard.show true;infoEntity.label.text infoText;infoEntity.label.show true;} else {// 点击无数据节点隐藏弹窗infoEntity.billboard.show false;infoEntity.label.show false;}}, ScreenSpaceEventType.LEFT_CLICK);// 7. 监听鼠标右键隐藏弹窗handler.setInputAction(() {infoEntity.billboard.show false;infoEntity.label.show false;}, ScreenSpaceEventType.RIGHT_CLICK);return handler; // 返回事件处理器方便销毁});return pumpModel;
};// 在onMounted中调用
onMounted(() {// ...之前的初始化代码地形、Viewer等const pumpModel loadPumpModel();// 组件卸载时销毁模型和事件onUnmounted(() {if (pumpModel pumpModel.readyPromise) {pumpModel.readyPromise.then(handler {handler.destroy(); // 销毁事件处理器});}viewer.scene.primitives.remove(pumpModel); // 移除模型});
});
关键说明
开启节点拾取allowPicking: true是点击部件的前提否则pickFromRay无法获取节点节点名称获取模型加载完成后通过model.nodeNames打印所有节点名称需与业务数据的键匹配如 “Motor” 对应电机数据弹窗位置计算通过clickedNode.computeWorldMatrix获取节点的世界坐标向上偏移避免遮挡模型。
场景 2材质动态修改 —— 故障状态可视化
需求说明
“当水泵电机温度超过 50°C 时电机部件从‘银色’变为‘红色’故障解除后恢复原色”支持手动触发故障模拟用于测试。
代码实现
// 在loadPumpModel的readyPromise中扩展接场景1的代码
pumpModel.readyPromise.then((model) {// ...场景1的点击逻辑代码// 8. 动态修改材质的核心函数const updateNodeMaterial (nodeName, targetColor) {// 1. 获取目标节点const targetNode model.getNode(nodeName);if (!targetNode) {console.error(未找到节点${nodeName});return;}// 2. 获取节点的材质假设每个节点只有一个材质const material model.getMaterial(targetNode.materialIds[0]);if (!material) {console.error(节点${nodeName}无材质);return;}// 3. 修改材质颜色glTF材质的baseColorFactor属性material.setValue(baseColorFactor, targetColor);// 4. 强制模型重新渲染model.requestRender();};// 9. 模拟故障触发温度超过50°Cwindow.triggerMotorFault () {console.log(电机温度超过50°C触发故障);updateNodeMaterial(Motor, Color.RED.withAlpha(1.0)); // 电机变红// 同时更新点击弹窗的状态真实项目从接口同步partData[Motor].status 故障;partData[Motor].temperature 58°C;};// 10. 模拟故障解除window.resetMotorStatus () {console.log(电机温度恢复正常故障解除);updateNodeMaterial(Motor, Color.fromCssColorString(#c0c0c0)); // 恢复银色partData[Motor].status 正常;partData[Motor].temperature 42°C;};console.log(故障测试调用 triggerMotorFault() 触发故障resetMotorStatus() 恢复正常);return handler;
});
操作与原理
故障触发在浏览器控制台调用triggerMotorFault()电机节点会立即变红点击电机弹窗显示 “故障” 状态材质修改原理glTF 模型的颜色由baseColorFactor基础颜色因子控制通过material.setValue修改该属性再调用model.requestRender()强制渲染注意事项若模型使用纹理非纯色材质需先移除纹理或修改baseColorTexture属性具体需看模型材质结构可通过console.log(material)查看属性。
场景 3模型动画控制 —— 播放 / 暂停 / 调速
需求说明
“加载带动画的电梯模型包含‘电梯上行’‘电梯下行’‘门打开’‘门关闭’4 个动画添加控制按钮实现动画的播放、暂停、速度调节且动画播放时更新电梯位置标签”。
代码实现
!-- 在template中添加动画控制按钮 --
div classanimation-controlsbutton clickplayElevatorAnimation(up)电梯上行/buttonbutton clickplayElevatorAnimation(down)电梯下行/buttonbutton clickplayElevatorAnimation(openDoor)开门/buttonbutton clickplayElevatorAnimation(closeDoor)关门/buttonbutton clickpauseElevatorAnimation暂停/buttoninput typerange min0.5 max3 step0.5 v-modelanimationSpeed changeadjustAnimationSpeedplaceholder动画速度span{{ animationSpeed }}x/span
/divscript setup
// 动画控制相关响应式数据
const animationSpeed ref(1.0); // 动画速度0.5x~3x
let elevatorModel null; // 电梯模型实例
let currentAnimation null; // 当前播放的动画// 1. 加载带动画的电梯模型
const loadElevatorModel () {elevatorModel viewer.scene.primitives.add(Model.fromGltf({url: /models/animated-elevator.glb, // 带动画的电梯模型modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(116.414, 39.915, 0) // 模型位置北京贴地),scale: 5,allowPicking: true}));// 2. 监听模型加载完成获取动画列表elevatorModel.readyPromise.then((model) {console.log(电梯模型加载完成动画列表, model.activeAnimations._clips.map(c c.name));// 3. 创建电梯位置标签显示当前楼层const floorLabel viewer.entities.add({name: 电梯楼层标签,position: Cartesian3.fromDegrees(116.414, 39.915, 5), // 标签在电梯上方5米label: new LabelGraphics({text: 当前楼层1,font: 16px sans-serif,fillColor: Color.WHITE,backgroundColor: Color.BLACK.withAlpha(0.7),showBackground: true,pixelOffset: new Cartesian2(0, -20)})});// 4. 监听动画播放进度更新楼层标签model.activeAnimations.progressUpdated.addEventListener((animation) {if (animation.name up) {// 上行动画进度0→1对应楼层1→10const floor Math.ceil(1 animation.progress * 9);floorLabel.label.text 当前楼层${floor};} else if (animation.name down) {// 下行动画进度0→1对应楼层10→1const floor Math.ceil(10 - animation.progress * 9);floorLabel.label.text 当前楼层${floor};}});return model;});return elevatorModel;
};// 5. 动画播放函数
const playElevatorAnimation (animationName) {if (!elevatorModel || !elevatorModel.ready) return;const model elevatorModel;// 先暂停当前动画if (currentAnimation) {model.activeAnimations.pause(currentAnimation);}// 查找目标动画const targetAnimation model.activeAnimations._clips.find(clip clip.name animationName);if (!targetAnimation) {alert(未找到动画${animationName});return;}// 播放动画循环1次currentAnimation model.activeAnimations.add({clip: targetAnimation,loop: ModelAnimationLoop.NONE, // 不循环speedup: animationSpeed.value, // 播放速度startOffset: 0, // 从开头播放stopOffset: targetAnimation.duration // 播放完整时长});
};// 6. 动画暂停函数
const pauseElevatorAnimation () {if (currentAnimation elevatorModel.ready) {elevatorModel.activeAnimations.pause(currentAnimation);}
};// 7. 动画速度调节函数
const adjustAnimationSpeed () {if (currentAnimation elevatorModel.ready) {currentAnimation.speedup animationSpeed.value;}
};// 在onMounted中调用注释掉水泵模型单独测试电梯
onMounted(() {// ...之前的初始化代码// const pumpModel loadPumpModel();elevatorModel loadElevatorModel(); // 加载电梯模型onUnmounted(() {viewer.scene.primitives.remove(elevatorModel); // 移除电梯模型});
});
/scriptstyle scoped
/* 动画控制按钮样式固定在页面下方 */
.animation-controls {position: absolute;bottom: 20px;left: 50%;transform: translateX(-50%);z-index: 100;display: flex;gap: 10px;align-items: center;padding: 10px;background: rgba(0, 0, 0, 0.5);border-radius: 8px;
}.animation-controls button {padding: 6px 12px;background: white;border: none;border-radius: 4px;cursor: pointer;
}.animation-controls input {width: 100px;
}.animation-controls span {color: white;font-size: 14px;
}
/style
关键说明
动画列表获取模型加载后通过model.activeAnimations._clips获取所有动画需确保模型导出时包含动画动画循环控制loop: ModelAnimationLoop.NONE表示播放 1 次ModelAnimationLoop.REPEAT表示循环播放进度监听通过progressUpdated事件获取动画进度0→1实现 “进度→楼层” 的映射更新标签。
五、常见问题与解决方案真实开发踩坑
1. 问题 1点击模型无响应无法获取节点
原因 1未开启allowPicking: true模型默认不允许拾取
原因 2模型节点未正确导出如 Blender 导出时未勾选 “导出节点”
原因 3射线检测范围错误点击位置不在模型包围盒内
解决方案 确认Model.fromGltf中allowPicking: true用 Blender 重新导出模型勾选 “Include Nodes”调试时开启debugShowBoundingVolume: true查看模型包围盒确保点击在包围盒内。
2. 问题 2材质修改不生效
原因 1模型材质属性名称错误非baseColorFactor如diffuseColor
原因 2材质使用纹理baseColorTexture纯色修改被纹理覆盖
原因 3未调用model.requestRender()强制渲染
解决方案 通过console.log(material)查看材质属性替换正确的属性名若有纹理先移除纹理material.setValue(baseColorTexture, undefined)修改材质后必须调用model.requestRender()。
3. 问题 3动画播放卡顿或不流畅
原因 1模型动画帧数过高如每秒 60 帧渲染压力大
原因 2同时播放多个动画CPU/GPU 负载过高
原因 3动画速度过快speedup超过 3
解决方案 用 Blender 简化动画降低帧数如每秒 24 帧避免同时播放多个动画播放新动画前暂停旧动画限制speedup最大值为 3避免过度加速。
六、总结
本篇我们聚焦三维模型的 “交互能力”实现了真实工业场景的核心需求
模型点击查询精准定位部件显示实时运行参数材质动态修改故障状态可视化直观展示设备异常动画控制播放 / 暂停 / 调速模拟设备动作电梯、阀门。