Cesium摄像头跟踪飞机实体时晃动问题分析

在3D场景下使用Cesium跟踪飞机时会出现摄像头晃动问题,导致地图背景不断晃动,影响观看。下面以最新的Cesium1.51源码为例,解析Cesium 渲染过程原理,分析跟踪实体时摄像头晃动的原因,找出可能的解决方法。

Cesium渲染过程分析

使用Cesium最简单示例代码如下:

1
var viewer = new Cesium.Viewer('cesiumContainer');

Viewer是Cesium构建应用的最基础的组件。它又是其他组件的容器,包括:

  • animation:控制时间前进、倒退、暂停以及前进和倒退速度的组件
  • baseLayerPicker:图层选择组件
  • fullscreenButton:控制是否全屏的组件
  • vrButton:控制是否VR显示的组件
  • geocoder:地理位置搜索组件
  • homeButton:返回摄像头默认位置按钮组建
  • infoBox:信息框组件
  • sceneModePicker:场景模式选择组件
  • selectionIndicator:选择指示组件
  • timeline:时间线组件
  • navigationHelpButton:导航帮助按钮,告诉使用者如何使用鼠标和触摸屏操纵虚拟地球
  • CesiumWidget:虚拟地球组件

其中,虚拟地球组件CesiumWidget是Viewer包含核心组件,在Viewer中创建CesiumWidget对象时,将设置其useDefaultRenderLoop属性。设置该属性将启动渲染函数startRenderLoop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//from Source/Widgets/CesiumWidget/CesiumWidget.js
useDefaultRenderLoop : {
get : function() {
return this._useDefaultRenderLoop;
},
set : function(value) {
if (this._useDefaultRenderLoop !== value) {
this._useDefaultRenderLoop = value;
if (value && !this._renderLoopRunning) {
startRenderLoop(this);
}
}
}
},

函数startRenderLoop是Cesium渲染的开始,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function startRenderLoop(widget) {
widget._renderLoopRunning = true;

var lastFrameTime = 0;
function render(frameTime) {
if (widget.isDestroyed()) {
return;
}

if (widget._useDefaultRenderLoop) {
try {
var targetFrameRate = widget._targetFrameRate;
if (!defined(targetFrameRate)) {
widget.resize();
widget.render();
requestAnimationFrame(render);
} else {
var interval = 1000.0 / targetFrameRate;
var delta = frameTime - lastFrameTime;

if (delta > interval) {
widget.resize();
widget.render();
lastFrameTime = frameTime - (delta % interval);
}
requestAnimationFrame(render);
}
} catch (error) {
...
}
} else {
widget._renderLoopRunning = false;
}
}

requestAnimationFrame(render);
}

CesiumWidget组件的render方法随后调用Scene的render方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Scene.prototype.render = function(time) {
if (!defined(time)) {
time = JulianDate.now();
}

var frameState = this._frameState;
this._jobScheduler.resetBudgets();

var cameraChanged = this._view.checkForCameraUpdates(this);
var shouldRender = !this.requestRenderMode || this._renderRequested || cameraChanged || this._logDepthBufferDirty || (this.mode === SceneMode.MORPHING);
if (!shouldRender && defined(this.maximumRenderTimeChange) && defined(this._lastRenderTime)) {
var difference = Math.abs(JulianDate.secondsDifference(this._lastRenderTime, time));
shouldRender = shouldRender || difference > this.maximumRenderTimeChange;
}

if (shouldRender) {
this._lastRenderTime = JulianDate.clone(time, this._lastRenderTime);
this._renderRequested = false;
this._logDepthBufferDirty = false;
var frameNumber = CesiumMath.incrementWrap(frameState.frameNumber, 15000000.0, 1.0);
updateFrameNumber(this, frameNumber, time);
}

// Update
this._preUpdate.raiseEvent(this, time);
tryAndCatchError(this, update);
this._postUpdate.raiseEvent(this, time);

if (shouldRender) {
// Render
this._preRender.raiseEvent(this, time);
tryAndCatchError(this, render);

RequestScheduler.update();
}

updateDebugShowFramesPerSecond(this, shouldRender);
callAfterRenderFunctions(this);

if (shouldRender) {
this._postRender.raiseEvent(this, time);
}
};

Scene的render方法中tryAndCatchError函数将调用render函数。在该render函数中,地球的主要要素(地形&影像)的渲染,将在Globe的beginFrame和endFrame之间完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function render(scene) {
...

if (defined(scene.globe)) {
scene.globe.beginFrame(frameState);
}

updateEnvironment(scene);
updateAndExecuteCommands(scene, passState, backgroundColor);
resolveFramebuffers(scene, passState);

passState.framebuffer = undefined;
executeOverlayCommands(scene, passState);

if (defined(scene.globe)) {
scene.globe.endFrame(frameState);

if (!scene.globe.tilesLoaded) {
scene._renderRequested = true;
}
}

...
}

