CesiumJS 源码杂谈 - 时间与时钟系统
你知道吗?
-
Cesium 是元素 铯 的英文单词,而 铯原子钟 具有世界上最高的计时精度
-
时间,是时刻间隔的意思,时刻是静态的点;而时间就指有起止时刻的一段范围
-
很多应用都要有一个时钟,例如 GPS 授时、实时渲染系统,时间可以测量很多事物,万物运动也体现了时间在流逝
1. 时间的“诞生”
首次创建时间是出现在 Scene 的构造函数中:
function Scene (/**/) {
// ...
updateFrameNumber(this, 0.0, JulianDate.now());
// ...
}
function updateFrameNumber(scene, frameNumber, time) {
const frameState = scene._frameState;
frameState.frameNumber = frameNumber;
frameState.time = JulianDate.clone(time, frameState.time);
}
源于此,很多自己应用 CesiumJS 着色器的文章中就用 FrameState
上的 frameNumber
就近似表达了“时间”的概念,因为在 60FPS 的屏幕上,可以通过 frameNumber / 60
粗略获得时间值(秒),但是一旦浏览器的帧速率变化,比如 144 FPS,这个获得的时间就会不准确。
CesiumJS 使用 JulianDate
类来表示整个程序中的时间,它是一种天文时间系统,叫作“儒略”日期,它有两个成员字段,一个是自儒略第一天(公元前 4713 年 1 月 1 日)到现在的天数 dayNumber
,另一个是今天已经走过的秒数(零点起算)secondsOfDay
。
注:我们所说的公历时间,即 GregorianDate(格里日历记法),在 CesiumJS 中也是有的,是作为 JS 原生类 Date 的高精度替代品。
根据上面的 Scene 类构造函数,使用 JulianDate.now
方法,无论什么时候初始化 CesiumJS,获取的时间值永远都是程序运行的那个时刻:
JulianDate.now = function (result) {
return JulianDate.fromDate(new Date(), result);
}
所以,真正的时间值在帧状态对象 scene._frameState
的 time
字段上。
2. 时间的推进
CesiumJS 内部的时间是如何更新的?
CesiumJS 的渲染源头是 CesiumWidget
对象,它每一帧都会运行 CesiumWidget.prototype.render
方法,会让此对象上的时钟 tick
一次(也就是跳一下),返回的时间就作为这一帧的时间,传递给 Scene.prototype.render
,进而调用 updateFrameNumber
函数更新累计帧数、时间值:
CesiumWidget.prototype.render = function () {
if (this._canRender) {
this._scene.initializeFrame();
const currentTime = this._clock.tick();
this._scene.render(currentTime);
} else {
this._clock.tick();
}
}
所以要看时间是如何更新的,就要看 Clock
对象的 tick
方法。
初始化 Clock 时,默认就以当前的 JulianDate 为时钟起点时刻,往后一天为终点时刻。
每当调用 tick
时,会获取当前的时刻 clock.currentTime
,然后调用 JulianDate.addSeconds()
方法把时间往前推。 在所有默认条件下,调用的逻辑分支是:
const milliseconds = currentSystemTime - this._lastSystemTime;
currentTime = JulianDate.addSeconds(
currentTime,
multiplier * (milliseconds / 1000.0),
currentTime
);
而这个 currentSystemTime
即时间戳,来自 Performance API
(浏览器高精度性能 API)或 Date API
,能获取当前的毫秒数。
最后,把计算的 currentTime
(类型是 JulianDate
)返回给调用者,也就是 CesiumWidget.prototype.render
方法,继续更新一帧。
3. Entity API 与 Property API 的更新动力源
在之前写源码系列的时候,就提过 Entity API 是怎么运作的。
首先,EntityAPI 挂载于 Viewer
上,若无 Viewer
那默认的 Entity 容器就得自己实现一套,很麻烦。
其次,Viewer
拥有 _onTick
事件,它监听了 CesiumWidget
的 clock
的 onTick
事件,通过 EventHelper
完成:
eventHelper.add(clock.onTick, Viewer.prototype._onTick, this);
往后就是 DataSourceDisplay、CustomDataSource 等内容了,较为复杂,请移步源码解析文章。
引自源码解析文章,以参数化几何的 Entity
为例,它用的是 GeometryVisualizer
,当 GeometryVisualizer
调用 fireChangedEvent
函数后,Visualizer 就会拿到最新的 Entity 定义,进而借助 Property API、Updater 等复杂架构更新数据。
总之,若无时钟的 onTick
跳动,也就没有办法根据当前时间去更新 Entity,也就拿不到最新的 Property,更别说动态更新场景中的三维 Entity 了。
4. 简单应用
4.1. 使用原生 JS Date 对象创建 JulianDate
这个最好的说明就是 JulianDate.now
了,在上面第 1 节已经列出源码。当然,也可以自己来搞一个:
const myDate = JulianDate.fromDate(new Date())
4.2. 使用时间字符串(ISO8601标准的时间字符串或 UTC 时间字符串)创建 JulianDate
以北京时间为例:
const myDate = JulianDate.fromIso8601('2023-05-01T13:15:21+08:00')
注意日期和时间之间有一个大写字母 T
。我在尾部加上了 +08:00
表示东八区北京时间。
4.3. 为时钟设置起止时间和速率
这个就很简单了:
clock.startTime = JulianDate.fromIso8601('2023-05-01T00:00:00+08:00')
clock.stopTime = JulianDate.fromDate(new Date('2023/05/02 00:00:00')) // Date 会默认使用当前时区,当然你也可以手动 +8,格式按 Date 的文档来就可以
clock.multiplier = 3600 // 3600倍速,一秒过一小时
注意,设置倍数要配合参数 clock.clockStep === ClockStep.SYSTEM_CLOCK_MULTIPLIER
或 ClockStep.TICK_DEPENDENT
才有效。
4.4. 调整时钟的循环情况
clock.clockRange = ClockRange.LOOP_STOP
LOOP_STOP
是默认的,到终点不会停止,会继续往前走,但是会重新回到起点时刻,类似于 重播效果。
CLAMPED
会在终点时刻停下来,类似于 播完就停在那里。
UNBOUNDED
即使超过终点时刻,也不会停下来,类似 直播效果。