超实用!数字孪生 Cesium 园区 3D 模型加载,一次学会的保姆级教程

接前文# 拿来就用!Vue3+Cesium 飞入效果封装,3D大屏多场景直接复用,目前已经实现了Cesium的飞入效果,虽然暂时没有腾出手解决离线地图的问题,但已经可以简单看看效果了。

需求

最终要实现的终极目标其实是针对园区级别的数字孪生大屏系统

其实数字孪生这个概念相对比较宽泛,更精确地说其实是针对目前园区内部的各类设备进行大屏展示。

比如厂区各地方的摄像头厂区内部设备状态人员在岗状态等等

其实更进一步升级可以做到自动化 的一些联动操作。

比如当出现突发的人员闯入,大屏系统自动给出预警,实时追踪异常人员的位置,同步给安保人员。

或者是厂区内某设备异常,自动联动到相应的人员/部门,达到快速处置的目的。

现在咱们先简单的进行第一步,园区的搭建。

解决方案

因为是针对园区的数字孪生大屏系统,所以园区内部的各个模型是一定要分开的。

也就是说各个设备的模型是一个个单独的文件。

考虑到设备可能比较多,所以建议大家在搭建此类园区的时候,优先按 功能区域/独立交互单元 拆分模型,而非单一整体或完全零散的建筑。

建议将园区地形/地面导出为一个模型,这部分包括园区的地面、道路、绿化、围墙等非交互性基础场景。

这样做的好处是保证作为整个园区数字孪生场景的基底 ,一次性加载,保证园区整体视觉完整性,不至于初始化时候看起来非常简陋。

然后将每栋独立建筑(如 1号厂房、2号办公楼、中控楼、配电柜等等)各导出为单独模型

需要注意,每个建筑模型的原点要统一对齐到园区地理坐标系(经纬度),确保加载后位置精准。

最后将园区内需要单独交互的设备(如门禁、充电桩、摄像头、传感器),单独导出为小型模型

实际代码

因为之前的文章内已经对Cesium进行了组件化的简单封装,所以这里不再赘述。详细参考此篇文章:# 拿来就用!Vue3+Cesium 飞入效果封装,3D大屏多场景直接复用

模型加载作为后期比较核心的方法,这里进行简单的抽离和封装。

js 复制代码
/**
 * 加载 glTF/glb 建筑模型到 Cesium 地图
 * @param {Object} viewer Cesium Viewer 实例
 * @param {Object} options 模型配置
 * @param {string} options.id 模型唯一ID(如 'building-01',用于交互识别)
 * @param {string} options.modelUrl glTF/glb 模型路径(如 '/models/building-01.gltf')
 * @param {number} options.lon 经度
 * @param {number} options.lat 纬度
 * @param {number} [options.height=0] 高度(米,默认0,贴地面)
 * @param {number} [options.scale=1.0] 缩放比例
 * @param {number} [options.heading=0] 航向角(度,0=正北,调整建筑朝向)
 * @param {Object} [options.properties={}] 自定义属性(如状态接口、建筑名称,用于交互)
 * @returns {Cesium.Entity} 加载后的模型实体(便于后续管理)
 */
export function loadBuildingModel(viewer, options) {
	if (!viewer || !options.id || !options.modelUrl || !options.lon || !options.lat) {
		console.error('模型加载参数缺失!');
		return null;
	}

	// 经纬度转 Cesium 笛卡尔坐标
	const position = Cesium.Cartesian3.fromDegrees(
		options.lon,
		options.lat,
		options.height || 0
	);

	const heading = Cesium.Math.toRadians(options.heading || 0);
	const orientation = Cesium.Transforms.headingPitchRollQuaternion(
		position,
		new Cesium.HeadingPitchRoll(heading, 0, 0)
	);

	// 加载模型
	const buildingEntity = viewer.entities.add({
		id: options.id,
		name: options.properties.name || options.id,
		position: position,
		orientation: orientation,
		model: {
			uri: options.modelUrl,
			scale: options.scale || 1.0,
			minimumPixelSize: 0,
			maximumScale: 20000,
			runAnimations: false,
			clampToGround: true,
		},
		properties: options.properties || {},
	});
	return buildingEntity;
}

