在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) } } ... };
参考文献
Cesium原理篇:1最长的一帧之渲染调度 , by 法克鸡丝