如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了。之前的几个系列,我们全面的介绍了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) 》已经详细讨论过了,如下是影像部分的代码:

//  请求地球网格
function prepareNewTile(tile, terrainProvider, imageryLayerCollection) { // 地形部分呢代码…… // 遍历imageryLayerCollection中对应的ImageryProvider
for (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 Mercator
var 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原理篇: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. Matlab 高脚杯模型切片

    前言:此文为去年我替人做的一个课题,觉得比较简洁,图形也比较美观,因此放在博文里 数据源我放到了百度云盘高脚杯数据源 有兴趣的可以下载数据,跑程序试一下.也可以单独看看代码,看下实现过程. 主函数 % ...

  2. 初步了解nodejs

    什么是Node.js? 很多初学者并没有真正地理解Node.js到底是什么.nodejs.org网站中的描述也没有多大帮助. 首先要清楚Node不是一个Web服务器,这十分重要.它本身并不能做任何事情 ...

  3. 谈谈一些有趣的CSS题目(一)-- 左边竖条的实现方法

    开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...

  4. shiro权限管理框架与springmvc整合

    shiro是apache下的一个项目,和spring security类似,用于用户权限的管理‘ 但从易用性和学习成本上考虑,shiro更具优势,同时shiro支持和很多接口集成 用户及权限管理是众多 ...

  5. 深入理解javascript的getTime方法

    1.理解getTime getTime() 方法返回一个时间的格林威治时间数值. 可以使用这个方法把一个日期时间赋值给另一个Date 对象. 语法: dateObj.getTime() 参数: 无. ...

  6. https 安全验证问题

    最近为了满足苹果的 https 要求, 经过努力终于写出了方法 验证 SSL 证书是否满足 ATS 要求 nscurl --ats-diagnostics --verbose https://你的域名 ...

  7. JQuery阻止事件冒泡

    冒泡事件就是点击子节点,会向上触发父节点,祖先节点的点击事件. 我们在平时的开发过程中,肯定会遇到在一个div(这个div可以是元素)包裹一个div的情况,但是呢,在这两个div上都添加了事件,如果点 ...

  8. linux下 lvm 磁盘扩容

    打算给系统装一个oracle,发现磁盘空间不足.在安装系统的时候我选择的是自动分区,系统就会自动以LVM的方式分区.为了保证系统后期的可用性,建议所有新系统安装都采用LVM,之后生产上的设备我也打算这 ...

  9. (整理)MyBatis入门教程(一)

    本文转载: http://www.cnblogs.com/hellokitty1/p/5216025.html#3591383 本人文笔不行,根据上面博客内容引导,自己整理了一些东西 首先给大家推荐几 ...

  10. MzBlog分析

    早上衣明志 在QQ群里说他的博客开源了,地址在 https://github.com/qihangnet/MZBlog,基于NancyFX和MongoDB开发的.博客内容需要使用 MarkDown 进 ...