使用上述方法的时候需要注意,buildingEntitymodel.minimumPixelSize 属性我这里设置了 0,表示取消最小像素限制。

目的是为了在缩放地图的时候让模型与地图一起进行缩放,让地图与模型按真实比例进行缩放看起来比较合理。

如果你想要保证缩放时模型不消失,可以设置为 128

需要加载的模型建议存放在 public/models 文件夹下,避免出现打包丢失模型的问题。另外如果你使用的是 GLTF 模型文件,要记得把 .bin 文件也放进来。

将模型文件整理好以后,创建一个数组,包含所有文件的详细信息,以 for 循环的方式调用 loadBuildingModel 方法,实现模型加载。

js 复制代码
const loadParkBuildingModels = () => {
  if (!isCesiumReady.value || !cesiumViewer.value) return;

  // 园区建筑数组
  const parkBuildings = [
    {
      id: 'building-01',
      modelUrl: '/models/building-01.gltf', // glTF 模型路径
      lon: 116.404567, // 经度
      lat: 39.915234, // 纬度
      height: 0,
      scale: 1.0,
      heading: 15, // 建筑朝向
      properties: {
        name: '1号生产厂房'
      }
    },
    {
      id: 'building-02',
      modelUrl: '/models/building-02.gltf',
      lon: 116.405123,
      lat: 39.914987,
      height: 0,
      scale: 1.0,
      heading: 0,
      properties: {
        name: '2号办公楼'
      }
    }
  ];

  // 循环加载每个建筑模型
  parkBuildings.forEach(building => {
    const entity = loadBuildingModel(cesiumViewer.value, building);
    if (entity) {
        // TODO 这里还可以添加loading
      buildingEntities.value.set(building.id, entity);
    }
  });
};

总结

这样基本实现了建筑模型加载在地图上,如果你的模型贴图足够真实,在设置好缩放倍率以后能够非常完美的融合在地图上。

这里我的模型因为做的比较粗糙,效果看起来有些格格不入。

如果有两个园区实现带跨度的位置调整,在加载模型的时候还可以增加距离显隐的方法。

通过对相机到模型距离的计算,控制模型的显隐,这样能够极大的降低同时加载多个模型带来的压力。

js 复制代码
const cameraPosition = viewer.camera.position;
const modelPosition = entity.position.getValue(Cesium.JulianDate.now());
const distance = Cesium.Cartesian3.distance(cameraPosition, modelPosition);

如果单纯一个园区,设备不多的话影响不大,但如果园区较大,隐藏部分模型带来的性能提升还是显而易见的。

相关推荐
游魂Andy1 小时前
零成本搭建专属AI助手:OpenClaw永久免费部署全攻略
前端·人工智能·ai编程
wuhen_n2 小时前
动态组件与 keep-alive:如何优化页面切换体验与性能?
前端·javascript·vue.js
wuhen_n2 小时前
插槽的作用域与分发:如何让组件更灵活、可定制?
前端·javascript·vue.js
IT_陈寒2 小时前
Vite凭什么比Webpack快10倍?5个核心优化原理大揭秘
前端·人工智能·后端
gyx_这个杀手不太冷静2 小时前
OpenCode 进阶使用指南(第三章:MCP 集成)
前端·ai编程
摸鱼的春哥2 小时前
你适合养龙虾🦞吗?4类人不适合2类适合
前端·javascript·后端
Moment3 小时前
Agent 开发本质上就是高级点的 CRUD
前端·后端·面试
恋猫de小郭3 小时前
OpenAI 亲自教你如何构建可靠 AI 代码,从古法编程转向 Agnet 编程,或者 PUA 你的 AI
前端·人工智能·ai编程
程序员爱钓鱼4 小时前
Go错误处理全解析:errors包实战与最佳实践
前端·后端·go