1.2 Cesium渲染流程

  

从前有座山,山里有座庙,庙里有个......”我们喜欢这样讲故事,有头有尾,一个调用接一个,特别因为JS本身的一些特点,往往我们会发现,半路杀出个“程咬金”,一些对象变量临场出现让人迷糊,这里面弄清楚整个流程显得尤为重要,搞清楚这个引擎流水线,我们才能把控这里面的机制。Cesium实时刷新,就是说每一帧都在更新,这就像最原始的动画制作一样,一页翻完翻另一个,只是刷新间隔快了,我们眼皮没发觉(但这也是一般状态下,如果场景完全静悄悄也可请求渲染模式,这时就不是每一帧都更新了,这个我们后面再说,先认为是动起来的)。 

我们在初始化一个球的时候,比较常用的个方式是创建一个Viewer容器,如下:

var viewer = new Cesium.Viewer('cesiumContainer', {
    shadows : true
});

然后可以在里面初始化一堆参数,很多人就误以为这个球的开口就是Viewer,其实不然;Viewer只是一个简单的启动器,帮忙携带了一些球启动的参数,但归根结底它仍然是一个二次封装的产物。Cesium球的大门在CesiumWidget里。在Viewer里我们可以看到,经过一通传递和组合,用户传入的参数,最终还是构建给了CesiumWidget:

         var cesiumWidget = new CesiumWidget(cesiumWidgetContainer, {
            imageryProvider: createBaseLayerPicker || defined(options.imageryProvider) ? false : undefined,           
            skyBox : options.skyBox,            
            scene3DOnly : scene3DOnly,            
            shadows : options.shadows,            
            mapMode2D : options.mapMode2D,
            requestRenderMode : options.requestRenderMode

...... });

options就是在外围Viewer传入进来的,包括最基础的影像、阴影设置等,一个widget包含一个三维场景。我们转入来看CesiumWidget,这里面也洋洋散散为自己也为外围调用封装了一堆东西,可谓是细致入微,但我们最终要看的是startRenderLoop函数

function render(frameTime) {  if (widget._useDefaultRenderLoop) {
        try {
          ......
                requestAnimationFrame(render);
            }
        } catch (error) {
           ......
        }
    } 
}
requestAnimationFrame(render);

看到这对WebGL稍熟悉便大彻大悟,requestAnimationFrame()是专门为脚本式的动画而生的,通过requestAnimationFrame()函数,跟传送带一样,高速运转一帧一帧,一个画面又一个画面的输送,不同的三维场景映入眼帘。开篇我们说Cesium是动起来的,就是在这运作的,当然,光到这我们只能说我们看清楚了JS的动画机制,还不能说看清楚了Cesium的动画机制,因为截止目前为止,Cesium还没有创建任何能看到的球上东西。

再接下来,同样是在CesiumWidget里,场景的渲染在一个自动调用函数里

 CesiumWidget.prototype.render = function() {
        if (this._canRender) {
            this._scene.initializeFrame();
            var currentTime = this._clock.tick();
            this._scene.render(currentTime);
        } else {
            this._clock.tick();
        }
    };

