UE4/Unity绘制地图基础元素-线(上篇)

前言

这篇文章是使用游戏引擎探索地图可视化的开篇。传统的地图渲染通常是在iOS/Android/Web平台进行的,为了探究更酷炫的地图展示,会记录基于UE4/Unity进行地图渲染的探索过程。

地图基础元素 - 线

线作为地图渲染的基本元素,在地图中可以代表各种形式的道路。道路数据通常以离散点串形式存储,因此如何将点串绘制成有宽度的线是渲染最关注的问题。本文记录了绘制有宽度的线的方法,并对优化线展示效果的各种线帽和拐角进行了阐述。

绘制有宽度的线

道路数据通常以离散点串和其对应线宽进行存储,为了在游戏引擎中进行显示,就需要将其扩展为有宽度的线。UE4和Unity都可以使用代码生成Mesh进行基本图元的渲染展示(UE4使用Procedural Mesh Component,Unity使用MeshFilter和MeshRenderer),而Mesh渲染的基本单位是三角形,因此问题就转化为如何根据点串和线宽,构造出一组三角形使其能够拼合产生具有宽度的线。

对于只有两个点的直线,通过获取与直线垂直的向量,向两个方向各扩展lineWidth/2长度产生顶点,划分为三角形即可。

image

而对于多个离散点构成的线,绘制的时候遇到2个问题:

  • 仅使用相邻点计算垂直向量,导致扩充出的线拐角处会有断裂,如下图所示。可以看到,仅仅每个相邻线段进行扩充是不够的,还需要考虑如何处理线的拐角。
image
  • 考虑处理线的拐角,但获取顶点扩充向量的方向和大小不对,导致绘制的线不等宽。下图根据相隔顶点连线的垂线确定扩充向量,但因向量随顶点位置变化而变化,因此不能作为生成等宽线的依据。
image

有了上面的思考,任务就变成了扩充出等宽且有拐角的线:相隔点的顶点位置会变化,但由其确定的向量方向是不变的,因此依靠顶点两侧线段的单位向量,就能确定出唯一的扩充向量。确定扩充方向后,还需要确定扩充向量的大小使得最终的线等宽。

image

伪代码如下,扩充方向可由线段单位向量组合确定,需要注意扩充长度并不是lineWidth/2,而是需要根据线段夹角进行计算调整。扩充向量计算好之后,即可根据离散点串生扩充顶点,根据顶点坐标剖分为三角形,构建Mesh进行渲染。

// 计算扩充方向
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)  // 根据角度调整扩充长度

绘制线帽LineCap

根据上一节操作已经可以绘制出有宽度的线,但也能够看出线在开头和结尾处都是矩形,不够优雅美观。因此本节主要会解决绘制线帽的问题。

较为常用的LineCap主要有以下三种:

  • Butt 无线帽模式,上一节绘制的线默认即为Butt
  • Round 在线的两端添加额外的半圆,其半径为lineWidth/2
  • Square 在线两端添加额外的矩形,其高度为lineWidth/2
image

Square形式的线帽绘制较为简单,只需要在开头和结尾部分根据延伸方向额外添加矩形即可,两个矩形可以很简单的划分为四个三角形,添加在画线mesh中一同渲染。而Round形式的半圆线帽在绘制上就麻烦了许多,在实践过程中主要探索了以下三个方案:

1、使用三角形近似绘制半圆

最直观的方式就是直接绘制半圆线帽,但是渲染的最小单元是三角形,因此只能通过添加多个三角形近似表示半圆。这种方式需要根据添加三角形的个数,进行几何运算确定各个顶点坐标,通过三角形组合成半圆,虽然方法直观可行,但为了使线帽圆滑,额外添加的较多顶点和进行的大量数学运算都会对性能带来影响,存在性能和效果的取舍。

image
2、使用图片近似绘制半圆

第二种方案借助图片可以省去添加额外顶点和进行数学计算的步骤,近似得到半圆线帽。

image

