前言

马上要去上班了,这阵子雀食是啥都不想干,技能编辑器接Slate又费脑子,索性整点小活吧,想起来当前Moba项目的血条间隔还是用虚拟列表做的,性能捉急,准备用Shader重新实现一下。

正文

所谓血条间隔就是在血条中使用分割线来帮助玩家快速计算自身生命值的,个人认为是一个非常优秀的设计,比如下面的大塔姆,玩家可以在一秒钟之内就可以知道其还有360左右的生命值

image-20210704102739106

因为血条间隔宽度和数量与战斗中英雄最大生命值有关,所以根据战斗中的英雄最大血量计算得出每个血条间隔的宽度值,传入Shader进行计算绘制,所以需要进行一些换算,现有以下条件

  • 血条宽度为107px
  • 英雄最大生命值为3700
  • 血条每格代表100生命值

首先是血条间隔的数量,为 3700/100 = 37 个,而这37个间隔要均匀分布在107px中,所以每个间隔的宽度为100 / 37 = 2.70,注意,是100,不是107,因为我们绘制血条间隔与血条UI自身的宽度完全无关,根本原因是我们在Shader中会使用uv.x * 100的方式来构建一个虚拟的血条,所以不论英雄最大生命值是多少,都要除100。

1
2
3
4
5
6
PerSplitWidth("分割块宽度:",float) = 10
GapLineWidth("分割线宽度:",float) = 3
[HideInInspector]
BlackColor("BlackColor",Color) = (0,0,0,1)

half virtualHealthBarWidth = i.uv.x * 100;

然后我们需要用一条黑线来标识一个血条间隔,具体来说就是在一个血条间隔的结尾处绘制指定宽度的黑色, 这里使用step来绕过if-else分支,因为分支语句会导致GPU流处理器线程挂起,浪费性能。(在这个血条示例中可能不需要这么麻烦,还牺牲了可读性,正常工程中需要考虑平衡性能和可读性,这里是学习用,就不管这么多了)

图自《全局光照技术:从离线到实时渲染》,我们的血条Shader如果使用if语句,就会成为下图中的b情况,十分拉跨

1
2
3
4
5
6
// 血条结尾处才绘制黑色线条
half result = step(PerSplitWidth - GapLineWidth, virtualHealthBarWidth % PerSplitWidth);
// call discard
// if ZWrite is Off, clip() is fast enough on mobile, because it won't write the DepthBuffer, so no GPU pipeline stall(confirmed by ARM staff).
// 以每100生命值作为间隔
clip(result - 1);

注意最后的clip语句,还需要继续在它上面做文章,仔细看的话,LOL血条间隔并不是直接隔断的,而是会留一些空隙,这是有说法的,后文会提到,所以需要在上述clip需要变成

1
2
//留出0.4比例的空隙
clip((result - 1) + (i.uv.y - 0.4));

接下来就是最关键的一点,大血条分隔,LOL中是1000血一个大分割,这个大分割是会完全隔断的,并且宽度是小分割的两倍,和前面那些小分割形成鲜明对比,更加明显

image-20210704105758699

首先计算大分割线所处位置

1
2
// 之所以需要执行virtualHealthBarWidth + PerSplitWidth,是为了避免第一个分隔符会被当成大分隔符的情况(此种情况还会导致结果不正确)
half bigGapResult = step((virtualHealthBarWidth + PerSplitWidth) / PerSplitWidth % 10, 1);

然后计算双倍宽度的位置

1
2
half secondResult = step(PerSplitWidth - GapLineWidth * 2,
virtualHealthBarWidth % PerSplitWidth) * bigGapResult;

最后的clip以及输出颜色

1
2
clip((result + secondResult - 1) + (i.uv.y - 0.4 + bigGapResult * 0.4));
return SplitColor - float4(1, 1, 1, 0);

适配图集

其实对于UGUI来说是不需要这一步的,因为我们没有进行任何的贴图采样操作,所以UV就是默认的0~1,但是FGUI默认导出的时候是有图集的,就导致UV是不规范的,例如

image-20210704152321597

这就需要我们去适配这种情况,原理也比较简单,就是记录UV起始位置和缩放系数,然后做规范化操作即可

成果

一个SRP Batch即可完成一个血条的所有分隔绘制,美中不足的是FGUI内置的渲染顺序优化导致没有办法一次SRP Batch绘制所有血条的分隔。

image-20210704154936787

完整Shader代码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//Code from https://blog.csdn.net/cyf649669121/article/details/82117638
Shader "NKGMoba/LifeBarGap"
{
Properties
{
PerSplitWidth("分割块宽度:",float) = 10
GapLineWidth("分割线宽度:",float) = 3
[HideInInspector]
BlackColor("BlackColor",Color) = (0,0,0,1)
UVFactor("UV缩放系数", float) = 1
UVStart("UV起始点", float) = 0
}
SubShader
{
// No culling or depth
Cull Off
ZWrite Off
ZTest Off
Blend SrcAlpha OneMinusSrcAlpha

Tags
{
"Queue" = "Transparent"
"RenderType" = "Transparent"
}

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

CBUFFER_START(UnityPerMaterial)

float PerSplitWidth;
float GapLineWidth;
half4 BlackColor;
float UVFactor;
float UVStart;

CBUFFER_END

v2f vert(appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = v.uv;
return o;
}

half4 frag(v2f i) : SV_Target
{
half virtualHealthBarWidth = (i.uv.x - UVStart)*100 * UVFactor;
half result = step(PerSplitWidth - GapLineWidth, virtualHealthBarWidth % PerSplitWidth);

half bigGapResult = step((virtualHealthBarWidth + PerSplitWidth) / PerSplitWidth % 10, 1);

half secondResult = step(PerSplitWidth - GapLineWidth * 2,
virtualHealthBarWidth % PerSplitWidth) * bigGapResult;
//return half4(secondResult,0,0,1);
// call discard
// if ZWrite is Off, clip() is fast enough on mobile, because it won't write the DepthBuffer, so no GPU pipeline stall(confirmed by ARM staff).
// 以每100生命值作为间隔
clip((result + secondResult - 1) + (i.uv.y - 0.4 + bigGapResult * 0.4));
return BlackColor;
}
ENDHLSL
}
}
}