这里调用scene的render(time)函数,才是至关重要,细微化会调用scene里面的私有函数render(scene),它就是这个WebGL三维场景的渲染调度和绘制命令的组织者。但特别说一下,Cesium利用颜色缓冲区来实现拾取,在scene.pick函数里面,将ID当作颜色写入到一个离屏缓冲区,对象与ID唯一对应,然后根窗口坐标(x,y)拾取内容,readPixels读取颜色,并返回拾取的对象,看源码会发现scene.pick与scene.render很相似,但太阳、大气和天空盒没必要做拾取做了不处理。

  function render(scene) {
var frameState = scene._frameState; var context = scene.context; var us = context.uniformState;

//Cesium最近几个版本在scene的基础之上,有多加了一层view,方便数据调用和抽象共有层 var view = scene._defaultView; scene._view = view; updateFrameState(scene); frameState.passes.render = true; frameState.passes.postProcess = scene.postProcessStages.hasSelected; frameState.tilesetPassState = renderTilesetPassState; var backgroundColor = defaultValue(scene.backgroundColor, Color.BLACK); if (scene._hdr) { backgroundColor = Color.clone(backgroundColor, scratchBackgroundColor); backgroundColor.red = Math.pow(backgroundColor.red, scene.gamma); backgroundColor.green = Math.pow(backgroundColor.green, scene.gamma); backgroundColor.blue = Math.pow(backgroundColor.blue, scene.gamma); } frameState.backgroundColor = backgroundColor; frameState.creditDisplay.beginFrame(); scene.fog.update(frameState); us.update(frameState); var shadowMap = scene.shadowMap; if (defined(shadowMap) && shadowMap.enabled) { // Update the sun's direction Cartesian3.negate(us.sunDirectionWC, scene._sunCamera.direction); frameState.shadowMaps.push(shadowMap); } scene._computeCommandList.length = 0; scene._overlayCommandList.length = 0; var viewport = view.viewport; viewport.x = 0; viewport.y = 0; viewport.width = context.drawingBufferWidth; viewport.height = context.drawingBufferHeight; var passState = view.passState; passState.framebuffer = undefined; passState.blendingEnabled = undefined; passState.scissorTest = undefined; passState.viewport = BoundingRectangle.clone(viewport, passState.viewport); 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; } } frameState.creditDisplay.endFrame(); context.endFrame(); }

这里完整地将整个函数列出来,表面不算太长,但设计了整个渲染周期前期准备、完整绘制、后期效果方方面面,可谓保罗万象,有必要仔细跟一下函数里面的细节,Cesium必由之路,不管是自建功能还是调整源码,都会不断地与这段代码打交道。下面我们就来仔细过一下这段代码。

首先一开始做了一些渲染前的参数传递与准备,frameState.passes.render = true是默认是渲染状态,有人可能问,这个重点函数不就是负责渲染的吗,为什么还有具体的默认状态?其实Cesium由于主要还是基于WebGL1.0来实现,很多的包括深度信息、最终合成帧等都是一帧一帧重新渲染场景的得到的;正常情况下就是很普通的将场景最终的样子推送到绘制缓冲区就OK了,但有些时候,我们并不需要它绘制出来,仅仅是为了获取深度图或者点选对象的ID列表,因此严谨Cesium对渲染不同的需求又做了分流如下几种:

passes = {
render : 最常见的渲染,
pick : 图元拾取渲染,
depth : 深度渲染,
postProcess : 后处理阶段渲染,
offscreen : 离屏渲染

......
}

 这下就明了了,比如仅仅为了拿到深度图,frameState.passes.depth= true设为即可,自然往下执行绘制命令,这张深度图就出来了。

var pickDepth = getPickDepth(this, 0);
var context = frameState.context;
var windowCoordinate = SceneTransforms.wgs84ToWindowCoordinates(this._scene,targetPosition,new Cartesian2());
var drawingBufferPosition = SceneTransforms.transformWindowToDrawingBuffer(this._scene, windowCoordinate, new Cartesian2());
drawingBufferPosition.y = this._scene.drawingBufferHeight - drawingBufferPosition.y;
var depth = pickDepth.getDepth(context, drawingBufferPosition.x, drawingBufferPosition.y);

具体屏幕坐标对应的深度值获取操作就顺理成章了,绘制结束后,直接获取深度图,计算绘制缓冲的坐标,y轴反转,就得到深度值了,封装很到位,看源码其实也就是WebGL坐标反算那套,这点很有用,很多用到深度对比的空间分析手法都可以插入,当然这是最简单粗暴的获取方式,因为中间Cesium又会根据各个绘制命令再次计算分支命令,其实有相当一部分是不需要的......关于这些绘制命令我们下节再讨论,不然本章不知道要写到猴年马月了。总有人问,Cesium深度图如何获取,是的,就是如此简单啊!

接下来就是一桶更新,每一帧都是不一样的世界。

视口指定和backgroundColor这些就没必要过多去讨论了。直接来看如下三个更新:

       1)  updateFrameState(scene);

        2) context.uniformState.update(frameState)

        3)  scene.fog.update(frameState)

第一个函数主要任务是更新帧状态,而FrameState是Cesium里很重要的一个渲染变量,还有context、PassState等,这里面是一些环境基础更新,包块地球mode、相机、投影方式、太阳颜色、对数深度设置等等,后面的计算需要基于这些设置生成;第二函数主要复杂US状态机的更新,对太阳月亮信息做了进一步计算;第三个函数就是对雾的专门更新,包括density、sse、brightness。

好了,总算是更新完了!

好比一场大考,前面该装备的装备好了,该上战场了吧,下面三个函数我们习惯性放一起看。

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

这三个方法可谓是整个渲染过程的中流砥柱!重要绘制在updateAndExecuteCommands()函数里一步一步完成实现的,其实地形要特殊一些,它的完成是在scene.globe.endFrame(frameState)基于四叉树逐步分瓦片请求。在updateAndExecuteCommands()函数里,一开始对不同的视图做了分别计算

 if (useWebVR) {
            executeWebVRCommands(scene, passState, backgroundColor);
        } else if (mode !== SceneMode.SCENE2D || scene._mapMode2D === MapMode2D.ROTATE) {
            executeCommandsInViewport(true, scene, passState, backgroundColor);
        } else {
            updateAndClearFramebuffers(scene, passState, backgroundColor);
            execute2DViewportCommands(scene, passState);
        }

有没有很熟悉,针对VR做VR命令计算,针对3D在视口下计算,针对2D也有对应的计算。我们常见的自然是三维模式走的 executeCommandsInViewport(true, scene, passState, backgroundColor)方法。参数早在前面的的更新中早就准备好了,这个方法也很讲究。

一开始是对所有的图元进行更新,这就包括我们在上层使用的一堆堆primitives、阴影图的更新,Cesium的机制是这样的,有什么三维数据类型 就有对象的集合类, 有add  就有对应的remove,对象渲染每个都有对应的update()函数,这个函数负责自己图元的渲染调度,这种设计很巧妙,真正意义做到了分而治之,而每个图元的update()就是这里遍历调用的,就像排队面试一样,HR只负责叫你伙计轮到你了,至于你要向我倾诉什么那也是你自己来说。

if (!renderTranslucentDepthForPick) {
            updateAndRenderPrimitives(scene);
        }

接下来该推Call了吧,但Cesium不急不忙,基于上面的图元来计算视锥体,这里就有点“多此一举”的感觉,其实不然。首先我们要明白,并不是每一帧都必须运用相同的视锥体,这是一个前提,然后,图形学里,我们知道当场景拉得很大很长的时候,近裁剪面与远裁剪面不得不拉开很大,这个时候有些问题就暴露出来了,最难看的便是精度问题。关于解决精度问题我们也可以将近裁剪面再拉远一点,或者远裁剪面在拉近一些,但这里面的开销以及效果只能说差强人意;多视锥体就此诞生了,它将当前场景分成了多个视锥体,通常2个左右,有效避免 z-fighting,每个frustum都有相同的视野范围和纵横比,只有近裁剪面和远裁剪面的距离不同。

view.createPotentiallyVisibleSet(scene);

最后,执行绘制命令,大功告成。

executeCommands(scene, passState);
resolveFramebuffers(scene, passState)函数主要是将前一个效果作为输入,计算后期渲染效果,再输出,作为下一个效果的输入,其中就包括我们知道的 


WebGL如此炫丽,绽放真实世界!

一阵清雨,躲进满是书香的小楼

爱技术,爱交流 685834990
原文地址:https://www.cnblogs.com/GISCesium/p/10420492.html