接前文# 拿来就用!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;
}
使用上述方法的时候需要注意,buildingEntity的 model.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);
如果单纯一个园区,设备不多的话影响不大,但如果园区较大,隐藏部分模型带来的性能提升还是显而易见的。