这篇文章是使用游戏引擎探索地图可视化的开篇。传统的地图渲染一般是在iOS/Android/Web平台进行的,为了探究更酷炫的地图展现,会记录基于UE4/Unity进行地图渲染的探索过程。程序员
线做为地图渲染的基本元素,在地图中能够表明各类形式的道路。道路数据一般以离散点串形式存储,所以如何将点串绘制成有宽度的线是渲染最关注的问题。本文记录了绘制有宽度的线的方法,并对优化线展现效果的各类线帽和拐角进行了阐述。markdown
道路数据一般以离散点串和其对应线宽进行存储,为了在游戏引擎中进行显示,就须要将其扩展为有宽度的线。UE4和Unity均可以使用代码生成Mesh进行基本图元的渲染展现(UE4使用Procedural Mesh Component,Unity使用MeshFilter和MeshRenderer),而Mesh渲染的基本单位是三角形,所以问题就转化为如何根据点串和线宽,构造出一组三角形使其可以拼合产生具备宽度的线。工具
对于只有两个点的直线,经过获取与直线垂直的向量,向两个方向各扩展lineWidth/2长度产生顶点,划分为三角形便可。oop
而对于多个离散点构成的线,绘制的时候遇到2个问题:性能
有了上面的思考,任务就变成了扩充出等宽且有拐角的线:相隔点的顶点位置会变化,但由其肯定的向量方向是不变的,所以依靠顶点两侧线段的单位向量,就能肯定出惟一的扩充向量。肯定扩充方向后,还须要肯定扩充向量的大小使得最终的线等宽。优化
伪代码以下,扩充方向可由线段单位向量组合肯定,须要注意扩充长度并非lineWidth/2,而是须要根据线段夹角进行计算调整。扩充向量计算好以后,便可根据离散点串生扩充顶点,根据顶点坐标剖分为三角形,构建Mesh进行渲染。spa
// 计算扩充方向
Vec2f a = (P1 - P0) * normalized()
Vec2f b = (P2 - P1) * normalized()
Vec2f avg = a + b
Vec2f direction = Vec2f(-avg.y, avg.x).normalized() //扩充方向为avg的垂直方向
// 计算扩充长度
float t = Abs(Asin(a × b)) / 2 // 单位向量叉乘得到夹角正弦
float length = lineWidth / 2 / Cos(t) // 根据角度调整扩充长度
复制代码
根据上一节操做已经能够绘制出有宽度的线,但也可以看出线在开头和结尾处都是矩形,不够优雅美观。所以本节主要会解决绘制线帽的问题。3d
较为经常使用的LineCap主要有如下三种:code
Square形式的线帽绘制较为简单,只须要在开头和结尾部分根据延伸方向额外添加矩形便可,两个矩形能够很简单的划分为四个三角形,添加在画线mesh中一同渲染。而Round形式的半圆线帽在绘制上就麻烦了许多,在实践过程当中主要探索了如下三个方案:orm
最直观的方式就是直接绘制半圆线帽,可是渲染的最小单元是三角形,所以只能经过添加多个三角形近似表示半圆。这种方式须要根据添加三角形的个数,进行几何运算肯定各个顶点坐标,经过三角形组合成半圆,虽然方法直观可行,但为了使线帽圆滑,额外添加的较多顶点和进行的大量数学运算都会对性能带来影响,存在性能和效果的取舍。
第二种方案借助图片能够省去添加额外顶点和进行数学计算的步骤,近似获得半圆线帽。
图片工具大小为16×16像素,左右两部分分别绘制半圆和矩形。对于半圆部分,内部点透明度设置为1,圆弧上覆盖的像素点,经过调低透明度值弱化锯齿感,圆弧以外部分则将透明度设置为0,总体使用透明度构建出近似的半圆。矩形部分则做为工具,用于填充非线帽部分。
这种方案在构建线Mesh时,与Square线帽方案一致,但须要将纹理uv值也与顶点进行绑定。Square线帽额外添加的矩形绑定图片左侧半圆的uv,而原有线部分绑定右侧矩形uv便可。渲染时,能够在片元着色器中逐像素提取到映射的图片颜色值,输出颜色使用顶点原色,但透明度值采用图片的透明度值,从而将圆弧外侧像素剔除。使用该方案须要开启透明度混合,从而不显示圆弧外侧像素。
这种方案也是半圆的近似表示,在距离较近观察时会出现圆弧线帽发虚,缘由是受限于图片大小,若是增长图片大小能够缓解问题,但也会增长开销,也须要作性能和效果的取舍平衡。
第三种方案由方案二演进而来,不是使用图片剔除像素,而是借助于半圆的特性,在片元着色器中剔除全部不知足条件的像素,作到绘制像素级的半圆线帽。其主要原理是在添加Square线帽后,判断渲染时像素距离线起始顶点距离,若超过lineWidth/2(即红色部分)则剔除像素,从而逐像素绘制出半圆线帽。
像素剔除会在片元着色器中并行进行,效率高但没法存储上下文信息,而剔除逻辑须要获取圆心信息,同时片元着色器的坐标已经转化为裁剪空间的齐次坐标,没法进行几何运算,所以须要将一些辅助信息传递到片元着色器中进行操做。
辅助信息定义为二维向量geometryInfo,其含义为顶点在线中的相对位置,点串的起点做为(0,0),终点做为(1,0),中间的点根据距离转化为[0,1]间的数值。根据扩充向量获得的顶点,则根据扩充方向,向量y值赋值为1或-1。由于已经人为定义了线宽为2的相对坐标系,所以线帽上顶点的辅助信息x值能够转化为-1和2,这样任何小于0和大于1的x值均可以表示该点是线帽部分,并且能够很方便的和(0,0)、(1,0)作距离计算,并与半圆半径1进行比较。
geometryInfo绑定在每一个顶点传入shader后,会在片元着色器中按像素进行线性插值,所以每个像素都会得到一个能够标识本身局部位置的辅助信息,借助于该信息进行距离判断就能够进行像素剔除,这里展现的是Unity Shader代码,UE4能够在Material中还原逻辑。
fixed4 frag (v2f i) : SV_Target
{
if(i.geometryInfo.x < 0) // 起点侧线帽
{
if(dot(float2(i.geometryInfo.x, i.geometryInfo.y), float2(i.geometryInfo.x, i.geometryInfo.y)) > 1)
{
discard; // 距离圆心距离大于1则剔除
}
}
else if(i.geometryInfo.x > 1) // 终点侧线帽
{
if(dot(float2(i.geometryInfo.x - 1, i.geometryInfo.y), float2(i.geometryInfo.x - 1, i.geometryInfo.y)) > 1)
{
discard;
}
}
return i.color;
}
复制代码
使用该方案生成的圆角,在近距离观看时由于线帽的渲染像素增多,所以也不会产生虚化或者锯齿感,可以获得圆滑的效果。
线帽已经圆润优雅以后,同时也发现绘制的线在一些极端状况下拐角会存在bad case。例以下图所示,对于夹角较小的线会产生很是大的尖角;而对于线段呈直角状况显示的也一样是直角拐角,不够圆润美观。本节主要会解决绘制线拐角的问题。
较为经常使用的LineJoin主要有如下三种:
有了扩充线和线帽的绘制经验,从上图能够看出Bevel和Round样式不须要根据线段夹角计算扩充向量。绘制时按照矩形扩展后,Bevel样式只须要根据扩充顶点补齐一个三角形构成切面。而对于Round样式,除了起终点外,每个顶点扩充处根据矩形方向绘制两个半圆,叠加就能达到圆拐角效果。
半圆部分的绘制原理和绘制半圆线帽同样,添加矩形再剔除多余像素,所以须要将geometryInfo扩充为四维向量,后两位表示顶点在当前段的相对位置,一样在片元着色器中进行像素剔除。这里片元着色器的代码逻辑与圆角线帽相似,再也不赘述。最终的拐角效果以下图。
总体的绘制流程能够简单总结为下图,等宽线做为线渲染的主体,线帽/拐角做为线渲染的效果优化项。在具体实践中,能够经过设置配置项的方式方便的更改线帽/拐角的样式。
做者:程序员阿Tu
连接:zhuanlan.zhihu.com/p/266026334
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。