使用three.js渲染瓦片地球-第一篇

1,435 阅读7分钟

近期有个需求是想使用自己的3d引擎去渲染地球逻辑,如果从头实现一遍会比较耗费时间,而且后续还要实现倾斜摄影等等,所以打算使用cesium来帮我们做调度然后用自己的3d引擎渲染。 在github上找到了个cesium-three发现该项目的策略是three.js去渲染three到物体,cesium去渲染cesium物体,这样实现起来比较容易,但是问题是当一个复杂场景出现的时候,排序怎么办,当然还有更多问题根本是无法解决的。正所谓一山不容二虎,当二虎有分歧时听谁的那? 看了下cesium代码发现cesium的逻辑和渲染分离的比较好,每帧的渲染就是执行最终的drawCommand,本来以为一天就能搞定,没想到还是花了几天时间,里面还是涉及到一些细节问题,现在分享给大家思路和方法(目前cesium地球调度还有一两个小点没有完全弄清楚,不过我这边选择了跳过,不影响瓦片逻辑,如果有知道的大神欢迎评论和指导,目前自己的引擎我暂且使用three.js来表示吧)

渲染顺序如下:

  1. frameState的更新-----主要是传入Cesium需要的framestate需要的各种参数,摄影机,viewPort,视锥等 这其中我们需要禁掉一些cesium不必要的开销,如阴影等等(因为阴影我们还是要用自己的引擎实现),当然由于摄影机操作也需要自己实现一套,摄影机操作和更新肯定是自己的引擎做的事情,同时cesium摄影机也并不太符合自己的操作习惯,摄影机操作其实也不是太简单,后续有时间也可以分享一下

  2. Cesium调度------主要是地块数据的创建与更新 + image的请求与下载,这一块基本上是最复杂的,需要完全理清楚cesium的逻辑,其中需要把一些不必要的逻辑停掉,尽量最大化性能(比如pick肯定要停掉,因为pick肯定是自己的引擎去实现,一定要明确一个思路,只用cesium地球调度,所以无关紧要的全部停掉,使性能达到最优)。当然中间还需要停掉cesium很多逻辑,如attribute的创建和上传(cesium直接创建buffer了这可不行,浪费内存啊),贴图sample对象的创建等等

  3. 渲染-----这时候我们拿到当前帧要渲染的地块tile(上面有imagery, data等各种信息),然后就去创建mesh挂到我们自己的scene下面执行渲染。每帧先清空地球节点下面的所有mesh,然后这帧cesium告诉我们渲染哪些就去挂上去(这样做完全是模拟cesium的drawCommand逻辑)。当然这里要做好mesh的缓存,material的缓存,texture的缓存以及各种uniform对象的缓存(其实这里还有个比较核心的就是地球的shader,这个其实还是比较复杂的,后续再花时间补上吧)

哎,说个小插曲,刚开始做的时候我理清楚了这个逻辑,但是由于我们这边渲染引擎的渲染比较复杂,导致我cesium的更新没有在引擎渲染之前(我一直以为是执行的上述逻辑,哎),然后一直出现缩小两边闪白块的问题(其实白块就是因为地球没有渲染,那一块白色的其实是大气。我还曾经一度怀疑是cesium将前后几帧的图全渲到targetBuffer然后混合出的结果那,但是实验了一下发现不是。最后又找了自己的原因,原来是先执行渲染了,相当于比cesium快了一帧)

下面详细说一下这三步如何实现的: 1.第一步 frameState的更新: 查看一下frameState这个类,看看里面都需要传入哪些信息,这一步应该比较容易,当然pick,depth,postProcess,creditDisplay(证书),shadow这些逻辑我们不需要(如果需要当然是用自己的引擎实现,不需要额外的开销)。fog的话其实可以使用cesium的逻辑,计算出fog的浓度,但是我这边自己实现了fog,所以也禁掉了fog的逻辑。 第一步核心就是摄影机和视锥,具体信息如下 frameState.camera = { positionWC:frameState.positionWC, positionCartographic: frameState.positionCartographic, directionWC:frameState.directionWC, frustum:frameState.perspectiveOffCenterFrustum, } frameState.cullingVolume = frameState.perspectiveOffCenterFrustum.computeCullingVolume(frameState.positionWC, cameraDirectionWC, cameraUpWC); (很多参数具体追一下cesium代码就知道如何计算的,不过要注意webgl引擎坐标系和cesium坐标系的转换)

