博客 / 詳情

返回

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 即使超過終點時刻,也不會停下來,類似 直播效果

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.