前言
上文提到了cesium中像素线段的实现——cesium抗锯齿像素线绘制,本文在以上实现前提下展开,如有对前文相关代码不熟悉的地方还请点击前面链接重新熟悉
本文实现的原理有参考此篇博客blog.csdn.net/Amesteur/ar… ,在此也感谢博主的分享
cesium虚线材质
熟悉cesium相关api的同学能够发现cesium中是有自带的虚线材质的,想看例子请点击此链接
在一般情况下,这种虚线是可以实现相关的业务需求的。但是在一些特定的GIS应用中,会需要同时渲染大量的虚线,指示线(铁路线,方向线),我们可以先观察一下它们的相同和不同之处。
- 铁路线
- 方向线
通过观察我们可以发现它们都是一段一段间隔交织组成的,比如虚线是一段有颜色一段透明,铁路线是一段白色一段红色,而方向线是一段蓝色一段箭头贴图,而不同之处便是间隔之中的填充不同,有的是空白透明,有的是颜色有的则是贴图,从这个方面思考我们是能够将三者结合起来的!
但是显然cesium自带的这种虚线材质是无法满足以上所有情况的,那接下来我们便需要实现一个自定义的primitive来支持以上多种情况,关于如何制作自定义的primitive网上的文档也比较多,此处就不多赘述这些细节了。
虚线/指示线实现原理
我们先假定后端会给我们提供一个数组,里面包含了需要绘制的线段的所有节点坐标,根据上一篇文章我们根据以上坐标渲染出了相应的像素线段,其中四个绿点是四个相应的节点,而且很有可能每一段的长度是不相等的尤其是在有弧度的线段上。 我们为了方便理解将其先转换成如下形式进行讲解原理 首先可以得知四个紫色圆点相当于上图的四个绿色节点,我们需要实现如上虚线或者指示线(铁路线,方向线)就需要均匀分段,假定我们根据长度markerDelta来进行分段,也就是蓝色竖线的地方,这样可以将如上一条线均匀的分为4段,每一段长度都是相同的这样就能保证虚实分段也能够均匀。
接着我们假定在距离每段起点为markerDelta/2的长度的地方设置uvDelta长度的范围作为渲染虚线、纹理、其他分段颜色的区域,这样我们也就能够实现相应的虚线或者指示线功能了。
贴图或者空白区域坐标的获取
有了这个思路我们就只需要在片元着色器中判断哪个位置在满足条件的区域就可以了。首先我们需要算出每个节点的坐标距离起点的距离,用米为单位比较合适。这个可以直接用Cartesian3.distance的方法算出距离然后依次从顶点着色器传到片元着色器中这边我们先记为uv中的u
然后我们假定每一段均匀分隔中满足条件的区域为dashLength,不满足条件的地方为fillLength,这两个当然是自己定的,比如我们需要实线10px然后连接着一个5px的空白来实现虚线,这样fillLength为10,dashLength则是5,这个时候单位以像素来计算这样对于纹理贴图来说更方便。这个时候markerDelta便是fillLength+dashLength的和了,而满足条件的区域则可以这么判断 x > markerDelta / 2 && x < markerDelta / 2 + dashLength
那么这个x的值如何求呢,这个时候上面传进片元着色器的uv中的u就可以派上用场了,因为此时片元着色器是线性插值,所以这个传入每个顶点的距离是会从起点也就是0线性递增的,那这个时候我们用一个求模函数很容易就能取到这个x了,也就是mod(uv.u,markerDelta),也就是如下的muvx
这里可以看出上面有个小问题,那就是单位不一致的问题,uv.u明明是以米为单位,怎么能对markerDelta这种像素单位进行取模运算呢?所以这个时候还要补充一个cesium公用着色器中自带的一个函数czm_metersPerPixel,它可以在顶点着色器中求得一个当前每像素代表多少米的值metersPerPixel,用这个再将markerDelta进行一次换算也就是markerDelta = markerDelta * metersPerPixel,此时markerDelta也转换成像素单位了。这样贴图或者空白区域的坐标就能轻易得到了,接下来就只需要进行贴图或者颜色的相关处理了,相关代码如下:
以下代码仅有此讲解部分的逻辑,关于像素线段的绘制逻辑麻烦移步上一篇文章 顶点着色器
....
varying float v_fillLength; // 填充区域像素长度 自定义
varying float v_dashLength; // 填充区域像素宽度 自定义
varying float v_metersPerPixel; // 当前状态下每像素代表多少米
varying vec2 v_uv; // u代表当前顶点距离起点的长度(单位米)外面用Cartesian3.distance计算
....
void main() {
...
vec4 positionEC = czm_modelViewRelativeToEye * czm_computePosition();
v_metersPerPixel = max(0.0, czm_metersPerPixel(positionEC));
...
}
片元着色器
....
varying float v_fillLength; // 填充区域像素长度 自定义
varying float v_dashLength; // 填充区域像素宽度 自定义
varying float v_metersPerPixel; // 当前状态下每像素代表多少米
varying vec2 v_uv; // u代表当前顶点距离起点的长度(单位米)外面用Cartesian3.distance计算
....
void main() {
....
// 每一段总长度
float markerdelta = (v_fillLength + v_dashLength) * v_metersPerPixel;
//总长度的一半
float halfd = markerdelta / 2.0;
// uv.u对markerDelta取模求出其所在间隔中的所占的长度
float muvx = mod(v_uv.u, markerdelta);
// 在虚线范围内则贴图或者虚线
if(muvx >= halfd && muvx <= halfd + v_dashLength) {
.... // 此处进行相关贴图或者颜色处理操作
}
...
}
纹理的处理
关于纹理如何进行处理这个很简单,因为绘制像素线顶点着色器中每个顶点会传入两次,这样我们第一次uv.v设置成0.0,第二次设置成1.0这样就能按照uv进行纹理贴图,如果对上述我说的贴图知识点不是很了解,可以再重温一下webgl纹理贴图相关的文档,接下来我们再完善下片元着色器 片元着色器
....
varying float v_fillLength; // 填充区域像素长度 自定义
varying float v_dashLength; // 填充区域像素宽度 自定义
varying float v_metersPerPixel; // 当前状态下每像素代表多少米
varying float v_type; // 渲染什么类型的线 0.0虚线 0.5 铁路线 1.0方向线段
varying vec2 v_uv; // u代表当前顶点距离起点的长度(单位米)外面用Cartesian3.distance计算
....
void main() {
....
vec4 c = color; // 默认颜色
// 每一段总长度
float markerdelta = (v_fillLength + v_dashLength) * v_metersPerPixel;
//总长度的一半
float halfd = markerdelta / 2.0;
// uv.u对markerDelta取模求出其所在间隔中的所占的长度
float muvx = mod(v_uv.u, markerdelta);
// 在虚线范围内则贴图或者虚线
if(muvx >= halfd && muvx <= halfd + v_dashLength) {
// 此处进行相关贴图或者颜色处理操作
if (v_type == 0.0) { // 虚线 此时此区域不着色
discard;
} else if (v_type == 0.5) { // 铁路线 此时设置为另外颜色即可
c = vec4(0.0, 0.0, 0.0, 1.0); // 此颜色也可以传入
} else { // 纹理贴图
float u = (muvx - halfd) / v_dashLength; // 算出贴图uv.u
vec4 imageC = texture2D(image, vec2(u, v_uv.v)); // 拿到纹素
c.xyzw = mix(c, imageC, imageC.w); // 纹理颜色与线段颜色融合
}
}
vec4 fragColor = czm_gammaCorrect(c);
gl_FragColor = fragColor;
...
}
以上就是自定义虚线和指示线的相关实现原理,效果如下
此功能实现可以满足多个场景需要并且能将此类线段合并draw call进行批处理大大提升性能,当然如果有同学能够有性能更好或者实现方法更优的方案也欢迎评论区交流!
尾记
webgl需要探索的领域很广很深,我也是在不断的学习其他优秀开源作品的源码,如果有同行欢迎一起交流学习经验。现阶段项目正忙,目前还有一些除webgl外的其他领域内容没有时间去输出,以后有时间在慢慢填坑吧!