如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了。之前的几个系列,我们全面的介绍了Cesium的地形内容,详见:

有了前面的“骨骼”,下面我们详细介绍一下影像篇的调度,以及最终如何结合地形的数据完成渲染的过程。

类关系概述

和TerrainProvider的类关系相似,ImageryProvider的创建也是从Globe类开始的。不过,在Cesium中,一个Globe只有一个TerrainProvider,而可以有多个ImageryProvider,比如Bing的, 天地图的,还有文字注记的,甚至在加上局部范围,自定义的Provider,在实际中,这种使用场景是很常见的,就想一个人,只有一副骨架,但可以搭配多件衣服一个道理。因此,在Globe中提供了ImageryLayerCollection成员,用来管理多个ImageryProvider。

对于ImageryProvider,Cesium还做了一层封装,通过ImageryLayer来封装不同的Provider,Provider用来负责切片数据的下载,工作的成果则通过ImageryLayer来管理,比如计算需要的瓦片数据,发送切片请求,判断是否在缓存中已经有了Imagery(切片数据),对数据进行动态投影的换算,切片数据创建对应纹理等,都是ImageryLayer来完成的。

最后就落到了Imagery,每一个瓦片对应一个Imagery,自己把自己的事情做好(动态投影,创建纹理),维护好自身的状态,不给组织添麻烦。

综上所述,大概的类关系如下:

创建Imagery

有了上面的初始化过程后,我们开始讨论地球网格调度的过程,Cesium是以地形Tile为标准来调度的。针对每一个地形Tile,提供prepareNewTile方法来创建地形和影像的Tile,地形的我们之前在《Cesium原理篇:3最长的一帧之地形(1) 》已经详细讨论过了,如下是影像部分的代码:

  1. // 请求地球网格
  2. function prepareNewTile(tile, terrainProvider, imageryLayerCollection) {
  3.  
  4. // 地形部分呢代码……
  5.  
  6. // 遍历imageryLayerCollection中对应的ImageryProvider
  7. for (var i = 0, len = imageryLayerCollection.length; i < len; ++i) {
  8. var layer = imageryLayerCollection.get(i);
  9. if (layer.show) {
  10. // 通过Provider·创建对应Tile的Imagery
  11. layer._createTileImagerySkeletons(tile, terrainProvider);
  12. }
  13. }
  14. }

这里就有一个问题,也就是地形的坐标系和影像坐标系可能不一致的情况。之前我们提到过,地形数据一般都是WGS84,而基本上,所有在线数据都是墨卡托投影。这样,地形的Tile(XYZ)和影像的Tile(XYZ)就不是一一对应的关系了。而_createTileImagerySkeletons函数就是来计算这个映射关系,确定每一个地形的tile所对应哪些Imagery Tile。如果地形和影像的坐标系是一致的,那地形和影像Tile是1:1的对应关系,如果两者不一致,则需要额外处理了。伪代码逻辑如下:

  1. ImageryLayer.prototype._createTileImagerySkeletons = function(tile, terrainProvider, insertionPoint) {
  2. // 获取当前地形Tile的有效的经纬度范围
  3. var rectangle = Rectangle.intersection(tile.rectangle, imageryBounds, tileImageryBoundsScratch);
  4.  
  5. // 获取该影像服务的投影坐标,WGS84 or Mercator
  6. var imageryTilingScheme = imageryProvider.tilingScheme;
  7. // 计算地形Tile有效范围的西北(左上角) 对应影像的XY序号
  8. var northwestTileCoordinates = imageryTilingScheme.positionToTileXY(Rectangle.northwest(rectangle), imageryLevel);
  9. // 计算地形Tile有效范围的东南(右下角) 对应影像的XY序号
  10. var southeastTileCoordinates = imageryTilingScheme.positionToTileXY(Rectangle.southeast(rectangle), imageryLevel);
  11.  
  12. // 通过两个for循环,遍历TileCoordinates,也就获取到该地形Tile所需要的影像切片了
  13. for ( var i = northwestTileCoordinates.x; i <= southeastTileCoordinates.x; i++) {
  14. for ( var j = northwestTileCoordinates.y; j <= southeastTileCoordinates.y; j++) {
  15. // 判断该影像切片是否已经创建了
  16. // 因为有可能出现相邻两个地形的Tile,一个需要影像切片的上半部分,一个需要下半部分
  17. var imagery = this.getImageryFromCache(i, j, imageryLevel, imageryRectangle);
  18. // 引用计数,将需要的imagery绑定到对应的GlobeSurfaceTile上
  19. surfaceTile.imagery.splice(insertionPoint, 0, new TileImagery(imagery, texCoordsRectangle));
  20. }
  21. }
  22. }

这样,我们就获取了需要的影像切片,接着就是下载,创建纹理,纠偏,足够幸运的话,最终会渲染到屏幕上,这个逻辑的代码实现如下:

  1. // TileImagery调用Imagery实现影像切片的相关调度
  2. TileImagery.prototype.processStateMachine = function(tile, frameState) {
  3. var loadingImagery = this.loadingImagery;
  4. loadingImagery.processStateMachine(frameState);
  5. }
  6. // 基于状态的影像数据调度
  7. Imagery.prototype.processStateMachine = function(frameState) {
  8. // 如果该影像切片没有下载,则下载
  9. if (this.state === ImageryState.UNLOADED) {
  10. this.state = ImageryState.TRANSITIONING;
  11. this.imageryLayer._requestImagery(this);
  12. }
  13.  
  14. // 下载后创建对应的纹理
  15. if (this.state === ImageryState.RECEIVED) {
  16. this.state = ImageryState.TRANSITIONING;
  17. this.imageryLayer._createTexture(frameState.context, this);
  18. }
  19.  
  20. // 进行投影换算,纠偏
  21. if (this.state === ImageryState.TEXTURE_LOADED) {
  22. this.state = ImageryState.TRANSITIONING;
  23. this.imageryLayer._reprojectTexture(frameState, this);
  24. }
  25. };

ReprojectTexture

这里代码都比较容易理解,着重讲一下这个投影转换的过程,先看如下两个图:

前者是WSG84,后者是墨卡托下对应地球全幅的效果,可见前者长宽比是2:1,而后者是1:1.因此,总体来说,如果对两者做四叉树剖分,前者需要先竖直切两半(X方向),剩下的都一样(Y方向)。这样,动态投影的过程可以粗略的认为就是把下面这张图拉伸成上面这个图的过程。

如果大家对动态投影有一定了解的话,应该知道这个过程的计算量是很大的,而我们毕竟是JS的应用,对此Cesium采用了两个策略,一是简化数据,将这个256*256简化为2*64大小,类似扫描行来矫正,二是通过Shader,通过GPU RTT的方式,从硬件上来实现高效转换。具体的实现函数是reprojectToGeographic,Cesium做了很详细的解释,为何最终选择这种方式,比如对移动平台的考虑等,有兴趣的可以看一下源码,这里仅给出最终position和纹理uv的计算过程,最终在shader中就是将图片当前position对应的位置,赋予纹理中对应uv的像素值。

  1. // position
  2. var positions = new Float32Array(2 * 64 * 2);
  3. var index = 0;
  4. for (var j = 0; j < 64; ++j) {
  5. var y = j / 63.0;
  6. positions[index++] = 0.0;
  7. positions[index++] = y;
  8. positions[index++] = 1.0;
  9. positions[index++] = y;
  10. }
  11. // 经纬度下对应的uv值
  12. for (var webMercatorTIndex = 0; webMercatorTIndex < 64; ++webMercatorTIndex) {
  13. var fraction = webMercatorTIndex / 63.0;
  14. var latitude = CesiumMath.lerp(south, north, fraction);
  15. sinLatitude = Math.sin(latitude);
  16. var mercatorY = 0.5 * Math.log((1.0 + sinLatitude) / (1.0 - sinLatitude));
  17. var mercatorFraction = (mercatorY - southMercatorY) * oneOverMercatorHeight;
  18. webMercatorT[outputIndex++] = mercatorFraction;
  19. webMercatorT[outputIndex++] = mercatorFraction;
  20. }

换句话说,通过上面的转换算法,对关键点构成三角网,其他的点在片元中插值,这样生成一张新的纹理(RTT),将经过坐标系转换的纹理替换之前原始的墨卡托纹理。这里回答之前的一个情况:如果地形也是采用Mercator(只有默认的EllipsoidTerrainProvider可以选择这种坐标系),影像也是Mercator,这样就不需要投影转换,性能上应该会更好吧。理论上确实如此,但实际上,通过代码,Cesium并没有考虑过这种情况,所以只要判断影像不是WGS84的,统一都做了一次转换。换个角度来说,我发现即使不做投影转换,肉眼看上去,效果上并没有什么差别。

DrawCommandsForTile

讲到这,终于到了这一帧的最后时刻,历尽千辛万苦,百般阻挠,强壮了我的骨骼,滋润了我的肌肤后,终于进入了渲染环节。

