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._frameStatetime 字段上。

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 事件,它监听了 CesiumWidgetclockonTick 事件,通过 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_MULTIPLIERClockStep.TICK_DEPENDENT 才有效。

4.4. 调整时钟的循环情况

clock.clockRange = ClockRange.LOOP_STOP

LOOP_STOP 是默认的,到终点不会停止,会继续往前走,但是会重新回到起点时刻,类似于 重播效果

CLAMPED 会在终点时刻停下来,类似于 播完就停在那里

UNBOUNDED 即使超过终点时刻,也不会停下来,类似 直播效果


空间的一粟
一介草民

一介草民

67 声望
33 粉丝
0 条评论
推荐阅读
和我一起学 Three.js【初级篇】:1. 搭建 3D 场景
本系列第 6,7,8 章节支持在我的个人公众号「前端乱步」内付费观看,将在全平台文章「点赞数」+「评论数」 >= 500(第 6 章), 1000(第 7,8 章) 时分别解锁发布。

libinfs7阅读 2.1k评论 2

封面图
three.js简明教程之:【1】入门用例
和直接使用WebGL相比,比如 着色器 ,大部分情况下你无需自己开发,不过,情况并不总是这样,如果你的需求太过特殊,我们依旧可以用更接近原生的方式来绘制,这是一个非常友好的设计。

zxl200707012阅读 796

封面图
和我一起学 Three.js【初级篇】:3. 掌握摄影机
本系列第 6,7,8 章节支持在我的个人公众号「前端乱步」内付费观看,将在全平台文章「点赞数」+「评论数」 >= 500(第 6 章), 1000(第 7,8 章) 时分别解锁发布。

libinfs2阅读 3k

封面图
WebGL简明教程之:【2】使用缓冲区绘制三角形
纯色三角形我们首先借助缓冲区绘制一个三角形:完整的代码如下: {代码...} 接着,我们将来解读上面这段代码。着色器三角形一共有3个点,因此不能再写死了,需要定义变量使用缓冲区进行传递,顶点着色器修改如下...

zxl200707011阅读 1k

和我一起学 Three.js【初级篇】:4. 掌握纹理
感谢您一路跟随我来到这里!截止目前为止,我们应该有能力搭建一个 3D 场景,在其中添加各种官方提供的几何体,并通过使用控制器,调整摄影机位置与几何体交互。这一切看起来都还不错,但未免有些单调。所幸本章...

libinfs2阅读 633

封面图
Python3 格式化时间
ISO 8601是国际标准化组织(ISO)制定的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前,ISO 8601是全球通用的日期和时间格式标准,它定义了日期和时间的表示方法,包...

qbit1阅读 1.7k

和我一起学 Three.js【初级篇】:2. 掌握几何体
本系列第 6,7,8 章节支持在我的个人公众号「前端乱步」内付费观看,将在全平台文章「点赞数」+「评论数」 >= 500(第 6 章), 1000(第 7,8 章) 时分别解锁发布。

libinfs1阅读 649

封面图

一介草民

67 声望
33 粉丝
宣传栏