WebGL中有宽度的线一直都是初学者的一道门槛,由于在windows系统中底层的渲染接口都是D3D提供的,因此不管你的lineWidth设置为多少,最终绘制出来的只有一像素。即便在移动端能够设置有宽度的线,可是在拐弯处原生api没有作任何处理,因此每每达不到项目需求,再者好比对于虚线、导航线的绘制,原生api是无能为力。差很少从事WebGL开发已经一周年,总结一下绘制线的方法和踩过的坑,聊以慰藉后来者。html
宽度线的绘制最核心的思想就是利用三角形来绘制线,将一根有宽度的线,当作是多个三角形的拼接ios
将线剖分红三角形的过程是一个计算密集型的过程,若是放在主线程中会阻塞渲染形成卡顿,一般来说都是放到顶点着色器中来处理,利用GPU并行计算来处理。一般来着色中,将顶点沿着法线方向平移lineWidth/2的距离。对于一个顶点只能平移一次,因此在cpu中咱们须要把一个顶点复制两份传给gpu同时提早肯定好剖分出来的三角形的顶点索引顺序。web
对于拐弯处,须要作一系列的计算来肯定拐角的距离,好比:算法
但这幅图过于复杂,我比较喜欢下面这个比较简单的图canvas
假设dir1为向量last->current的单位向量,dir2为向量current->next的单位向量,根据这两个向量求出avg向量,avg向量 = normalize(dir1 + dir2);将avg向量旋转九十度便可求出在拐角处的偏移向量,固然这个向量可向下,也能够向上,因此通常对上文中重复的顶点还有对应的一个side变量,来告诉着色器应该向下仍是向上偏移,一样上面图中的last和next也要传入对应上一个和下一个顶点的坐标值。对应的着色器代码:windows
// ios11下直接使用==判断会有精度问题致使两个数字不相同引出bug ' if( abs(nextP.x - currentP.x)<=0.000001 && abs(nextP.y - currentP.y)<=0.000001) dir = normalize( currentP - prevP );', ' else if( abs(prevP.x - currentP.x)<=0.000001 && abs(prevP.y - currentP.y) <=0.000001) dir = normalize( nextP - currentP );', // ' if( nextP.x == currentP.x && nextP.y == currentP.y) dir = normalize( currentP - prevP );', // ' else if( prevP.x == currentP.x && prevP.y == currentP.y ) dir = normalize( nextP - currentP );', ' else {', ' vec2 dir1 = normalize( currentP - prevP );', ' vec2 dir2 = normalize( nextP - currentP );', ' dir = normalize( dir1 + dir2 );', '', '', ' }', '', ' vec2 normal = vec2( -dir.y, dir.x );',
原理上面已经实现,那么在具体的绘制中,咱们还要明白一个问题,lineWidth的单位是什么,若是你须要绘制的是以像素为单位,那么咱们就须要将3d坐标映射到屏幕坐标来进行计算,这样绘制出来的线不会有明显的透视效果,即不会受相机距离远近的影响。api
咱们须要几个函数来帮忙,第一个是transform函数,用来将3D坐标转换成透视坐标系下的坐标:iphone
'vec4 transform(vec3 coord) {', ' return projectionMatrix * modelViewMatrix * vec4(coord, 1.0);', '}',
接下来是project函数,这个函数传入的是透视坐标,也就是通过transform函数返回的坐标;ide
'vec2 project(vec4 device) {', ' vec3 device_normal = device.xyz / device.w;', ' vec2 clip_pos = (device_normal * 0.5 + 0.5).xy;', ' return clip_pos * resolution;', '}',
其中第一步device.xyz / device.w将坐标转化成ndc坐标系下的坐标,这个坐标下,xyz的范围所有都是-1~1之间。函数
第二步device_normal * 0.5后全部坐标的取值范围在-0.5~0.5之间,后面在加上0.5后坐标范围变为0~1之间,因为咱们绘线在屏幕空间,因此z值无用能够丢弃,这里咱们只取xy坐标。
第三部resolution是一个vec2类型,表明最终展现canvas的宽高。将clip_pos * resolution彻底转化成屏幕坐标,这时候x取值范围在0~width之间,y取值范围在0~height之间,单位像素。
接下来的unproject函数,这个函数的做用是当咱们在屏幕空间中计算好最终顶点位置后,将该屏幕坐标从新转化成透视空间下的坐标。是project的逆向过程。
'vec4 unproject(vec2 screen, float z, float w) {', ' vec2 clip_pos = screen / resolution;', ' vec2 device_normal = clip_pos * 2.0 - 1.0;', ' return vec4(device_normal * w, z, w);', '}',
因为屏幕空间的坐标没有z值和w值,因此须要外界传入。
最终着色器代码:
上面介绍了有宽度线的绘制,可是在一些地图场景中,每每须要绘制虚线、地铁线以及导航路线等有必定规则的路线。这里主要介绍导航线的绘制,明白这个后虚线以及地铁的线绘制就很简单了。首先介绍一下导航线的核心原理,要绘制导航线咱们有几个问题须要解决,好比:
不管是虚线、地铁线、导航线均可以用这个图来表达。咱们能够规定每一个markerDelta米在halfd(halfd = markerDelta/2)到uvDelta长的距离里绘制一个标识(虚线的空白区域,地铁线的黑色区域、导航线的箭头)。那么问题来了如何让每个像素都清楚的知道本身应该成为线的哪一部分?这个时候个人方案是求出每一个顶点距离起始坐标点的 ~距离/路线总长度~,将这个距离存入纹理坐标中,利用纹理坐标的插值保证每一个像素都能均匀的知道本身的长度占比;在着色器中乘以路线总长度,算出这个像素距离起始点距离uvx。uvx对markerDelta取模运算得muvx,求出在本间隔中的长度,在根据规则(if(muvx >= halfd && muvx <= halfd + uvDelta))计算这个像素是否在uvDelta中。对于导航线,咱们须要从箭头图片的纹理中取纹素,因此该像素对应的真正的纹理坐标是float s = (muvx - halfd) / uvDelta;对应着色器代码为
float uvx = vUV.x * repeat.x;', ' float muvx = mod(uvx, markerDelta);', ' float halfd = markerDelta / 2.0;', ' if(muvx >= halfd && muvx <= halfd + uvDelta) {', ' float s = (muvx - halfd) / uvDelta;', ' tc = texture2D( map, vec2(s, vUV.y));', ' c.xyzw = tc.w >= 0.5 ? tc.xyzw : c.xyzw;', ' }',
最终完整着色器代码为:
关于markerDelta和uvDelta来讲,则须要跟相机距离、纹理图片性质等因素来综合计算,好比在个人项目中的计算法方式:
let meterPerPixel = this._getPixelMeterRatio(); let radio = meterPerPixel / 0.0746455; // 当前比例尺与21级比例尺相比 let mDelta = Math.min(30, Math.max(radio * 10, 1)); // 最大间隔为10米 let uvDelta = 8 * meterPerPixel;// 8是经验值,实际要根据线实际像素宽度、纹理图片宽高比来计算 uvDelta = /*isIOSPlatform() ? 8 * meterPerPixel : */parseFloat(uvDelta.toFixed(2)); this.routes.forEach(r => { if (r._isVirtual) { return; } r._material.uniforms.uvDelta = {type: 'f', value: uvDelta};// 暂时取一米 r._material.uniforms.markerDelta = {type: 'f', value: mDelta}; });
另外一个问题如何绘制有边框的线,能够在着色器中来控制,好比设定一个阈值,超过这个阈值的就绘制成border的颜色;或者简单点也能够把一条线绘制两遍,宽的使用border的颜色,窄的使用主线的颜色,同时控制两条线的绘制顺序,让主线压住border线。
首先发如今iphone6p 10.3.3中纹理失真;
纹理失真确定是设备像素与纹理纹素没有对应,可是为何没有对应呢?纹理失真就是uv方向上对应问题,为了排查这个过程我把只要落在纹理区域的范围都设置成红色,发如今纵向方向上无论纹理在什么尺度下红色区域范围都是同样的,并且结合图片发现纵向上基本覆盖了整个纹理图片,因此纵向没有问题。
那么就是横向上的取值,问题,可是横向是经过纹理坐标产生的,没有计算的内容;最后怀疑到数字精度问题;将其中的mediump改为highp;这个问题获得解决;iphone6上能画出完美的箭头
'precision mediump float;',
然而又碰到了另外一个很是棘手的问题,iphone7以上的设备箭头周围有碎点。。。
首先要搞清楚这些碎点是什么,发现不论换那张图片都有碎点,一开始我觉得这些碎点是纹理坐标计算时的精度问题,后来发现不论怎么调整纹理u的取值范围都没法作到在任什么时候刻彻底避免这个问题。
最后偶然发现改变一下这个等式就能解决问题。
因此确定这个些碎点确定是从纹理中取得的,有可能在这个区域内,Linear过滤模式恰好取得了几个像素的平均值,致使这里的alpha通道非是0.0同时取到了必定的平均颜色才会显示这些碎点;最后怀疑这是由于mipmap方式致使这个设备像素恰好落到先后两章图片的像素上,综合差值后获得一个碎点;至因而否是跟mipmap有关还须要后续验证,因为项目时间关系先往下解决。解决完这个问题已是凌晨四点多
然而又出现了另外一个问题,iphone6下在某些角度下,纹理会消失,发现是由于上面的判断引发的
将阈值范围改为可以解决问题,后续这块须要梳理一下,做为一个外部可传入的变量来处理
如今的线并无对端头作处理,也就是没有没有实现lineCap效果,若是想知道lineCap的实现原理能够看个人这篇文章:WebGL绘制有端头的线。
http://codeflow.org/entries/2012/aug/05/webgl-rendering-of-solid-trails/
https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader
Drawing Antialiased Lines with OpenGLhttps://www.mapbox.com/blog/drawing-antialiased-lines/