图片工具大小为16×16像素,左右两部分分别绘制半圆和矩形。对于半圆部分,内部点透明度设置为1,圆弧上覆盖的像素点,通过调低透明度值弱化锯齿感,圆弧之外部分则将透明度设置为0,整体使用透明度构建出近似的半圆。矩形部分则作为工具,用于填充非线帽部分。

这种方案在构建线Mesh时,与Square线帽方案一致,但需要将纹理uv值也与顶点进行绑定。Square线帽额外添加的矩形绑定图片左侧半圆的uv,而原有线部分绑定右侧矩形uv即可。渲染时,可以在片元着色器中逐像素提取到映射的图片颜色值,输出颜色使用顶点原色,但透明度值采用图片的透明度值,从而将圆弧外侧像素剔除。使用该方案需要开启透明度混合,从而不显示圆弧外侧像素。

这种方案也是半圆的近似表示,在距离较近观察时会出现圆弧线帽发虚,原因是受限于图片大小,如果增加图片大小可以缓解问题,但也会增加开销,也需要做性能和效果的取舍平衡。

3、逐像素绘制半圆

第三种方案由方案二演进而来,不是使用图片剔除像素,而是借助于半圆的特性,在片元着色器中剔除所有不满足条件的像素,做到绘制像素级的半圆线帽。其主要原理是在添加Square线帽后,判断渲染时像素距离线起始顶点距离,若超过lineWidth/2(即红色部分)则剔除像素,从而逐像素绘制出半圆线帽。

image

像素剔除会在片元着色器中并行进行,效率高但无法存储上下文信息,而剔除逻辑需要获取圆心信息,同时片元着色器的坐标已经转化为裁剪空间的齐次坐标,无法进行几何运算,因此需要将一些辅助信息传递到片元着色器中进行操作。

辅助信息定义为二维向量geometryInfo,其含义为顶点在线中的相对位置,点串的起点作为(0,0),终点作为(1,0),中间的点根据距离转化为[0,1]间的数值。根据扩充向量得到的顶点,则根据扩充方向,向量y值赋值为1或-1。因为已经人为定义了线宽为2的相对坐标系,因此线帽上顶点的辅助信息x值可以转化为-1和2,这样任何小于0和大于1的x值都可以表示该点是线帽部分,而且可以很方便的和(0,0)、(1,0)做距离计算,并与半圆半径1进行比较。

image

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;
 }

使用该方案生成的圆角,在近距离观看时因为线帽的渲染像素增多,因此也不会产生虚化或者锯齿感,能够得到圆滑的效果。

image

绘制线拐角LineJoin

线帽已经圆润优雅之后,同时也发现绘制的线在一些极端情况下拐角会存在bad case。例如下图所示,对于夹角较小的线会产生非常大的尖角;而对于线段呈直角情况显示的也同样是直角拐角,不够圆润美观。本节主要会解决绘制线拐角的问题。

image

较为常用的LineJoin主要有以下三种:

  • Miter 尖角样式,上一节绘制的线即属于Miter
  • Bevel 切角样式,以横切面替代尖角
  • Round 圆角样式,以圆弧替代尖角
image

有了扩充线和线帽的绘制经验,从上图可以看出Bevel和Round样式不需要根据线段夹角计算扩充向量。绘制时按照矩形扩展后,Bevel样式只需要根据扩充顶点补齐一个三角形构成切面。而对于Round样式,除了起终点外,每一个顶点扩充处根据矩形方向绘制两个半圆,叠加就能达到圆拐角效果。

image

半圆部分的绘制原理和绘制半圆线帽一样,添加矩形再剔除多余像素,因此需要将geometryInfo扩充为四维向量,后两位表示顶点在当前段的相对位置,同样在片元着色器中进行像素剔除。这里片元着色器的代码逻辑与圆角线帽类似,不再赘述。最终的拐角效果如下图。

image

整体的绘制流程可以简单总结为下图,等宽线作为线渲染的主体,线帽/拐角作为线渲染的效果优化项。在具体实践中,可以通过设置配置项的方式方便的更改线帽/拐角的样式。

image

作者:程序员阿Tu

链接:https://zhuanlan.zhihu.com/p/266026334

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335