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

  • Cesium原理篇:1最长的一帧之渲染调度
  • Cesium原理篇:2最长的一帧之网格划分
  • Cesium原理篇:3最长的一帧之地形(1)
  • Cesium原理篇:3最长的一帧之地形(2:高度图)
  • Cesium原理篇:3最长的一帧之地形(3:STK)
  • Cesium原理篇:3最长的一帧之地形(4:重采样)

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

类关系概述

和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) 》已经详细讨论过了,如下是影像部分的代码:

//  请求地球网格
function prepareNewTile(tile, terrainProvider, imageryLayerCollection) {// 地形部分呢代码……// 遍历imageryLayerCollection中对应的ImageryProviderfor (var i = 0, len = imageryLayerCollection.length; i < len; ++i) {var layer = imageryLayerCollection.get(i);if (layer.show) {// 通过Provider·创建对应Tile的Imagery
            layer._createTileImagerySkeletons(tile, terrainProvider);}}
}

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

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

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

// TileImagery调用Imagery实现影像切片的相关调度
TileImagery.prototype.processStateMachine = function(tile, frameState) {var loadingImagery = this.loadingImagery;loadingImagery.processStateMachine(frameState);
}
// 基于状态的影像数据调度
Imagery.prototype.processStateMachine = function(frameState) {// 如果该影像切片没有下载,则下载if (this.state === ImageryState.UNLOADED) {this.state = ImageryState.TRANSITIONING;this.imageryLayer._requestImagery(this);}// 下载后创建对应的纹理if (this.state === ImageryState.RECEIVED) {this.state = ImageryState.TRANSITIONING;this.imageryLayer._createTexture(frameState.context, this);}// 进行投影换算,纠偏if (this.state === ImageryState.TEXTURE_LOADED) {this.state = ImageryState.TRANSITIONING;this.imageryLayer._reprojectTexture(frameState, this);}
};

ReprojectTexture

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

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

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

// position
var positions = new Float32Array(2 * 64 * 2);
var index = 0;
for (var j = 0; j < 64; ++j) {var y = j / 63.0;positions[index++] = 0.0;positions[index++] = y;positions[index++] = 1.0;positions[index++] = y;
}
// 经纬度下对应的uv值
for (var webMercatorTIndex = 0; webMercatorTIndex < 64; ++webMercatorTIndex) {var fraction = webMercatorTIndex / 63.0;var latitude = CesiumMath.lerp(south, north, fraction);sinLatitude = Math.sin(latitude);var mercatorY = 0.5 * Math.log((1.0 + sinLatitude) / (1.0 - sinLatitude));var mercatorFraction = (mercatorY - southMercatorY) * oneOverMercatorHeight;webMercatorT[outputIndex++] = mercatorFraction;webMercatorT[outputIndex++] = mercatorFraction;
}

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

DrawCommandsForTile

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

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

多重纹理

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

var computeDayColor = '\
vec4 computeDayColor(vec4 initialColor, vec2 textureCoordinates)\n\
{\n\vec4 color = initialColor;\n';for (var i = 0; i < numberOfDayTextures; ++i) {computeDayColor += '\
color = sampleAndBlend(\n\color,\n\u_dayTextures[' + i + '],\n\textureCoordinates,\n\u_dayTextureTexCoordsRectangle[' + i + '],\n\u_dayTextureTranslationAndScale[' + i + '],\n\' + (applyAlpha ? 'u_dayTextureAlpha[' + i + ']' : '1.0') + ',\n\' + (applyBrightness ? 'u_dayTextureBrightness[' + i + ']' : '0.0') + ',\n\' + (applyContrast ? 'u_dayTextureContrast[' + i + ']' : '0.0') + ',\n\' + (applyHue ? 'u_dayTextureHue[' + i + ']' : '0.0') + ',\n\' + (applySaturation ? 'u_dayTextureSaturation[' + i + ']' : '0.0') + ',\n\' + (applyGamma ? 'u_dayTextureOneOverGamma[' + i + ']' : '0.0') + '\n\
);\n';}computeDayColor += '\return color;\n\
}';

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

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

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

ImageryLayer.prototype._calculateTextureTranslationAndScale = function(tile, tileImagery) {var imageryRectangle = tileImagery.readyImagery.rectangle;var terrainRectangle = tile.rectangle;var terrainWidth = terrainRectangle.width;var terrainHeight = terrainRectangle.height;var scaleX = terrainWidth / imageryRectangle.width;var scaleY = terrainHeight / imageryRectangle.height;// xy为偏移,zw为缩放return new Cartesian4(scaleX * (terrainRectangle.west - imageryRectangle.west) / terrainWidth,scaleY * (terrainRectangle.south - imageryRectangle.south) / terrainHeight,scaleX,scaleY);
};// 片元中纹理计算公式
vec2 textureCoordinates = tileTextureCoordinates * scale + translation;

