GIS融合之路(二)CesiumJS和ThreeJS深度缓冲区整合
摘要: 上一篇文章里简单介绍了山海鲸中城市大师为了整合GIS系统所做的技术选型的探索,最终我们决定采用先后绘制的形式在单个Canvas上整合山海鲸的3D引擎和CesiumJS。那有同学要问了,如果一个先画,一个后画,后画的不就把先画的覆盖了吗?这里我们就要学到深度缓冲区的概念了。 本文内容基于山海鲸可视化软件操作,您可先免费下载山海鲸可视化后再阅读本文。 下载山海鲸可视化软件
在这篇文章开始前再次重申一下,山海鲸并没有使用ThreeJS引擎。但由于ThreeJS引擎使用广泛,下文中直接用ThreeJS同CesiumJS的整合方案代替山海鲸中3D引擎和CesiumJS整合。
系列传送门:
山海鲸可视化:GIS融合之路(一)技术选型CesiumJS/loaders.gl/iTowns?
文章开始之前大家可以看下这个视频当中山海鲸中CesiumJS与山海鲸深度整个的结果,图片中展示了Cesium的地形和山海鲸中的水面的整合,这个过程中就有一个完整的深度缓冲区的整合:
具体内容可以移步完整的教程查看: GIS地形编辑-山海鲸可视化视频教程
上一篇文章里简单介绍了山海鲸中城市大师为了整合GIS系统所做的技术选型的探索,最终我们决定采用先后绘制的形式在单个Canvas上整合山海鲸的3D引擎和CesiumJS。那有同学要问了,如果一个先画,一个后画,后画的不就把先画的覆盖了吗?这里我们就要学到深度缓冲区的概念了。
深度缓冲度也称之为DepthBuffer,是GPU为了对光栅化渲染时物体的遮挡关系进行排序用到的概念。概念本身很简单,就是每绘制一个物体的同时,把这个物体在每一个像素点上的深度信息与这个像素点之前的深度信息进行对比,如果这个像素点的深度较小(注意这要看具体深度缓冲的DepthFunction,一般在WebGL上默认是最大的是Depth是1,因此越小越近)则继续渲染像素颜色,否则直接丢弃。
有了深度缓冲区,问题就变得简单了,让CesiumJS先画,山海鲸引擎后画。只要保证深度写入和深度测试都是默认打开的,那不就万事大吉了。Done,下班!
等等,好像有问题;等等,好像有不少问题!等等,好像完全不行….
问题可是真不少,咱还得一关一关的过啊:
1.CesiumJS默认用的LogarithmicDepth,而普通的3D引擎默认用的是LinearDepth
按说这也不是什么大问题,CesiumJS支持修改Scene上的logarithmicDepthBuffer改成linearDepth,Threejs这类也基本都实现了LogarithmicDepth,因此不是大问题。不过由于CesiumJS一般都是大场景和超大场景,改成Linear的话一定会有严重的Z-Fighting,而ThreeJS这类主要是小场景,改成LogarithmicDepth,又会导致在近景部分depth精度不足。当然希望近景和远景同时完美,本身在技术上也是鱼和熊掌的问题,我们暂时不去深入解决这个问题。
2.CesiumJS默认的渲染方式是距离切段后逐段渲染
这条就非常坑了,CesiumJS默认会将整个相机裁切空间(近平面到远平面之间的空间)分成多段,然后逐段渲染。这样做的好处显而易见,可以进一步拓展Depth的利用率,非常适合CesiumJS这种知道所有模型的位置且不会有体积超出范围的大模型的情况。然后每次分段绘制结束之后,depth的信息就会被清除,导致最初规划的深度缓冲度整合的方案完全无法使用(除非关闭这个分段绘制机制),只能采用新的方案。
3.CesiumJS绘制过程无法嵌入
CesiumJS绘制过程机制及其复杂,想要找到一个合适的时机将ThreeJS这类引擎的绘制过程嵌入进去非常困难,而且也没有对应的接口,写起来对CesiumJS代码侵入性极强,后续CesiumJS升级时很难跟随升级,为将来的可维护性留下很深的隐患。
综合这三个问题,最终决定不再让CesiumJS直接绘制到Canvas上,而且采用CesiumJS提供的PostProcessStage接口将整个绘制的ColorBuffer和DepthBuffer都存入FrameBuffer当中,在ThreeJS中再将这两个FrameBuffer转换为WebGLRenderTarget。通过这两种方式就可以拿到CesiumJS的绘制结果。
将CesiumJS的绘制结果转换为两个Texture之后,就要在ThreeJS端绘制进去。这个过程类似PostProcess的过程,但是要先做。这里参考CesiumJS中的ViewportQuad接口,在ThreeJS中创建一个PlaneGeometry,设置一个ShaderMaterial,在VetexShader中,将四个点对齐到整个视口的四个角上,实现代码非常简单。
void main() {
gl_Position = vec4( position.x, position.y, 1., 1. );
}
在FragmentShader当中读入ColorBuffer和DepthBuffer
uniform sampler2d czmColorSampler;
uniform sampler2d czmDepthSampler;
ColorBuffer直接写入,非常简单,depth如何写入呢。
首先我们要明确我们从CesiumJS拿到的是什么Depth,查看Cesium源码可以发现,我们拿到的depth是LogarithmicDepth被映射到0~1之间之后被pack在rgba四个通道上之后的结果。因此我们首先要对CesiumJS的depth进行unpack,并且根据相机的near和far将depth恢复到相机空间的z距离。拿到这个距离之后为了方便存储,山海鲸目前的做法是将这个z距离再在自己引擎的相机中的near和far做一次映射,算出线性的0~1的depth,这样就可以和自己引擎中拿到的depth一致了,当然为了方便存储,也pack到rgba中区。最终得到的结果存入czmDepthSampler,具体结果如下图所示:
这个结果理论上就可以直接和ThreeJS里的相机空间的depth进行对比了,但是注意我们这里并不打算认为对比,而是希望用GPU自己的深度缓冲区测试,这个怎么做呢。这里就要用到Shader的深度写入功能。一般来说GPU在拿到vetexShader中的gl_Position之后会自己把得到的坐标转换到NDC空间中,并进一步将depth映射到0~1之间,再存入depth buffer。
WebGL中转换前和转换后坐标
我们需要在FragmentShader中接管这个功能,WebGL也提供的接口:gl_FragDepth。(但是要特别注意的是,一旦开启Shader的缓冲区写入,GPU的early-Z优化就会自动关闭,所有的像素点着色都会进行,因此不是我们这种迫不得已的情况,还是尽量不要用的。)有的这些只是,我们只需要最后将线性空间的depth模拟GPU的计算过程,转换为ndc空间的depth写入就可以了。
我们通过以上方式正式将Cesium的渲染过程并入了山海鲸引擎的渲染过程当中,当然这中间还要处理很多gl state状态的问题,不过不管怎样,最难的一步已经越过去了,剩下的就是对目前的机制进行完善,防止状态冲突问题即可。但是深度整合成功是不是Cesium就整合完成了呢,正式成为了山海鲸可视化的一部分?答案显然是否定的,现在的Cesium除了遮挡正常了以外,相机还没有同步,一旦移动,就会发现完全对不上位置。另外除了相机起码还有3个比较大问题:1.阴影的整合 2.光照的整合 3.G-buffer管线数据的同步。别急,我们一步一步来,后面的文章逐个给大家展开这些问题的处理。