前言
马上要去上班了,这阵子雀食是啥都不想干,技能编辑器接Slate又费脑子,索性整点小活吧,想起来当前Moba 项目的血条间隔还是用虚拟列表做的,性能捉急,准备用Shader重新实现一下。
正文
所谓血条间隔就是在血条中使用分割线来帮助玩家快速计算自身生命值的,个人认为是一个非常优秀的设计,比如下面的大塔姆,玩家可以在一秒钟之内就可以知道其还有360左右的生命值
因为血条间隔宽度和数量与战斗中英雄最大生命值有关,所以根据战斗中的英雄最大血量计算得出每个血条间隔的宽度值,传入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流处理器线程挂起,浪费性能。(在这个血条示例中可能不需要这么麻烦,还牺牲了可读性,正常工程中需要考虑平衡性能和可读性,这里是学习用,就不管这么多了)
1 2 3 4 5 6 half result = step (PerSplitWidth - GapLineWidth, virtualHealthBarWidth % PerSplitWidth); clip(result - 1 );
注意最后的clip语句,还需要继续在它上面做文章,仔细看的话,LOL血条间隔并不是直接隔断的,而是会留一些空隙,这是有说法的,后文会提到,所以需要在上述clip需要变成
1 2 clip((result - 1 ) + (i.uv.y - 0.4 ));
接下来就是最关键的一点,大血条分隔,LOL中是1000血一个大分割,这个大分割是会完全隔断的,并且宽度是小分割的两倍,和前面那些小分割形成鲜明对比,更加明显
首先计算大分割线所处位置
1 2 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是不规范的,例如
这就需要我们去适配这种情况,原理也比较简单,就是记录UV起始位置和缩放系数,然后做规范化操作即可
成果
一个SRP Batch即可完成一个血条的所有分隔绘制,美中不足的是FGUI内置的渲染顺序优化导致没有办法一次SRP Batch绘制所有血条的分隔。
完整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 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 { 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; clip((result + secondResult - 1 ) + (i.uv.y - 0.4 + bigGapResult * 0.4 )); return BlackColor; } ENDHLSL } } }