Cesium的渲染都是通过DrawCommand来完成,这一块的理解需要对Render模块有一个认识,所以这里也不打算展开讲。简单的说,主要是VertexArray来绑定VBO(地形数据),通过uniformMap来传递顶点和片元着色器的参数,而通过dayTextures将该Tile对应的多个影响纹理传入到Shader中。下面,主要介绍一下多个纹理叠加和水面的实现。

多重纹理

为了考虑多重纹理的可能,Cesium在GlobeSurfaceShaderSet.prototype.getShaderProgram中用一个笨方法来处理:

  1. var computeDayColor = '\
  2. vec4 computeDayColor(vec4 initialColor, vec2 textureCoordinates)\n\
  3. {\n\
  4. vec4 color = initialColor;\n';
  5.  
  6. for (var i = 0; i < numberOfDayTextures; ++i) {
  7. computeDayColor += '\
  8. color = sampleAndBlend(\n\
  9. color,\n\
  10. u_dayTextures[' + i + '],\n\
  11. textureCoordinates,\n\
  12. u_dayTextureTexCoordsRectangle[' + i + '],\n\
  13. u_dayTextureTranslationAndScale[' + i + '],\n\
  14. ' + (applyAlpha ? 'u_dayTextureAlpha[' + i + ']' : '1.0') + ',\n\
  15. ' + (applyBrightness ? 'u_dayTextureBrightness[' + i + ']' : '0.0') + ',\n\
  16. ' + (applyContrast ? 'u_dayTextureContrast[' + i + ']' : '0.0') + ',\n\
  17. ' + (applyHue ? 'u_dayTextureHue[' + i + ']' : '0.0') + ',\n\
  18. ' + (applySaturation ? 'u_dayTextureSaturation[' + i + ']' : '0.0') + ',\n\
  19. ' + (applyGamma ? 'u_dayTextureOneOverGamma[' + i + ']' : '0.0') + '\n\
  20. );\n';
  21. }
  22.  
  23. computeDayColor += '\
  24. return color;\n\
  25. }';

半自动植入计算computeDayColor的方法,其中,sampleAndBlend是shader中自带的函数,通过这些参数来获取纹理对应位置的颜色,而computeDayColor本身就是一个for循环,实现该位置下多个颜色的叠加,这样做的好处是里面的参数很多,而且不是定长的,所以避开了传参的麻烦。只要了解了这个过程,我们在看GlobeFS.glsl就简单多了:

  1. vec4 color = computeDayColor(u_initialColor, clamp(v_textureCoordinates, 0.0, 1.0));

轻松一句话,实现了多重纹理叠加,处理起来也方便很多,看来笨也有笨的智慧。当然,这里还有一个纹理纠偏的处理。有可能一个地形切片各占两个影像切片的一部分,这样,纹理对应地形切片的起始点就会有一个偏移和缩放的处理,保质两者匹配吻合。

  1. ImageryLayer.prototype._calculateTextureTranslationAndScale = function(tile, tileImagery) {
  2. var imageryRectangle = tileImagery.readyImagery.rectangle;
  3. var terrainRectangle = tile.rectangle;
  4. var terrainWidth = terrainRectangle.width;
  5. var terrainHeight = terrainRectangle.height;
  6.  
  7. var scaleX = terrainWidth / imageryRectangle.width;
  8. var scaleY = terrainHeight / imageryRectangle.height;
  9. // xy为偏移,zw为缩放
  10. return new Cartesian4(
  11. scaleX * (terrainRectangle.west - imageryRectangle.west) / terrainWidth,
  12. scaleY * (terrainRectangle.south - imageryRectangle.south) / terrainHeight,
  13. scaleX,
  14. scaleY);
  15. };
  16.  
  17. // 片元中纹理计算公式
  18. vec2 textureCoordinates = tileTextureCoordinates * scale + translation;

水面

坦白说,这块我也是一知半解,里面有两个关键的参数,waterMask和oceanNormalMap,时间是根据czm_frameNumber来模拟的。坦白说,这部分代码的物理原理我还真不清楚,最终就是各类反射光的叠加,不多说废话了,等以后有机会再说吧。该方法可参考:

  1. vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat3 enuToEye, vec4 imageryColor, float maskValue)

至此,最长的一帧之Cesium告一段落,个人尽力详细介绍了Cesium整个球在渲染过程中的相关细节,希望对大家会有所收获。后面,会继续在应用,原理上继续深入的学习,研究和分享关于Cesium的个人见解。

Cesium原理篇:5最长的一帧之影像的更多相关文章

  1. Cesium原理篇:3最长的一帧之地形(2:高度图)

           这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维 ...

  2. Cesium原理篇:7最长的一帧之Entity(下)

    上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSo ...

  3. Cesium原理篇:7最长的一帧之Entity(上)

    之前的最长的一帧系列,我们主要集中在地形和影像服务方面.简单说,之前我们都集中在地球是怎么造出来的,从这一系列开始,我们的目光从GLOBE上解放出来,看看球面上的地物是如何渲染的.本篇也是先开一个头, ...

  4. Cesium原理篇:3最长的一帧之地形(3:STK)

    有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具 ...

  5. Cesium原理篇:1最长的一帧之渲染调度

    原计划开始着手地形系列,但发现如果想要从逻辑上彻底了解地形相关的细节,那还是需要了解Cesium的数据调度过程,这样才能更好的理解,因此,打算先整体介绍一下Cesium的渲染过程,然后在过渡到其中的两 ...

  6. Cesium原理篇:3最长的一帧之地形(1)

    前面我们从宏观上分析了Cesium的整体调度以及网格方面的内容,通过前两篇,读者应该可以比较清楚的明白一个Tile是怎么来的吧(如果还不明白全是我的错).接下来,在前两篇的基础上,我们着重讨论一下地形 ...

  7. Cesium原理篇:3最长的一帧之地形(4:重采样)

           地形部分的原理介绍的差不多了,但之前还有一个刻意忽略的地方,就是地形的重采样.通俗的讲,如果当前Tile没有地形数据的话,则会从他父类的地形数据中取它所对应的四分之一的地形数据.打个比方 ...

  8. Cesium原理篇:2最长的一帧之网格划分

    上一篇我们从宏观上介绍了Cesium的渲染过程,本章延续上一章的内容,详细介绍一下Cesium网格划分的一些细节,包括如下几个方面: 流程 Tile四叉树的构建 LOD 流程 首先,通过上篇的类关系描 ...

  9. Cesium原理篇:6 Render模块(6: Instance实例化)

    最近研究Cesium的实例化,尽管该技术需要在WebGL2.0,也就是OpenGL ES3.0才支持.调试源码的时候眼前一亮,发现VAO和glDrawBuffers都不是WebGL1.0的标准函数,都 ...

随机推荐

  1. WCF学习之旅—第三个示例之四(三十)

           上接WCF学习之旅—第三个示例之一(二十七)               WCF学习之旅—第三个示例之二(二十八)              WCF学习之旅—第三个示例之三(二十九)   ...

  2. 浏览器中用JavaScript获取剪切板中的文件

    本文转自我的个人网站  , 原文地址:http://www.zoucz.com/blog/2016/01/29/get-file-from-clipboard/  ,欢迎前往交流讨论 在网页上编辑内容 ...

  3. Hawk 4.4 执行器

    执行器是负责将Hawk的结果传送到外部环境的工具.你可以写入数据表,数据库,甚至执行某个特定的动作,或是生成文件等等. 在调试模式下,执行器都是不工作的.这是为了避免产生副作用.否则,每刷新一遍数据, ...

  4. java设计模式之--单例模式

    前言:最近看完<java多线程编程核心技术>一书后,对第六章的单例模式和多线程这章颇有兴趣,我知道我看完书还是记不住多少的,写篇博客记录自己所学的只是还是很有必要的,学习贵在坚持. 单例模 ...

  5. psoc学习

    第一是:项目的路径需要放在Documents and Settings\,也就是默认的文件夹的地方,不然会报错错误范例为:Question:CY8CKIT-023 kit example projec ...

  6. 对Maven、gradle、svn、spring 3.0 fragment、git的想法

    1.Maven Maven可以构建项目,采用pom方式配置主项目和其他需要引用的项目.同时可结合spring3.0的新特性web  fragment. 从现实出发,特别是对于管理不到位,程序员整体素质 ...

  7. 如何理解MySQL中auto_increment?

    1.auto_increment用于主键自动增长.比如从1开始增长,当把第一条数据删除,再插入第二条数据时,主键值为2,不是1.

  8. Oracle中的commit详解

    本文转自 : http://blog.csdn.net/hzhsan/article/details/9719307 它执行的时候,你不会有什么感觉.commit在数据库编程的时候很常用,当你执行DM ...

  9. OpenSUSE下编译安装OpenFoam

    在不是Ubuntu系统下安装OpenFoam,需要采用编译安装的方式.以下以OpenSuSE为例进行编译安装. 1 软件包准备 需要下载两个程序包: OpenFOAM-4.x-version-4.1. ...

  10. 找到第k个最小元----快速选择

    此算法借用快速排序算法. 这个快速选择算法主要利用递归调用,数组存储方式.包含3个文件,头文件QuickSelect.h,库函数QuickSelect.c,测试文件TestQuickSelect. 其 ...