webGL 如何绘制dash array(虚线),有宽度的道路等地图中的样式?
时间:2024-01-16 13:24:01 | 来源:网站运营
时间:2024-01-16 13:24:01 来源:网站运营
webGL 如何绘制dash array(虚线),有宽度的道路等地图中的样式?:可视化研发之线的画法:直线、曲线和动画(WebGL篇)
背景介绍项目的背景源自我们的自研图表库的目标,由于市面上的众多图表库在扩展等方面不满足我们如今众多产品的需求,我们需要一套灵活、高效、可扩展性强的图形语法来适应各种业务场景。所以我们就开始了自研图表库的进程,由于WebGL相较于Canvas出色的性能(在某些场景中),我们的渲染层提供了基于WebGL的渲染方式。
在研发过程中有总结了很多实践(线段绘制、文本渲染、反走样、高效的cache与顶点等属性的merge策略等)。那么今天先来唠唠我们是如何使用WebGL进行线段绘制的。
摘要本文将介绍基于GPU的直线绘制方案,只需要在CPU中将计算好的路径点传入,动态的在GPU中计算出指定粗细,line cap,line dash的线段,并且具有用户体验良好的动画。该方案具有较强的灵活性,对于给定路径的线段,不用为了线段的效果重复生成线段的顶点数据。
绘制普通的线段
绘制一条1px粗细线段我们需要两步,给出线段的几个顶点,然后在这些顶点之间插值画线就行了。WebGL中提供了线段这种基本图元(GL_LINES),但是GL_LINES只能绘制1px的线段,那么绘制超过1px像素的线段我们又该如何绘制呢?
并且即使是1px就可以直接使用GL_LINES绘制吗?好像也不行,我们的渲染库需要支持指定分辨率渲染,如果用户指定2倍分辨率渲染,那么显示在界面上的画面是1024*1024,而需要渲染的画面大小是2048*2048,所以在指定分辨率的情况下,包括位置、大小等属性都需要等比缩放,所以即使用户看到的是1px线段,在缓存中也可能超过1px大小。所以我们需要支持指定粗细的线段绘制。
基于法线绘制动态粗细的线段
针对超过1px大小的线段,我们就需要使用其他的方式进行渲染,这里介绍我们采用的方式,如图1.1所示,也就是将每段线段拆分为上下两个三角形进行渲染:
图1.1: 基于法线的渲染(图片引自
https://mattdesl.svbtle.com/drawing-lines-is-hard)
对于上下顶点的计算方式,如图1.2所示,其实就是计算p1处的角平分线。然后沿着角平分线扩充:
图1.2: 计算交点的角平分线(图片引自:
https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader)
以上方案就可以在GPU中渲染出普通的指定粗细的线段,这不是本文重点,就不赘述了,具体可以参考
https://mattdesl.svbtle.com/drawing-lines-is-hard这篇博客。
这里只做简单介绍,如图1.3所示的路径p0 p1 p2,需要将路径扩充为指定粗细的线段,也就需要计算扩充后的线段边界的顶点p0_0、p0_1、p1_0、p1_1、p2_0、p2_1
图1.3:指定粗细的线段
这样只需要传入线段路径,就可以计算出指定粗细大小的线段中的各个顶点了。我们直接将角平分线作为向量传入,并传入线段粗细,就可以直接在顶点着色器中动态计算出指定粗细的线段了。
图1.4:绘制指定粗细的线段
如何绘制线段Cap上面介绍了如何使用GPU绘制动态粗细的线段,接下来我们来聊聊如何绘制带有line cap的线段,line cap表示线段首尾的样式,它是线段的额外样式,不受线段长度影响。
如图2.1所示,在Canvas2d中,line cap表现出的是额外长度,所以我们只需要在线段的首尾动态添加line cap即可。
图2.1:canvas 2d中的line cap
绘制square cap我们来聊聊如何绘制square的cap,square cap的长度取决于线段粗细,而线段粗细用上面的算法是在GPU里动态渲染的,所以square cap我们最好也需要在GPU里动态渲染,那么cap的长度就需要在GPU里计算出来,计算方式非常简单,我们可以使用上面的方式通过向量计算:
如图2.2所示,图中蓝线就是cap中需要用到的向量,令该向量为n0
图2.2:计算square cap
绘制round cap上面介绍了如何绘制square cap,对于round cap就是将cap变为圆形即可,这里GPU需要知道以下信息:
- 这个像素是线段体中的像素还是cap中的像素(cap中的像素需要计算round)
- 对于cap中的像素,需要知道圆心的位置
对于判断是线段体中的像素还是cap中的像素,有很多方法,我们采取的是使用距离的方式判断(这主要是因为后面的策略也要用到距离)
图2.3:距离
如图2.3所示,线段总长度为200,起始的cap区间长度始终为0,终止的cap区间长度始终为200,这样只需要判断这个像素的距离属性就行。对于圆心的位置,就更好判断了,因为左边cap区间传入顶点着色器的顶点和p0相同,所以直接传入片元着色器就行。
如何绘制虚线上面我们介绍了如何绘制普通线段以及如何绘制线段Cap,那么虚线该如何绘制呢?在Canvas中虚线通过传入line dash数组(
https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/setLineDash)进行绘制。在WebGL中我们使用与Canvas2d相同的接口,并且为了更强的灵活性,我们希望在GPU中计算虚线。
计算虚线我们需要以下信息:
- 当前像素在线段中的位置(用来判断是实心区域还是空心区域,如果是空心区域再判断是否是在cap区域内)
- line dash信息
如何绘制普通虚线虚线绘制我们需要知道每个像素的位置或者说距离起点的距离,所以传入图2.3所示的距离,这也是为什么在绘制round cap时我们要舍近求远传入距离了,因为虚线正好可以使用这个数据。算法比较简单,我们已知当前像素与起点的距离distance,又已知line dash数组:
line dash总周期长度:
当前像素在该周期中的位置:
然后判断该位置pos在在line dash数组中是在实心区间还是空心区间即可。
如何绘制具有square cap属性的虚线
上面介绍了绘制普通虚线,对于square cap的虚线需要在每个实心区间两侧添加square cap,由于首尾我们已经自动添加了square cap,所以我们只讨论中间的线段区域。
通过上面的计算,我们已知像素在当前周期中的位置pos,我们在计算square cap时只需要将实心区间左右两侧扩充线段宽度即可。
如何绘制具有round cap属性的虚线上面介绍了如何绘制square cap,对于round cap我们需要计算出圆形,如图2.5所示,我已知距离边界的u坐标(距离),但是我们需要额外的信息来计算v坐标,通过uv,计算得到round的半径。
图2.5:round cap虚线
图2.6:vCoord参数
如图2.6所示,我们给每个顶点再传入个vCoord参数,这样在v轴方向上就可以得到当前像素在v轴上的距离
通过以上uv数据,就可以计算出圆角:
图2.7:虚线绘制
漂亮的线段动画与漂亮的虚线上面介绍了如何绘制线段、line cap、line dash,但是上面的渲染方式在虚线以及动画上具有怪异的渲染效果,如图3.1所示,在多段折线的时候虚线就会出现这种怪异的效果:
图3.1:多段折线虚线
如图3.2所示,在绘制动画的时候,也会出现怪异的效果:
图3.2:线段动画
如图3.3所示,中间的红线是线段的路径,左右边界是我们绘制有宽度的线段时放大的边界,我们将中间的长度同时给了外边界导致上下边界具有相同的长度属性,但是很明显上边界的真实长度比下边界小,这就导致了如上图所示的斜线dash:
图3.3:上下边界实际长度不同,但计算时都使用的是中间红线的长度
用户希望看到的是两边平行的效果,所以我们需要计算出两个边距真实的距离,如图3.4所示:
图3.4:边界的长度
我们目前已知的变量包括:
- 线段的几个顶点(在生成顶点数据的时候就已知)
- 线段的线宽(生成顶点数据时未知,渲染时动态传入)
因为我们采用的是动态渲染各种粗细的线段,所以在生成线段顶点以及属性的时候我们是不知道这个线段到底是要渲染多宽的,所以这里我们引入变量delta:
图3.5:通过delta计算边界的实际长度
如图3.5所示,根据上面的计算,我们已知端点的法向量n,该端点的方向向量d,那么delta就是法向量在方向向量上的投影:
并且由于向量n的长度表示线段粗细,所以投影长度delta的也可以用线段粗细表示,在着色器里的通过delta*lineWidth就可以得到delta所表示的真实长度。线段的总长度直接取上下边界中最长的即可。
如图3.6和3.7所示的就是比较好看的动画和虚线效果了。
图3.6:绘制虚线
图3.7:绘制线段动画
原文链接: