Unity3D 基于shader和Image的雷达图

最近在项目中有需求在UI中增加一个雷达图。首先考虑到使用BaseMeshEffect通过OnPopulateMesh中计算顶点位置实现,但由于项目已上线,增加cs脚本无法热更新支持,因此考虑在shader中实现,并将材质的一些属性暴露出来,供lua控制修改。

最终得到效果是这样的:

img

一开始的想法是在片段着色器中计算,通过比较某个点是否在多边形内来控制输出颜色,而多边形的各个顶点是通过雷达图各维度计算出来的。这么一来在frag中需要进行大量的计算、if...else判断和大量的for循环。

调整思路,将雷达图的各个维度分解为一个一个的三角形,三角形中心重叠在一起且中心角的角度相等(等分了360度),于是决定在vert阶段做手脚,将一个由两个三角形组成的Image,通过修改顶点绘制成为一个三角形,且三角形的顶角角度、相邻两边长度都受shader的属性控制,大概的示意图如下:

img

实现过程中的一些要点如下:

  1. 绘制Image时,可以通过uv坐标判断出当前绘制的是哪个顶点;

  2. 使用PositionAsUV1组件传递个顶点相对于轴点的坐标;

  3. 利用简单的三角函数计算,将Image的四个顶点的坐标表示成角度和长度的函数;

  4. 片段着色器中,根据像素距离边缘的尺寸处理透明度;

以下详细说明:

Image顶点的判断

因为是一个正方形,很容易根据纹理uv判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
v2f vert(appdata_t IN)     
{
if(IN.texcoord.x > 0.5f && IN.texcoord.y < 0.5f)
{
// 右下角
}
else if(IN.texcoord.x < 0.5f && IN.texcoord.y > 0.5f)
{
// 左上角
}
else if(IN.texcoord.x > 0.5f && IN.texcoord.y > 0.5f)
{
// 右上角
}
else
{
// 左下角
}
return OUT;
}

position的传递

借助UGUI的组件PositionAsUV1。此处传到TEXCOORD1position是相对于轴点Pivot的坐标,而vertex

1
2
3
4
5
struct appdata_t     
{
float4 vertex : POSITION;
// ...
};

是顶点在Canvas上的坐标。

修改顶点位置

这个是最重要的过程了。这里有四个输入参数:

  • ValueStart : 起始侧的数值,范围0-1,对应起始边的长度;

  • ValueEnd : 终止侧的数值,范围0-1,对应终止边的长度;

  • AngleStart : 起始角度,从此角度开始绘制三角形;

  • AngleRange : 范围角度,三角形从起始角度开始持续的角度;

具体参见下图:

img

需要得到的四个点的坐标,也标注在图上了,左上角、右上角、左下角、右下角以此记为lt、rt、lb、rb。除了lb不受影响之外,原来的lt、rb及rt都需要经过一些变换,表示成这前边四个参数的函数,图上表示为lt’、rb’和rt’。主要处理lt’和rb’,因为rt可处理为lt和rb的中点。

img

在处理各个顶点时,要注意传入的vertexposition也是对应定点的值。因此必须分别处理四个顶点,最后将处理完成的顶点赋值给vertex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
float AngleEnd = _AngleStart + _AngleRange;

if(IN.texcoord.x > 0.5f && IN.texcoord.y < 0.5f)
{
// 右下角
IN.vertex.x -= IN.position.x;
IN.vertex.x += (IN.position.x * cos(radians(_AngleStart)))*_ValueStart;
IN.vertex.y += sin(radians(_AngleStart)) * IN.position.x * _ValueStart;
}
else if(IN.texcoord.x < 0.5f && IN.texcoord.y > 0.5f)
{
// 左上角
IN.vertex.y -= IN.position.y;
IN.vertex.x += (IN.position.y * cos(radians(AngleEnd)))*_ValueEnd;
IN.vertex.y += sin(radians(AngleEnd)) * IN.position.y * _ValueEnd;

}
else if(IN.texcoord.x > 0.5f && IN.texcoord.y > 0.5f)
{
// 左下 作为基准
float2 lb = IN.vertex - IN.position;

// 左上
float2 lt = lb;
lt.x += (IN.position.y * cos(radians(AngleEnd)))*_ValueEnd;
lt.y += sin(radians(AngleEnd)) * IN.position.y * _ValueEnd;

// 右下
float2 rb = lb;
rb.x += (IN.position.x * cos(radians(_AngleStart)))*_ValueStart;
rb.y += sin(radians(_AngleStart)) * IN.position.x * _ValueStart;

// 右上是中点
IN.vertex.xy = 0.5f * (lt + rb);
}
else
{
// 左下角了
}

像素透明度计算

截止上一步,已经可以绘制出各个三角形了,但是目前这些图形使用的都是统一的颜色,视觉效果不佳。现在给雷达图增加描边效果。

img

原理是在顶点着色器中,保存顶点到雷达外部边缘的距离edge,实际上除了左下角的点之外,另外三个顶点的edge值都是0。左下角的点需要计算点到对边的距离。考虑到各个三角形的_AngleRange都是一样的,为了简化计算,做了近似处理,直接使用两个边长的乘积:

1
edge = _ValueStart * _ValueEnd;

接下来在片段着色器中获取到插值后的edge,使用smoothstep()来计算alpha值,edge小于0时将alpha取1,edge小于0.8时将alpha取0:

1
half alpha = smoothstep(0.8, 1.0, 1.0 - IN.edge);

然后将参数alpha值和step的阈值参数化:

1
half alpha = _InnerAlpha + (1 - _InnerAlpha) * smoothstep( 1.0 - _EdgeThickness, 1.0, 1.0 - IN.edge);

最终效果和相关代码参见这里

REFERENCE