其中updateAndExecuteCommands负责数据的调度,比如哪些Tile需要创建,这些Tile相关的地形数据,以及涉及到的影像数据之间的调度,都是在该函数中维护。而scene.globe.endFrame中,会对该帧所涉及的GlobeTile的下载,解析等进行处理。

Cesium跟踪实体

在Viewer组件构造函数内,Viewer订阅了场景组件Scene的渲染后事件postRender,以执行Viewer自己的_postRender函数。

1
eventHelper.add(scene.postRender, Viewer.prototype._postRender, this);

Viewer的_postRender函数代码如下,其中updateTrackedEntity函数将更新被跟踪实体的摄像头位置:

1
2
3
4
Viewer.prototype._postRender = function() {
updateZoomTarget(this);
updateTrackedEntity(this);
};

updateTrackedEntity函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function updateTrackedEntity(viewer) {
if (!viewer._needTrackedEntityUpdate) {
return;
}

var trackedEntity = viewer._trackedEntity;
var currentTime = viewer.clock.currentTime;

//Verify we have a current position at this time. This is only triggered if a position
//has become undefined after trackedEntity is set but before the boundingSphere has been
//computed. In this case, we will track the entity once it comes back into existence.
var currentPosition = Property.getValueOrUndefined(trackedEntity.position, currentTime);

if (!defined(currentPosition)) {
return;
}

var scene = viewer.scene;

var state = viewer._dataSourceDisplay.getBoundingSphere(trackedEntity, false, boundingSphereScratch);
if (state === BoundingSphereState.PENDING) {
return;
}

var sceneMode = scene.mode;
if (sceneMode === SceneMode.COLUMBUS_VIEW || sceneMode === SceneMode.SCENE2D) {
scene.screenSpaceCameraController.enableTranslate = false;
}

if (sceneMode === SceneMode.COLUMBUS_VIEW || sceneMode === SceneMode.SCENE3D) {
scene.screenSpaceCameraController.enableTilt = false;
}

var bs = state !== BoundingSphereState.FAILED ? boundingSphereScratch : undefined;
viewer._entityView = new EntityView(trackedEntity, scene, scene.mapProjection.ellipsoid);
viewer._entityView.update(currentTime, bs);
viewer._needTrackedEntityUpdate = false;
}

除此之外,Viewer组件订阅了Clock组建的onTick事件,以执行其自身的_onTick事件处理函数:

1
eventHelper.add(clock.onTick, Viewer.prototype._onTick, this);

在Viewer组件的_onTick事件处理函数中,同样会更新被跟踪实体的摄像头位置。而Cesium摄像头跟踪飞机实体时产生晃动的根源即在此处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Viewer.prototype._onTick = function(clock) {
var time = clock.currentTime;

var isUpdated = this._dataSourceDisplay.update(time);
if (this._allowDataSourcesToSuspendAnimation) {
this._clockViewModel.canAnimate = isUpdated;
}

var entityView = this._entityView;
if (defined(entityView)) {
var trackedEntity = this._trackedEntity;
var trackedState = this._dataSourceDisplay.getBoundingSphere(trackedEntity, false, boundingSphereScratch);
if (trackedState === BoundingSphereState.DONE) {
entityView.update(time, boundingSphereScratch);
}
}

...
};

可行的解决方案

在Viewer组件的_onTick函数做如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Viewer.prototype._onTick = function(clock) {
var time = clock.currentTime;

var isUpdated = this._dataSourceDisplay.update(time);
if (this._allowDataSourcesToSuspendAnimation) {
this._clockViewModel.canAnimate = isUpdated;
}

var entityView = this._entityView;
if (defined(entityView)) {
var trackedEntity = this._trackedEntity;
var trackedState = this._dataSourceDisplay.getBoundingSphere(trackedEntity, false, boundingSphereScratch);
if (trackedState === BoundingSphereState.DONE) {
//entityView.update(time, boundingSphereScratch);
var range=this.camera.distanceToBoundingSphere(boundingSphereScratch);
var targetRange=range>boundingSphereScratch.radius*10?range:boundingSphereScratch.radius*10;
var offset=new HeadingPitchRange(0.0,-Math.toRadians(45.0),targetRange);
this.camera.viewBoundingSphere(boundingSphereScratch,offset)
}
}

...
};

参考文献

  1. Cesium原理篇:1最长的一帧之渲染调度, by 法克鸡丝