2.第二步 cesium调度 首先要先理清cesium的地块渲染逻辑(这里由于篇幅原因不详细讲了,如果不清楚,cesium最长的一帧可以参考一下) 我们目前只是使用地块最核心的部分自己去手动调接口进行渲染调度, (1)首先创建QuadtreePrimitive (2)beginFrame: 接下来就可以可以开始调用它的接口手动更新了 直接调用:this._surface.beginFrame(this._frameState),这里主要是释放和创新创建0级的tile,不需要做更改 (3)render: 这块是比较核心的地方:直接调用this._surface.render(this._frameState); 在这里插入图片描述 这里把endUpdate干掉,其实我们的draw基本就是和endUpdate做一样的事情(生成drawComand,使用var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount;最终的这个tilesToRenderByTextureCount数据,从里面取我们的tile,imagery和terrian),当然和pick有关的逻辑一定要干掉,接下来就是调用selectTilesForRendering来获取当前需要创建的网格(不过这些可能需要对一些参数进行相应的更改,毕竟像context这种webgl的上下文我们不可能传入framestate中,当然render之后你如果需要使用cesium的截流或者别的功能也可以手动去更新) (4)endFrame:接下来就是最核心的部分:直接调用this._surface.endFrame(this._frameState);在processTileLoadQueue中,遍历更新队列中所有的GlobeSurfaceTile,解析tile数据,下载图片 在这里插入图片描述 测试发现updateHeighs在持续飞行很长时间会掉帧很厉害,这块我先注释掉了,没有细看,如果有知道的可以留言,谢谢 接下来就是 GlobeSurfaceTile.processStateMachine,这里会遍历所有tile如果状态是START就去prepareNewTile,如果是LOADING就去processTerrainStateMachine,这里处理地形的下载以及地块网格的生成逻辑,水面的生成。这部分cesium解析成功之后就直接去生成attribute了,我们要做的就是跳过attribute的逻辑,手动去标记地块(地形)网格ready的状态。 接下来就是图片的下载,这部分处理起来最复杂,我刚开始是用自己的引擎去下载图片资源,虽然没问题,但是就无法使用cesium的截流(可以设置每帧最大请求数,多余请求可以拦截下来,这样再持续飞行的时候能够解决内存泄露的问题)。所以建议还是使用cesium的下图逻辑,但是拦截掉sampler的创建就行了,不过注意需要绑定texture对象,因为cesium在没有下载到新图的时候回递归查找最近的图,这一点很重要,具体细节可以看TileImagery.prototype.processStateMachine的逻辑。 在这里插入图片描述reprojectTexture我们可以不需要,将其与createTexture合并在一起,然后重点修改createTexture的代码,将创建sampler的代码改成自己引擎生成texture的代码即可。(好了,cesium代码基本改造完了,对了,还有卸载,在cesium卸载tile和image的时候注意卸载你自己的资源就行了) 3最后一步:自己引擎的渲染逻辑,由于是大尺度下的渲染,所以引擎首先要实现logDepth。然后开始准备地球的shader(就是把cesium的shader看一遍,理解了就行了,不过注意一些坐标转换,当然这部分也有一定难度,后续可以分享一下)。然后就可以开始渲染逻辑了。专门为地球创建一个节点,然后每帧先清空,然后遍历this._surface.tileProvider._tilesToRenderByTextureCount去执行渲染,注意缓存tile和material,也要注意缓存uniform,这一块实现之后一个流畅的地球已经出现了,没想到完美实现一个hello world并不是那么容易 在这里插入图片描述 当然我们可以加入各种地图滤镜效果: 在这里插入图片描述