Vue.js - 集成Cesium构建三维可视化场景教程6(添加glTF/GLB模型、模型移动效果)
作者:hangge | 2026-07-09 09:00
在实际业务中,三维模型往往是最直观、信息量也最丰富的表现形式之一,例如设备、车辆、人员的三维呈现与动态展示。本将重点介绍如何在 Vue.js 项目中加载 glTF / GLB 三维模型,并实现模型在三维场景中的移动与朝向变化效果。





(2)这是由于模型坐标系 ≠ Cesium 世界坐标系导致的。在 Blender 里,模型看着“朝前”,但导出 glTF 后,Cesium 认为前方是 X 轴。
六、添加 glTF/GLB 模型、模型移动效果
1,样例代码
下面样例演示了如何在指定地理位置加载 glTF/GLB 格式的三维模型,并利用 SampledPositionProperty 定义时间序列路径,使模型沿预设路线进行平滑移动,同时通过 VelocityOrientationProperty 自动计算模型朝向以始终指向运动方向。
<template>
<div class="layer-container">
<div ref="cesiumContainer" class="cesium-container"></div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<p>加载三维场景中...</p>
</div>
<!-- 工具栏 -->
<div class="toolbar" v-if="!loading">
<button @click="addGlbEntity">添加模型</button>
<button @click="startMove">开始移动</button>
<button @click="stopMove">停止移动</button>
<button @click="removeAllEntities">清除所有实体</button>
</div>
</div>
</template>
<script>
// 引入 Cesium 核心类
import * as Cesium from "cesium";
export default {
name: 'CesiumViewer',
data() {
return {
viewer: null,
loading: true,
modelEntity: null,
pathEntity: null,
positionProperty: null, // SampledPositionProperty
clockStart: null,
moveTimer: null,
moving: false,
sampleTimeSec: 0
}
},
mounted() {
// 确保 Cesium 已加载
if (typeof Cesium === 'undefined') {
console.error('Cesium 未加载,请检查配置')
this.loading = false
return
}
// 初始化 Cesium 场景
this.initCesium()
},
beforeDestroy() {
// 组件销毁时清理资源
this.destroyCesium()
},
methods: {
// 初始化 Cesium
async initCesium() {
try {
// 配置 Cesium 访问令牌(如果需要,从 Cesium Ion 获取)
//Cesium.Ion.defaultAccessToken = '你的Cesium Ion访问令牌'
// 创建 Viewer 实例
this.viewer = new Cesium.Viewer(this.$refs.cesiumContainer, {
animation: false, // 是否显示动画控件
baseLayerPicker: false, // 是否显示图层选择控件
fullscreenButton: false, // 是否显示全屏按钮
geocoder: false, // 是否显示地理编码搜索控件
homeButton: false, // 是否显示Home按钮
infoBox: false, // 是否显示信息框
sceneModePicker: false, // 是否显示3D/2D选择器
selectionIndicator: false, // 是否显示选取指示器组件
timeline: false, // 是否显示时间轴
navigationHelpButton: false, // 是否显示帮助按钮
scene3DOnly: true, // 每个几何实例仅以3D渲染以节省GPU内存
shouldAnimate: true, // 自动播放动画
// 地形配置
terrainProvider: await Cesium.createWorldTerrainAsync()
})
// 强制重新计算画布尺寸
this.viewer.canvas.width = this.viewer.container.clientWidth;
this.viewer.canvas.height = this.viewer.container.clientHeight;
this.viewer.resize(); // 核心方法:告知 Cesium 容器大小已变
// 设置初始视图
this.setInitialView()
// 加载完成
this.loading = false
console.log('Cesium 场景初始化完成')
} catch (error) {
console.error('初始化 Cesium 失败:', error)
this.loading = false
}
},
// 设置初始视图
setInitialView() {
// 设置初始位置(例如:北京天安门)
const initialPosition = Cesium.Cartesian3.fromDegrees(116.3914, 39.9074, 1100)
this.viewer.camera.setView({
destination: initialPosition,
orientation: {
heading: Cesium.Math.toRadians(0),
pitch: Cesium.Math.toRadians(-90),
roll: 0
}
})
},
// 添加glb模型
addGlbEntity() {
this.modelEntity = this.viewer.entities.add({
name: '模型',
position: Cesium.Cartesian3.fromDegrees(116.3894, 39.9047, 0),
model: {
uri: '/models/glb/car_type_0.glb',
minimumPixelSize: 64, // 在缩放时保持最小像素大小
scale: 1.0,
incrementallyLoadTextures: true, // 逐步加载贴图
}
})
},
// 使用 SampledPositionProperty 创建时间序列位置(路径)并让模型沿路径移动
startMove() {
if (!this.modelEntity) {
console.warn('请先加载模型(点击 "加载 3D 模型")。');
return;
}
// 若已存在 positionProperty,清理
if (this.positionProperty) {
this.positionProperty = null;
}
// 创建 SampledPositionProperty,并填充若干时间点(演示用)
const positionProperty = new Cesium.SampledPositionProperty();
// 使用当前时刻作为起点
const start = Cesium.JulianDate.now();
this.clockStart = start;
// 示例路径点(可替换为任意经纬度序列)
const pathCoords = [
[116.3894, 39.9047, 0],
[116.3894, 39.9057, 0],
[116.3899, 39.9060, 0],
[116.3928, 39.9060, 0]
];
// 每个点间隔 6 秒
let seconds = 0;
for (let i = 0; i < pathCoords.length; i++) {
const t = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
const coord = pathCoords[i];
const cart = Cesium.Cartesian3.fromDegrees(coord[0], coord[1], coord[2]);
positionProperty.addSample(t, cart);
seconds += 6;
}
// 将 positionProperty 绑定到实体
this.modelEntity.position = positionProperty;
// 自动计算朝向(沿速度方向)
this.modelEntity.orientation = new Cesium.VelocityOrientationProperty(positionProperty);
// 给实体一个路径可视化(折线)
if (this.pathEntity) {
this.viewer.entities.remove(this.pathEntity);
this.pathEntity = null;
}
// 从 sampled property 取出插值点以构建 polyline positions
// (这里我们取了固定时间步进行可视化)
const polylinePositions = [];
const samplesCount = 60; // 分段数,越大越平滑
for (let i = 0; i <= samplesCount; i++) {
const frac = i / samplesCount;
const secondsOffset = Math.round(seconds * frac);
const t = Cesium.JulianDate.addSeconds(start, secondsOffset,
new Cesium.JulianDate());
const pos = positionProperty.getValue(t);
if (pos) polylinePositions.push(pos);
}
this.pathEntity = this.viewer.entities.add({
polyline: {
positions: polylinePositions,
width: 4,
material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.2,
color: Cesium.Color.CYAN
}),
clampToGround: false
}
});
// 设置时钟范围与速度,便于使用 timeline/animation 控制回放
const stop = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
this.viewer.clock.startTime = start.clone();
this.viewer.clock.currentTime = start.clone();
this.viewer.clock.stopTime = stop.clone();
this.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED; // 到达 stopTime 停止
this.viewer.clock.multiplier = 1; // 真实时间倍率(可调整)
// 开始播放
this.viewer.clock.shouldAnimate = true;
this.moving = true;
this.positionProperty = positionProperty;
},
// 停止模型移动
stopMove() {
if (this.viewer) {
this.viewer.clock.shouldAnimate = false;
this.moving = false;
}
},
// 清空所有实体
removeAllEntities() {
if (!this.viewer) return;
this.viewer.entities.removeAll();
this.modelEntity = null;
this.pathEntity = null;
this.positionProperty = null;
this.moving = false;
// 重置时钟(可选)
this.viewer.clock.shouldAnimate = false;
},
// 销毁 Cesium 资源
destroyCesium() {
if (this.viewer) {
this.viewer.destroy()
this.viewer = null
}
}
}
}
</script>
<style scoped>
.layer-container {
position: relative;
width: 100%;
height: 100%;
}
.cesium-container {
width: 100%;
height: 100%;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
color: white;
}
.loading-spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top: 4px solid #42b983;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.toolbar {
position: absolute;
top: 20px;
left: 20px;
z-index: 999;
display: flex;
gap: 5px;
}
.toolbar button {
padding: 8px 15px;
background: rgba(40, 44, 52, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.toolbar button:hover {
background: rgba(66, 185, 131, 0.8);
}
</style>
2,运行效果
(1)点击工具栏“添加模型”按钮会在预设的经纬度位置处加载并显示一个 glTF/GLB 格式的三维车辆模型,模型按照真实地理坐标放置在场景中,并随视角缩放自动调整显示尺寸。

(2)点击工具栏“开始移动”按钮后,模型开始沿预先定义的路径进行平滑移动,移动过程中位置随时间连续更新,同时模型会根据运动方向自动调整朝向,始终保持“头部”指向前进方向;与此同时,场景中会绘制一条发光折线,用于可视化展示模型的运动轨迹,整体效果类似车辆或设备在地图中的动态行进过程。

(3)点击工具栏“停止移动”按钮,模型的运动动画暂停,模型停留在当前时刻对应的位置,路径依然保留在场景中,便于观察模型当前状态和已行进路线。

(4)点击工具栏“清除所有实体”按钮,移除场景中所有已添加的模型和轨迹实体,三维场景恢复为初始状态,方便重新加载模型并再次进行运动演示。

附:旋转三维模型
1,问题描述
(1)有时我们在 Cesium 中将 glb 模型加载进来可能会发现模型出现偏转 90° / 180° / 倒着 / 侧着等问题。例如,上面样例移动时发现吊车朝向有问题,应该逆时针旋转 90°。

2,解决办法
(1)对于静态模型(即加载进来后不用移动)只需要用 HeadingPitchRoll 明确告诉 Cesium 模型如何旋转即可。例如想要逆时针旋转 90°:
// 添加glb模型
addGlbEntity() {
const position = Cesium.Cartesian3.fromDegrees(116.3894, 39.9047, 0)
const orientation =
Cesium.Transforms.headingPitchRollQuaternion(
position,
new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(90), // 逆时针 90°
0,
0
)
)
this.modelEntity = this.viewer.entities.add({
name: '模型',
position,
orientation,
model: {
uri: '/models/glb/car_type_0.glb',
minimumPixelSize: 64, // 在缩放时保持最小像素大小
scale: 1.0,
incrementallyLoadTextures: true, // 逐步加载贴图
}
})
},
(2)如果想要模型沿路径移动时,在朝向运动方向此基础上逆时针旋转 90°,那么需要替换我们 startMove() 里的 orientation 设置部分。即将如下部分:
// 自动计算朝向(沿速度方向) this.modelEntity.orientation = new Cesium.VelocityOrientationProperty(positionProperty);
- 替换成如下高亮部分内容:
// 使用 SampledPositionProperty 创建时间序列位置(路径)并让模型沿路径移动
startMove() {
if (!this.modelEntity) {
console.warn('请先加载模型(点击 "加载 3D 模型")。');
return;
}
// 若已存在 positionProperty,清理
if (this.positionProperty) {
this.positionProperty = null;
}
// 创建 SampledPositionProperty,并填充若干时间点(演示用)
const positionProperty = new Cesium.SampledPositionProperty();
// 使用当前时刻作为起点
const start = Cesium.JulianDate.now();
this.clockStart = start;
// 示例路径点(可替换为任意经纬度序列)
const pathCoords = [
[116.3894, 39.9047, 0],
[116.3894, 39.9057, 0],
[116.3899, 39.9060, 0],
[116.3928, 39.9060, 0]
];
// 每个点间隔 6 秒
let seconds = 0;
for (let i = 0; i < pathCoords.length; i++) {
const t = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
const coord = pathCoords[i];
const cart = Cesium.Cartesian3.fromDegrees(coord[0], coord[1], coord[2]);
positionProperty.addSample(t, cart);
seconds += 6;
}
// 将 positionProperty 绑定到实体
this.modelEntity.position = positionProperty;
// 1. 速度方向 orientation
const velocityOrientation =
new Cesium.VelocityOrientationProperty(positionProperty)
// 2. 固定旋转偏移(逆时针 90°)
const offsetHPR = new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(90), // 逆时针 90°
0,
0
)
// 3. 用 CallbackProperty 组合两个旋转
this.modelEntity.orientation = new Cesium.CallbackProperty((time) => {
// 当前速度方向的四元数
const baseOrientation = velocityOrientation.getValue(time)
if (!baseOrientation) return baseOrientation
// 偏移旋转四元数
const offsetQuat =
Cesium.Quaternion.fromHeadingPitchRoll(offsetHPR)
// 最终方向 = 速度方向 × 偏移
return Cesium.Quaternion.multiply(
baseOrientation,
offsetQuat,
new Cesium.Quaternion()
)
}, false)
// 给实体一个路径可视化(折线)
if (this.pathEntity) {
this.viewer.entities.remove(this.pathEntity);
this.pathEntity = null;
}
// 从 sampled property 取出插值点以构建 polyline positions
// (这里我们取了固定时间步进行可视化)
const polylinePositions = [];
const samplesCount = 60; // 分段数,越大越平滑
for (let i = 0; i <= samplesCount; i++) {
const frac = i / samplesCount;
const secondsOffset = Math.round(seconds * frac);
const t = Cesium.JulianDate.addSeconds(start, secondsOffset,
new Cesium.JulianDate());
const pos = positionProperty.getValue(t);
if (pos) polylinePositions.push(pos);
}
this.pathEntity = this.viewer.entities.add({
polyline: {
positions: polylinePositions,
width: 4,
material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.2,
color: Cesium.Color.CYAN
}),
clampToGround: false
}
});
// 设置时钟范围与速度,便于使用 timeline/animation 控制回放
const stop = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
this.viewer.clock.startTime = start.clone();
this.viewer.clock.currentTime = start.clone();
this.viewer.clock.stopTime = stop.clone();
this.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED; // 到达 stopTime 停止
this.viewer.clock.multiplier = 1; // 真实时间倍率(可调整)
// 开始播放
this.viewer.clock.shouldAnimate = true;
this.moving = true;
this.positionProperty = positionProperty;
},
全部评论(0)