**摘要:**本文介绍了一个基于 Cesium 的轨迹回放播放器 CesiumTrackPlayer 的实现与应用。该播放器将时间、经纬度和高度数据转换为 SampledPositionProperty 动态属性,通过 Entity 承载模型、图标或点以及轨迹线,并托管 viewer.clock 的时间区间、倍速、循环和相机跟随。文中详细说明了类的设计要点、核心方法(load、play、pause、stop、seek 等)以及模型朝向、静止保持、pitch 限幅等高级特性,并提供了一个完整的 HTML 应用实例,演示了如何初始化 Viewer、准备轨迹数据并加载播放器实现模型沿轨迹运动。
javascript
class CesiumTrackPlayer {
/**
* Cesium 轨迹回放播放器
*
* 职责:
* 1. 把 [{time, lon, lat, height}] 数据转成 SampledPositionProperty(时间→位置采样);
* 2. 用 Entity 承载 模型/图标/点 + path 轨迹线;
* 3. 托管 viewer.clock 的时间区间、倍速、循环、跟随;
* 4. 可选:模型朝向(速度方向)、每帧回调、播放控制。
*
* 设计要点:
* - 位置/朝向都用 Cesium 的 Property(动态属性),由时钟驱动,无需手写 requestAnimationFrame;
* - path 的 leadTime/trailTime 控制"历史/未来"轨迹显示,不传则用 Cesium 默认;
* - 所有可调参数集中在 options,load() 内部一次性消费,便于外部替换数据重载。
*
* @param {Cesium.Viewer} viewer
* @param {Object} options
*/
constructor(viewer, options = {}) {
if (!viewer) throw new Error('viewer is required');
this.viewer = viewer;
this.options = Object.assign({
// ---- 实体标识 ----
id: null, // 不传则自动生成 `track-${Date.now()}`,避免多实例 id 冲突
// ---- 模型(三选一:modelUrl > iconUrl > point)----
modelUrl: null, // glTF/glb 地址;有值时用 model,并自动启用速度朝向
modelScale: 1,
modelMinimumPixelSize: 64, // 模型缩到极小时保底像素,防止远处"消失"
modelMaximumScale: 20000, // 配合 minimumPixelSize 的上限,避免过近时爆炸放大
iconUrl: null, // 无 modelUrl 时用 billboard
iconScale: 1,
pointColor: Cesium.Color.YELLOW,
pointPixelSize: 8,
// ---- 轨迹线 ----
showPath: true,
pathWidth: 3,
pathMaterial: Cesium.Color.CYAN.withAlpha(0.8), // 传 Color 会自动包成 ColorMaterialProperty
pathResolution: 1, // 单位:度。越大顶点越少越"折",越小越贴合曲面但性能下降
// 不传则使用 Cesium 默认行为;传 0 表示不显示历史/未来轨迹
// leadTime:从当前位置往前画多久(未来);trailTime:往后画多久(历史)
pathLeadTime: undefined,
pathTrailTime: undefined,
// ---- 时钟/播放 ----
speed: 1, // clock.multiplier,1 表示 1 秒数据=1 秒真实
loop: true, // true=LOOP_STOP 循环;false=CLAMP 停在终点
shouldAnimate: true, // load 后是否自动播放
follow: false, // true=viewer.trackedEntity 锁定相机;与 flyTo 互斥
flyTo: true, // follow=false 时,load 后飞向实体
// ---- 插值(决定位置曲线的平滑度,也影响朝向求导)----
interpolation: 'Lagrange', // 'Lagrange' | 'Hermite'
interpolationDegree: 5, // 阶数越高越平滑,但采样点少时会产生 Runge 过冲(弧度鼓包)
// ---- 模型朝向(仅 modelUrl 生效)----
// 模型朝向偏移(度)------修正模型自身 +X 不是前方的问题
modelHeadingOffset: 0,
modelPitchOffset: 0,
modelRollOffset: 0,
// 静止判定阈值(m/s),速度低于此值保持上一帧朝向(避免重复点导致朝向乱跳)
orientationStopThreshold: 0.1,
// pitch 限幅(度),防止高度突变时模型俯冲/翻跟头;null 表示不限
orientationMaxPitch: null,
// ---- 时间覆盖(不传则用数据首尾时间)----
startTime: null,
stopTime: null,
// ---- 回调 ----
onTick: null, // (julianTime, entity, self) 每帧
onPlay: null,
onPause: null,
onStop: null,
}, options);
// ---- 运行时状态(load 时创建,destroy 时清空)----
this.entity = null; // 当前轨迹 Entity
this.positionProperty = null; // SampledPositionProperty,供 path/orientation 复用
this.startTime = null; // JulianDate,时钟区间起点
this.stopTime = null; // JulianDate,时钟区间终点
this._tickListener = null; // onTick 的移除函数(Event.addEventListener 的返回值)
this._isPlaying = false; // 与 clock.shouldAnimate 同步,供 isPlaying() 查询
}
/**
加载轨迹数据
@param {Array} data
@returns {Cesium.Entity}
*/
load(data) {
// 先销毁旧实体/监听,保证重复 load 不泄漏(同一实例可反复换数据)
this.destroy();
if (!Array.isArray(data) || data.length === 0) {
throw new Error('轨迹数据不能为空');
}
const opts = this.options;
const viewer = this.viewer;
// 1. 按时间排序
// SampledPositionProperty 要求采样时间单调递增,否则插值/求导会错乱
const sorted = data.slice().sort((a, b) => this._getTimeValue(a) - this._getTimeValue(b));
// 2. 创建位置属性
// 用 SampledPositionProperty 而非 CallbackProperty:Cesium 内部做插值 + 求导,性能好
const positionProperty = new Cesium.SampledPositionProperty();
// 插值算法选择:Lagrange 更平滑但易过冲;Hermite 更稳但需要切线信息(无则退化为近似)
if (opts.interpolation === 'Hermite') {
positionProperty.setInterpolationOptions({
interpolationDegree: opts.interpolationDegree,
interpolationAlgorithm: Cesium.HermitePolynomialApproximation,
});
} else if (opts.interpolation === 'Lagrange') {
positionProperty.setInterpolationOptions({
interpolationDegree: opts.interpolationDegree,
interpolationAlgorithm: Cesium.LagrangePolynomialApproximation,
});
}
let firstTime = null;
let lastTime = null;
// 逐点写入采样:time 支持 ISO/Date/13位时间戳/相对秒数(见 _toJulianDate)
sorted.forEach((item, index) => {
const time = this._toJulianDate(item.time, opts.startTime, index);
const position = this._toCartesian3(item);
positionProperty.addSample(time, position);
if (!firstTime) firstTime = time;
lastTime = time;
});
// 时钟区间:外部可覆盖(startTime/stopTime),否则用数据首尾
this.startTime = opts.startTime
? this._toJulianDate(opts.startTime)
: firstTime;
this.stopTime = opts.stopTime
? this._toJulianDate(opts.stopTime)
: lastTime;
// 3. 创建 Entity
const entityOptions = {
id: opts.id || `track-${Date.now()}`,
// availability 限定实体可见时间窗,窗外不渲染,避免时间轴拖到区间外仍显示
availability: new Cesium.TimeIntervalCollection([
new Cesium.TimeInterval({
start: this.startTime,
stop: this.stopTime,
}),
]),
position: positionProperty,
};
// 模型朝向运动方向(带偏移 / 静止保持 / pitch 限幅)
// 仅 modelUrl 时启用:billboard/point 是二维图标,无"朝向"概念
if (opts.modelUrl) {
entityOptions.orientation = this._createOrientation(positionProperty);
}
// 轨迹线
if (opts.showPath) {
// path.material 需要 Property,传 Color 时自动包一层
let material = opts.pathMaterial;
if (material instanceof Cesium.Color) {
material = new Cesium.ColorMaterialProperty(material);
}
const pathOptions = {
resolution: opts.pathResolution, // 度;越小越贴合曲面
material,
width: opts.pathWidth,
};
// 仅在显式传入时才设置 leadTime/trailTime,避免 undefined 覆盖 Cesium 默认
if (opts.pathLeadTime !== undefined) pathOptions.leadTime = opts.pathLeadTime;
if (opts.pathTrailTime !== undefined) pathOptions.trailTime = opts.pathTrailTime;
entityOptions.path = pathOptions;
}
// 模型 / 图标 / 点:优先级 modelUrl > iconUrl > point
if (opts.modelUrl) {
entityOptions.model = {
uri: opts.modelUrl,
scale: opts.modelScale,
minimumPixelSize: opts.modelMinimumPixelSize, // 远处保底像素
maximumScale: opts.modelMaximumScale, // 近处上限
clampAnimations: true, // 动画循环时不插值到首帧之外
};
} else if (opts.iconUrl) {
entityOptions.billboard = {
image: opts.iconUrl,
scale: opts.iconScale,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 图标底部对准坐标点
disableDepthTestDistance: Number.POSITIVE_INFINITY, // 不被地形遮挡
};
} else {
entityOptions.point = {
pixelSize: opts.pointPixelSize,
color: opts.pointColor,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 1,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
};
}
this.entity = viewer.entities.add(entityOptions);
this.positionProperty = positionProperty;
// 4. 配置时钟
// 注意:clock 是 viewer 全局的,多实例会互相覆盖;单实例场景足够
const clock = viewer.clock;
clock.startTime = this.startTime.clone();
clock.stopTime = this.stopTime.clone();
clock.currentTime = this.startTime.clone();
// LOOP_STOP:到 stopTime 跳回 startTime;CLAMP:停在 stopTime
clock.clockRange = opts.loop
? Cesium.ClockRange.LOOP_STOP
: Cesium.ClockRange.CLAMP;
// SYSTEM_CLOCK_MULTIPLIER:multiplier 表示"真实 1s = 模拟 multiplier 秒"
clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER;
clock.multiplier = opts.speed;
clock.shouldAnimate = opts.shouldAnimate;
// 5. 时间轴范围:让 timeline 只显示本轨迹区间
if (viewer.timeline) {
viewer.timeline.zoomTo(this.startTime, this.stopTime);
}
// 6. 相机:follow 优先级高于 flyTo(trackedEntity 会接管相机,flyTo 会被打断)
if (opts.follow) {
viewer.trackedEntity = this.entity;
} else if (opts.flyTo) {
viewer.flyTo(this.entity);
}
// 7. 每帧回调:保存移除函数,destroy 时调用,避免多次 load 叠加监听
if (opts.onTick) {
this._tickListener = viewer.clock.onTick.addEventListener((clock) => {
opts.onTick(clock.currentTime, this.entity, this);
});
}
this._isPlaying = clock.shouldAnimate;
return this.entity;
}
/** 播放:若已到/超过终点,先回到起点再播,避免"卡在末尾" */
play() {
const clock = this.viewer.clock;
if (Cesium.JulianDate.greaterThanOrEquals(clock.currentTime, this.stopTime)) {
clock.currentTime = this.startTime.clone();
}
clock.shouldAnimate = true;
this._isPlaying = true;
if (this.options.onPlay) this.options.onPlay(this);
}
/** 暂停:只停时钟,位置/实体保留 */
pause() {
this.viewer.clock.shouldAnimate = false;
this._isPlaying = false;
if (this.options.onPause) this.options.onPause(this);
}
/** 停止并回到起点:pause + 重置 currentTime */
stop() {
this.pause();
this.viewer.clock.currentTime = this.startTime.clone();
if (this.options.onStop) this.options.onStop(this);
}
/**
跳转到指定时间
复用 _toJulianDate:支持 ISO/Date/13位时间戳/相对秒数(相对 startTime)
@param {String|Date|Number|Cesium.JulianDate} time
*/
seek(time) {
const julian = this._toJulianDate(time, this.startTime);
this.viewer.clock.currentTime = julian;
}
/** 设置播放倍速:同步 clock 与 options,便于后续查询 */
setSpeed(multiplier) {
this.viewer.clock.multiplier = multiplier;
this.options.speed = multiplier;
}
/** 设置是否相机跟随:undefined 解除 trackedEntity */
setFollow(follow) {
this.viewer.trackedEntity = follow ? this.entity : undefined;
}
/** 销毁:清监听 → 解跟随 → 删实体 → 清字段。可重复调用 */
destroy() {
// 1) 先移除 onTick 监听,否则后续 tick 仍会回调已删除的 entity
if (this._tickListener) {
this._tickListener();
this._tickListener = null;
}
// 2) 若相机正跟随本实体,先解除,避免删除后 trackedEntity 指向空
if (this.viewer.trackedEntity === this.entity) {
this.viewer.trackedEntity = undefined;
}
// 3) 删实体
if (this.entity) {
this.viewer.entities.remove(this.entity);
this.entity = null;
}
// 4) 清引用,便于 GC 与状态判断
this.positionProperty = null;
this.startTime = null;
this.stopTime = null;
this._isPlaying = false;
}
/** 是否正在播放:与 clock.shouldAnimate 同步的本地副本 */
isPlaying() {
return this._isPlaying;
}
// ---------------- 私有方法 ----------------
/**
创建模型朝向 Property
有偏移/限幅/静止保持需求时包装 VelocityOrientationProperty
无任何特殊需求时直接返回原生 VelocityOrientationProperty(零开销)
原理:VelocityOrientationProperty 通过对 positionProperty 做中心差分求导
得到速度方向,再构建 ENU 旋转矩阵(模型 +X 对齐速度),转成四元数。
*/
_createOrientation(positionProperty) {
const opts = this.options;
const hasOffset =
opts.modelHeadingOffset || opts.modelPitchOffset || opts.modelRollOffset;
const hasGuard =
opts.orientationStopThreshold > 0 || opts.orientationMaxPitch != null;
// 快速路径:无任何增强需求时,直接用原生 Property,避免 CallbackProperty 每帧开销
if (!hasOffset && !hasGuard) {
return new Cesium.VelocityOrientationProperty(positionProperty);
}
// 基础朝向:速度方向
const base = new Cesium.VelocityOrientationProperty(positionProperty);
// 模型自身朝向偏移四元数
// 例:模型 +Z 朝前 → modelHeadingOffset = -90 修正
let offsetQuat = null;
if (hasOffset) {
offsetQuat = Cesium.Quaternion.fromHeadingPitchRoll(
new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(opts.modelHeadingOffset || 0),
Cesium.Math.toRadians(opts.modelPitchOffset || 0),
Cesium.Math.toRadians(opts.modelRollOffset || 0)
)
);
}
const stopThreshold = opts.orientationStopThreshold;
const maxPitch = opts.orientationMaxPitch != null
? Cesium.Math.toRadians(opts.orientationMaxPitch)
: null;
// 闭包缓存上一帧朝向:静止/速度为零时 VelocityOrientationProperty 会返回 undefined
// 或跳到默认朝向(东),此处用缓存保持,避免模型"抽搐"
let lastQuat = null;
return new Cesium.CallbackProperty((time) => {
// 1) 取基础朝向
let q = base.getValue(time);
if (!q) return lastQuat || undefined;
// 2) 静止检测:用位置前后差分估速度模长
// 注意 dt 取 0.5s 而非更小,是为了让差分跨越插值噪声区;
// 若数据点稀疏,0.5s 可能跨不过一个采样区间,需按实际采样间隔调整
if (stopThreshold > 0) {
const dt = 0.5;
const t1 = Cesium.JulianDate.addSeconds(time, -dt, new Cesium.JulianDate());
const t2 = Cesium.JulianDate.addSeconds(time, dt, new Cesium.JulianDate());
const p1 = positionProperty.getValue(t1);
const p2 = positionProperty.getValue(t2);
if (p1 && p2) {
const speed = Cesium.Cartesian3.distance(p1, p2) / (2 * dt);
if (speed < stopThreshold && lastQuat) {
return lastQuat; // 保持上一帧朝向
}
}
}
// 3) pitch 限幅
// ⚠️ 注意:_clampPitch 内部用 Cartesian3.ZERO 作参考位置,结果不可靠;
// 如需启用 orientationMaxPitch,请先把当前位置传入 _clampPitch。
if (maxPitch != null) {
q = this._clampPitch(q, maxPitch);
}
// 4) 叠加模型自身朝向偏移
// 顺序很重要:先速度朝向,再叠加模型偏移(右乘),得到"模型前方对准速度"的正确结果
if (offsetQuat) {
q = Cesium.Quaternion.multiply(q, offsetQuat, new Cesium.Quaternion());
}
lastQuat = q;
return q;
}, false); // false = 非恒定,Cesium 每帧求值
}
/**
对四元数施加 pitch 限幅(在局部 ENU 下限制俯仰角)
⚠️ 已知问题:Transforms.headingPitchRollQuaternion 需要"参考位置"来定义 ENU,
这里传 Cartesian3.ZERO 会把 HPR 解释为地心坐标系下的朝向,结果与期望不符。
正确做法是由调用方传入当前位置(positionProperty.getValue(time))。
由于当前调用处未传 position,此方法实际不可靠,建议暂不启用 orientationMaxPitch。
*/
_clampPitch(quat, maxPitch) {
// 转成 heading/pitch/roll
const hpr = Cesium.HeadingPitchRoll.fromQuaternion(quat);
if (Math.abs(hpr.pitch) <= maxPitch) return quat;
const clamped = new Cesium.HeadingPitchRoll(
hpr.heading,
Math.sign(hpr.pitch) * maxPitch,
hpr.roll
);
return Cesium.Transforms.headingPitchRollQuaternion(
Cesium.Cartesian3.ZERO, // FIXME: 应传当前位置
clamped
);
}
/**
取数据项的时间数值(毫秒),仅用于排序
支持:JulianDate / Date / 13位时间戳 / ISO 字符串 / 相对秒数
无法解析时返回 0(排最前),避免排序抛错
*/
_getTimeValue(item) {
const t = item.time ?? item.timestamp ?? item.date;
if (t instanceof Cesium.JulianDate) return Cesium.JulianDate.toDate(t).getTime();
if (t instanceof Date) return t.getTime();
if (typeof t === 'number') return t;
if (typeof t === 'string') {
try {
return Cesium.JulianDate.toDate(Cesium.JulianDate.fromIso8601(t)).getTime();
} catch (e) {
// 兼容 'YYYY-MM-DD HH:mm:ss' 这类非 ISO 格式
return new Date(t.replace(/-/g, '/')).getTime();
}
}
return 0;
}
/**
把各种时间输入转成 JulianDate
优先级/规则:
JulianDate / Date:直接转换
number > 1e12:视为 13 位毫秒时间戳
number <= 1e12:视为"相对秒数",基准为 startTime,缺省为 JulianDate.now()
string:先 ISO8601,失败再 new Date(兼容 'YYYY/MM/DD')
其他:fallback 用索引 index 作为相对秒数
*/
_toJulianDate(time, startTime, index = 0) {
if (time instanceof Cesium.JulianDate) return time.clone();
if (time instanceof Date) return Cesium.JulianDate.fromDate(time);
if (typeof time === 'number') {
// 13 位时间戳
if (time > 1e12) {
return Cesium.JulianDate.fromDate(new Date(time));
}
// 相对秒数
const base = startTime
? this._toJulianDate(startTime)
: Cesium.JulianDate.now();
return Cesium.JulianDate.addSeconds(base, time, new Cesium.JulianDate());
}
if (typeof time === 'string') {
try {
return Cesium.JulianDate.fromIso8601(time);
} catch (e) {
// 非 ISO 格式(如 '2024-01-01 00:00:00')走 Date 解析
const date = new Date(time.replace(/-/g, '/'));
if (!isNaN(date.getTime())) {
return Cesium.JulianDate.fromDate(date);
}
}
}
// fallback:按索引秒数(数据缺 time 字段时,按数组顺序每隔 1 秒一个点)
const base = startTime
? this._toJulianDate(startTime)
: Cesium.JulianDate.now();
return Cesium.JulianDate.addSeconds(base, index, new Cesium.JulianDate());
}
/**
把各种位置输入转成 Cartesian3
支持形式(按优先级):
Cartesian3 实例
item.position:Cartesian3 / [lon,lat,h] / {lon,lat,height}
顶层字段:lon/lng/longitude/x + lat/latitude/y + height/alt/altitude/z
经纬度单位:度(fromDegrees);高度缺省 0
*/
_toCartesian3(item) {
if (item instanceof Cesium.Cartesian3) return item.clone();
if (item.position) {
if (item.position instanceof Cesium.Cartesian3) return item.position.clone();
if (Array.isArray(item.position)) {
return Cesium.Cartesian3.fromDegrees(
item.position[0],
item.position[1],
item.position[2] || 0
);
}
if (typeof item.position === 'object') {
const lon = item.position.lon ?? item.position.lng ?? item.position.longitude;
const lat = item.position.lat ?? item.position.latitude;
const height = item.position.height ?? item.position.alt ?? item.position.altitude ?? 0;
if (typeof lon === 'number' && typeof lat === 'number') {
return Cesium.Cartesian3.fromDegrees(lon, lat, height);
}
}
}
const lon = item.lon ?? item.lng ?? item.longitude ?? item.x;
const lat = item.lat ?? item.latitude ?? item.y;
const height = item.height ?? item.alt ?? item.altitude ?? item.z ?? 0;
if (typeof lon === 'number' && typeof lat === 'number') {
return Cesium.Cartesian3.fromDegrees(lon, lat, height);
}
// 解析失败直接抛错,带上原始数据便于定位
throw new Error('无法解析位置数据: ' + JSON.stringify(item));
}
}
应用实例:
下面是一个完整的 HTML 页面示例,演示如何将上述 CesiumTrackPlayer 类应用到实际项目中。该示例初始化了一个 Cesium Viewer,准备了一段包含时间、经纬度和高度的轨迹数据,然后创建轨迹回放实例并加载数据,实现模型沿轨迹运动、相机跟随、轨迹线绘制等功能。你可以直接复制这段代码,替换为自己的 Cesium Ion Token 和模型地址即可运行。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Cesium 原生建筑层轮廓</title>
<link rel="stylesheet" href="https://unpkg.com/cesium@1.120.0/Build/Cesium/Widgets/widgets.css">
<script src="https://unpkg.com/cesium@1.120.0/Build/Cesium/Cesium.js"></script>
<style type="text/css">
html, body, #containerId { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; }
#btns { position: fixed; top: 10px; left: 10px; z-index: 999; }
#btns button { min-width: 150px; font-size: 12px; padding: 5px 10px; margin-bottom: 10px; display: block; }
</style>
</head>
<body>
<div id="containerId"></div>
<script src="common/CesiumTrackPlayer.js"></script>
<script type="text/javascript">
// 设置你自己的 Cesium ion Access Token
Cesium.Ion.defaultAccessToken = '你自己的Token';
// 1. 初始化 Viewer
const viewer = new Cesium.Viewer('containerId', {
animation: true,
timeline: true,
shouldAnimate: true,
baseLayerPicker: false,
});
// 2. 准备轨迹数据
const trackData = [
// 116.40494839195037 39.91403246373624
{ time: '2024-01-01T00:00:00Z', lon: 116.39701454940013, lat: 39.906934869462084, height: 0 },
{ time: '2024-01-01T00:00:05Z', lon: 116.39751454940013, lat: 39.906934869462084, height: 0 },
{ time: '2024-01-01T00:00:10Z', lon: 116.39867173491211, lat: 39.906934869462084, height: 0 },
{ time: '2024-01-01T00:00:20Z', lon: 116.40052618305599, lat: 39.906934869462084, height: 0 },
{ time: '2024-01-01T00:00:30Z', lon: 116.40052618305599, lat: 39.908444517196365, height: 0 },
{ time: '2024-01-01T00:00:40Z', lon: 116.40052618305599, lat: 39.91070573183383, height: 0 },
{ time: '2024-01-01T00:00:50Z', lon: 116.40063626733688, lat: 39.914128412024446, height: 0 },
{ time: '2024-01-01T00:00:60Z', lon: 116.40146374906904, lat: 39.914128412024446, height: 0 },
{ time: '2024-01-01T00:01:00Z', lon: 116.40360396559713, lat: 39.914128412024446, height: 0 },
{ time: '2024-01-01T00:01:10Z', lon: 116.40494839195037, lat: 39.914128412024446, height: 0 },
{ time: '2024-01-01T00:01:20Z', lon: 116.40494839195037, lat: 39.9117550671162, height: 0 },
//
];
// 3. 创建轨迹回放实例
const player = new CesiumTrackPlayer(viewer, {
modelUrl: 'assets/glb/dog.glb', // dog模型地址
// 模型 +Z 朝前 → 需要 -90° heading 修正
modelHeadingOffset: 40,
// 静止时保持朝向
orientationStopThreshold: 0.1,
// 限制俯仰 ±30°,防止降落时翻跟头
// orientationMaxPitch: 30,
modelScale: 1,
showPath: true,
pathWidth: 5,
pathMaterial: Cesium.Color.YELLOW,
speed: 5,
loop: true,
follow: true, // 相机跟随
flyTo: true, // 加载后飞向轨迹
onTick: (time, entity, self) =&gt; {
// 每帧回调,可更新 UI 显示当前时间
// console.log('当前时间:', Cesium.JulianDate.toDate(time));
},
onPlay: (self) =&gt; console.log('开始播放'),
onPause: (self) =&gt; console.log('暂停'),
onStop: (self) =&gt; console.log('停止'),
});
// 4. 加载并播放
player.load(trackData);
// player.play(); // 默认 shouldAnimate: true,会自动播放
// player.pause();
// player.stop();
// player.seek('2024-01-01T00:00:15Z');
// player.setSpeed(20);
// player.setFollow(false);
// player.destroy();
// 视角定位到所有建筑
if (viewer.entities.values.length &gt; 0) {
viewer.zoomTo(viewer.entities);
}
&lt;/script&gt;
</body>
</html>
效果图:
