CesiumJS 源码杂谈 - 时间与时钟系统
[TOC]你知道吗?-Cesium是元素**铯**的英文单词,而**铯原子钟**具有世界上最高的计时精度-时间,是时刻间
2023-05-21你知道吗?
【资料图】
Cesium 是元素 铯的英文单词,而 铯原子钟具有世界上最高的计时精度
时间,是时刻间隔的意思,时刻是静态的点;而时间就指有起止时刻的一段范围
很多应用都要有一个时钟,例如 GPS 授时、实时渲染系统,时间可以测量很多事物,万物运动也体现了时间在流逝
首次创建时间是出现在 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字段上。
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方法,继续更新一帧。
在之前写源码系列的时候,就提过 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 了。
这个最好的说明就是 JulianDate.now了,在上面第 1 节已经列出源码。当然,也可以自己来搞一个:
const myDate = JulianDate.fromDate(new Date())以北京时间为例:
const myDate = JulianDate.fromIso8601("2023-05-01T13:15:21+08:00")注意日期和时间之间有一个大写字母 T。我在尾部加上了 +08:00表示东八区北京时间。
这个就很简单了:
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才有效。
clock.clockRange = ClockRange.LOOP_STOPLOOP_STOP是默认的,到终点不会停止,会继续往前走,但是会重新回到起点时刻,类似于 重播效果。
CLAMPED会在终点时刻停下来,类似于 播完就停在那里。
UNBOUNDED即使超过终点时刻,也不会停下来,类似 直播效果。