水面

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

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

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

Cesium原理篇:5最长的一帧之影像相关推荐

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

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

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

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

  3. 通过“四大行为”对WCF的扩展[原理篇]

    整个WCF框架由两个基本的层次构成,即服务模型层和信道层.对信道层的扩展主要通过针对绑定的扩展实现,具体来说就是自定义绑定元素,以及相关的信道管理器(信道监听器和信道工厂).信道来改变对消息的处理和传 ...

  4. 原理篇 | 推荐系统之矩阵分解模型

    导语:本系列文章一共有三篇,分别是 <科普篇 | 推荐系统之矩阵分解模型> <原理篇 | 推荐系统之矩阵分解模型> <实践篇 | 推荐系统之矩阵分解模型> 第一篇用 ...

  5. [zz]GMM-HMM语音识别模型 原理篇

    GMM-HMM语音识别模型 原理篇 分类: Data Structure Machine Learning Data Mining 2014-05-28 20:52 20662人阅读 评论(34) 收 ...

  6. Redis基本操作——队列 List(原理篇)

    2019独角兽企业重金招聘Python工程师标准>>> Redis基本操作--List(原理篇) 学习过数据结构的同学,一定对链表(Linked List)十分的熟悉.相信我们自己也 ...

  7. 【原理篇】推荐系统之矩阵分解模型

    导语: 上次给大家分享了本系列文章第一篇<[科普篇]推荐系统之矩阵分解模型>,第一篇用一个具体的例子介绍了MF是如何做推荐的.今天给大家带来第二篇<[原理篇]推荐系统之矩阵分解模型& ...

  8. 前端面试题之浏览器原理篇

    前端面试题之浏览器原理篇 一.浏览器安全 1. 什么是 XSS 攻击? (1)概念 (2)攻击类型 2. 如何防御 XSS 攻击? 3. 什么是 CSRF 攻击? (1)概念 (2)攻击类型 4. 如 ...

  9. Android卡顿掉帧问题分析之原理篇

    当用户抱怨手机在使用过程中存在卡顿问题的时候,会严重影响用户对手机品牌的好感和应用APP的体验,从而导致用户对手机品牌的忠诚度降低或应用APP的装机留存率下降.所以无论是手机设备厂商还是应用APP开发 ...

  10. iOS内存管控实战(上)—原理篇

    因文章单篇过长,按照 原理.分析工具 和 实战 拆分成上.中.下三部分,点击阅读. iOS内存管控实战(上)-原理篇 iOS内存管控实战(中)-分析工具篇 iOS内存管控实战(下)-实战篇 前言 近期 ...

最新文章

  1. Kaggle心得(一)
  2. 4.6 W 字总结!Java 11—Java 17特性详解
  3. 设计模式C++实现(16)——状态模式
  4. tomcat与apache的面试题
  5. Go语言入门——Go语言环境搭建
  6. java报错symbol_为何此处会出现找不到symbol的错误呢?
  7. Java MyBatis 别名
  8. python机器学习库sklearn——岭回归(Ridge、RidgeCV)(L2正则化)
  9. centos7 源码安装redis
  10. Mongodb源码分析--内存文件映射(MMAP)
  11. 图文详解VxLAN技术(二)
  12. HTTP Live Streaming基础知识
  13. 揭秘金庸笔下的假面江湖
  14. 正大国际期货:如何提升外盘恒指交易技巧?
  15. (Step1-500题)UVaOJ+算法竞赛入门经典+挑战编程+USACO
  16. 创意名片大全:一组精美的折叠效果名片设计
  17. 教师空间HTML没有怎么查找,人人通空间为什么看不到老师发的作业?
  18. Cardboard Unity SDK Reference 翻译版
  19. SQL 注释语句 (--与/*...*/)
  20. 基于javaweb小说评价下载网站管理系统 ssm框架

热门文章

  1. 基于STM32MP1的IOT参考设计(采用QT和web技术)
  2. 美国电话号码正则表达式
  3. Java 实战:桌球小游戏
  4. 华为HCNA综合实验
  5. html页面如何嵌入cms,制作好的网页模板如何放入网站CMS中?
  6. 安装、选择-如何制作U盘系统盘以及U盘安装操作系统的方法 -by小雨
  7. Cisco nat inside接口,outside接口,nvi接口的区别
  8. 前端-优雅的VueJS
  9. react实现问卷调查
  10. Tech Blog by Eason!