共计 5314 个字符,预计需要花费 14 分钟才能阅读完成。
基于 UE4/Unity 绘制地图根底元素 - 线(上篇)
前言
上篇中记录了绘制线的根本流程,而下篇次要是对绘制线中遇到的性能和成果问题进行论述。在绘制完一条线并且心愿给其加上描边款式时,会遇到不可避免的闪动问题。而在绘制大量的交织路线时,须要同时思考绘制性能和闪动问题如何解决。本文总结了高效绘制描边线的办法,并对调研过的解决 Z -Fighting 闪动的计划进行论述。
像素圆角渲染的性能优化
在上篇中介绍了逐像素剔除产生圆角的办法,概括的来说,为了达到动静圆滑的目标,将原来 CPU 中的数学计算移入了片元着色器中进行。这样做尽管能失去最圆滑的成果,却也给 GPU 带来了压力。以圆角线帽代码为例,受 GPU 解决形式影响,动静分支的 if/else 指令须要被全副执行,同时 discard 指令也会影响 GPU 的 Early Z 优化,二者都会对性能产生影响。
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;
}
因而在片元着色器中指令的性能优化上,次要是将其逻辑改为线性,移除动静分支,并以 Alpha Blending 代替 discard。简化流程的次要工具是 CG 规范函数 step/clamp/lerp,其定义如下,灵活运用这些函数就能够躲避动静分支。
简化流程后的片元着色器代码如下,通过打消动静分支语句和 discard 指令缩小性能开销,就义局部代码的可读性,但晋升了并行效率。其中为了确定像素是否属于线帽结构了二次函数,实际上也能够结构其余类型的函数达到目标。
fixed4 frag (v2f i) : SV_Target
{
fixed4 clearColor = 0;
fixed isClear = 0;
fixed origin = clamp(i.geometryInfo.z, 0 ,1); // 两侧线帽 x 值膨胀到 0 和 1
fixed4 isCap = step(0, origin * (origin - 1)); // 构建二值函数,线帽为 1,线段为 0
fixed2 dist = fixed2(i.geometryInfo.z - origin, i.geometryInfo.w); // 构建间隔向量
isClear = step(1, dot(dist, dist)) * isCap; // 间隔小于 1(不须要剔除) 为 0,间隔大于等于 1(须要剔除) 且是线帽像素,则为 1
return lerp(i.color, clearColor, isClear);
}
绘制线的描边
依据上篇实现一条线的绘制后,为了使线易于察看,通常须要使得线具备描边款式。实际上,上篇中展现的线曾经为了好看都带上了描边,但要让线有描边局部还须要进行额定的绘制。
为了缩小顶点数减少并简化三角剖分的计算,通常是在绘制的填充线之下应用描边线宽进行一次同样的扩大绘制,描边线宽结构产生的面更大,使得两个线形成的面叠加展现就能够达到线描边的成果。这种计划的描边宽度为 (sideLineWidth – lineWidth) / 2。
描边线的基本原理如上所述,而在理论的绘制中能够针对填充线和描边线的个性,对渲染逻辑进行优化。在实践中次要进行了以下摸索:
1、提取变动点
能够看到描边线和填充线在绘制时的扩大方向是一样的,差异在于依据扩大向量扩大的线宽不同。因而能够将裁减顶点的计算抽离到顶点着色器中并行进行,数据处理时只计算裁减的基准向量,将其和线宽信息借助 uv 构造一起传入 shader 中,这样两局部的线就能够复用同一个 Shader 进行渲染。但两局部的线仍须要分两次进行绘制,耗费两个 Draw Call。
2、从数据上改良为一个 Draw Call 调用
基于顶点着色器的思考,两个线的绘制只有顶点地位和色彩的不同,因而能够模仿 Batching 操作,将两条线的 mesh 数据进行合并,就能够在一个 Draw Call 调用进行绘制。能够看到,在两个 mesh 的合并过程中只须要对三角形索引依据顶点数进行调整,其余的数据都能够间接合并。
public LineMesh CombineLineMesh(LineMesh appendMesh)
{
int index = this.vertices.Count;
for (int i = 0; i < appendMesh.triangles.Count; ++i)
{appendMesh.triangles[i] += index;
}
this.triangles.AddRange(appendMesh.triangles);
this.vertices.AddRange(appendMesh.vertices);
this.color32s.AddRange(appendMesh.color32s);
this.geometrys.AddRange(appendMesh.geometrys);
this.parameters.AddRange(appendMesh.parameters);
return this;
}
3、从绘制形式上改良为一个 Draw Call 调用
尽管摸索 2 中曾经达到了一个 Draw Call 进行渲染,然而描边线和填充线是应用两组顶点进行的渲染,本着能省则省的精力,为了缩小顶点数,能够思考在一组顶点中,依据描边线宽和填充线宽的比例信息,一次性绘制出整个线。这种做法须要利用上篇文章中为了绘制圆角引入的 geometry 信息,x 信息能够标识长度,而 y 值就能够作为宽度方向上的标识。若定义 ratio 为线宽的比值,则可依据片元着色器中 y 值的散布确定渲染色彩。
ratio = lineWidth / sideLineWidth
abs(y)∈[0,ratio] -> color
abs(y)∈(ratio,1] -> sideColor
这个计划能够只应用一组顶点绘制完描边线,但也存在一些问题:
1、在线帽和拐角的圆角反对上须要相似同心圆的绘制逻辑,须要再引入额定的条件判断,对逻辑复杂度和性能都有影响。
2、在绘制大量互相交织的线时,线的压盖程序须要动静的去调整,会遇到一部分交织线的所有填充局部要压盖所有描边局部,而一次性绘制的线是无奈撑持这一成果的。
综上,从绘制形式上的改良有其局限性,摸索 2 的绘制形式更为适合。
解决闪动 Z -fighting 问题
绘制计划确定当前,在绘制时遇到的下一个问题就是线的 Z -fighting 问题,即察看时线始终在闪动。其起因是描边线和填充线重叠局部所在的世界坐标完全一致,坐标转换后受深度缓冲精度影响导致片元在渲染时无序通过深度检测,最终体现为面的闪动问题。
Z-fighting 问题算是绘制线的最初一个阻碍,其中波及许多图形学的基础知识,在摸索解决方案的过程中也对渲染的全流程有了更多的意识,摸索的计划总结如下:
1、调整顶点的世界坐标
解决 Z -fighting 问题的第一步是定位出深度值抵触的对象。在绘制带描边的线这个场景中,导致闪动的起因是描边线和填充线的重叠局部世界坐标高度值统一,导致坐标转换后片元深度值统一。因而能够在抵触的面的高度值上减少一点儿偏移,通过扭转部分坐标影响转换后的深度值,最终能够看到闪动景象隐没。
依据后面的探讨,批改部分坐标的操作能够放在 Shader 中并行进行,以 Unity 为例,通过设置一个 priority 变量用于微调顶点 y 方向的偏移,从而管制显示的优先级。
fillLineMesh.priority = 1;
v2f vert (a2v v)
{
v2f o;
float4 pos = v.vertex + float4(v.parameter.x, 0, v.parameter.y, 0) * v.parameter.z; // 依据向量和线宽计算理论顶点地位
pos += float4(0, priority / 100, 0, 0); // 顶点 y 方向进行微调,须要把握微调大小
o.pos = UnityObjectToClipPos(pos);
o.color = v.color;
o.geometry = v.geometry;
return o;
}
这种形式能临时解决闪动问题,但在将摄像头地位拉远后仍会呈现。其起因是深度缓冲的精度无限,因而间隔摄像头越远须要的偏移量越大,微调的偏移量须要依据顶点和摄像头的间隔动静调控。在实际操作中,眼帘方向与顶点微调方向少数状况下并不相同,而在解决大量线重叠的 Z -fighting 时,大量偏移的累加可能会从视觉上察看到线不共面,与所有线在同一立体的地图展现形式不符,因而计划一通常仅作为初步验证 Z -fighting 起因的工具。
2、应用 Offset 指令
Unity ShaderLab 提供了微调偏移的 Offset 指令,指令定义和计算公式如下:
Offset Factor, Units
offset = m * factor + r * units
其中 m 是由零碎计算出的多边形深度斜率的最大值,多边形越是与近裁剪面平行,m 就越靠近于 0,r 是深度值可分辨的最小单位,是由零碎指定的常量。若多边形与裁剪面平行,则能够应用 factor=0,units= 1 的组合管制偏移,而对于与裁剪面有夹角的多边形,须要 factor 一起管制偏移量的大小,Offset 后果大于 0 会使得多边形远离近裁剪面进行偏移,具体的参数值须要实际过程中进行摸索确认。
应用 Offset 指令作用于裁剪空间的深度值能够解决多个 Object 之间的 Z -fighting 问题,但当为了缩小 Draw Call 将所有线合并为一个 mesh 后就无奈应用了,因而须要借助于其原理手动调控同一 mesh 中不同线的深度信息。
3、调整顶点的裁剪坐标
深度信息是在片元着色器之后计算失去的,因而无奈通过着色器的可编程局部间接更改。但深度信息是由裁剪空间的齐次坐标计算而来,因而能够通过操控裁剪空间坐标达到调整深度的目标。
在光栅化之前,坐标会进行模型 - 视图 - 投影变换由部分坐标转换为裁剪坐标,其中由察看空间经由投影矩阵变换失去的就是裁剪空间齐次坐标,其后转换为屏幕空间失去的 NDC 坐标 z 值由齐次坐标的 z / w 得来,决定了深度值。由察看空间坐标转换为裁剪坐标须要以下参数:
f:远裁剪面
n:近裁剪面
fov:视角
aspect:摄像机横纵比
设察看空间坐标为 ,
则转换到裁剪空间坐标为:
依据深度值规定,在裁剪坐标 z 值上增加 -z*offset 的偏移即可将深度向后微调 offset 大小。在 UE4 的 material 中,也能够通过调整 Pixel Depth Offset 达到偏移的成果。
v2f vert (a2v v)
{
v2f o;
o.pos = float4(UnityObjectToViewPos(float3(v.vertex.xyz)), 1.0);
float z = o.pos.z;
o.pos = mul(UNITY_MATRIX_P, o.pos);
o.pos.z = o.pos.z - z * v.parameter.z/1E8;// 应用 parameter.z 存储顶点偏移信息
return o;
}
4、调整深度检测
上述计划都是通过在不同的面之间结构渺小偏移来解决 Z -fighting 问题,而另一种思路是不减少偏移,通过指定渲染时的压盖规定,先绘制的面被后绘制的面压盖,最终显示出正确的图像。这种计划须要首先了解深度检测的概念。
深度检测在片元着色器之后进行,每个片元携带本身的深度值与深度缓冲内的深度值进行比拟检测,若检测通过,深度缓冲内的值将被设为该深度值。若检测失败,则抛弃该片元。Unity ShaderLab 应用 ZWrite 和 ZTest 两个指令管制这一过程:
- ZWrite 管制检测通过后,是否将片元深度写入深度缓冲,默认开启 (ZWrite On)
- ZTest 定义深度值通过深度检测的规定,默认是当片元深度值小于等于深度缓冲内的深度值时通过深度检测 (ZTest LEqual)
在绘制二维地图这一 case 中,不须要更改深度缓冲的写入策略,只须要将深度检测的策略改为全副通过即可:
ZWrite On
ZTest Always
小结
对于闪动问题,前三个摸索计划外围都是结构渺小偏移,若 fighting 的面数过多,造成渺小偏移大量叠加产生质变,可能会对图形的透视显示大小产生影响,这时举荐应用计划四。而对于多 Object 的状况,能够搭配计划二与计划四独特应用,成果更佳。
至此,曾经解决了绘制线的所有问题,下图应用各种纯色进行了道路线绘制,如果成果不称心,还能够尝试进行纹理贴图,使得道路线更加酷炫。
作者:程序员阿 Tu
链接:https://zhuanlan.zhihu.com/p/…
起源:知乎
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。