【shader】Unity/Laya圆形进度条的实现

摘要

实现一个圆环形的进度条,通过一个取值范围是0到1的值来确定进度。

索引

摘要

索引

正文 

简介

使用透明贴图实现

最简单的方法

改进:计算宽度

完全计算实现

在Shader Forge中

在Unity中直接编写

移植到LayaAir中

参考文献

正文 

简介

实现这种环形的进度条有两个主要问题,第一是将图片显示成一个透明的圆环,第二是控制要显示的扇形的圆心角。

这里每一个问题都可以有两类实现方案:使用图片采样或者实时的数学计算。这两种方案可以看做是同一个操作的两种不同优化方向。

事实上在shader编写和特效的实现过程中,有很多操作都可以在这两个优化方向上选择,以达到平衡负载的目的。使用图片、常数或顶点色来保存一些数据更加直观,对计算性能的要求很低,不随计算复杂度的增加而增加,缺点是占用空间更大、会有精度限制而且结果固定,无法实现高解析度、平滑过渡、动态改变等功能;使用数学方法实时计算得到数据,可以解决上述问题,不会占用存储空间,但是会增加CPU/GPU的运算负载,如果是十分复杂的运算则会消耗大量时间,在占用面积很大的片段着色器中执行可能会导致帧率明显下降等问题,而且寻找和修正这些复杂算法本身就会增大开发难度。所以具体在项目中选择哪种方案还是要根据项目的性能要求和实际情况灵活选择。

下面将具体介绍这两种不同的实现方案。

使用透明贴图实现

最简单的方法

使用透明贴图就是使用一张同时包含环形和角度Alpha值的纹理,角度渐变的方向和进度条的走向一致,然后使用Value值进行AlphaTest,剪裁掉透明区域。随着Value值的变化,进度条被剪裁掉的部分也越来越多,由此达到进度变化的目的。

使用到的纹理如下

【shader】Unity/Laya圆形进度条的实现_第1张图片

这是最简单的方法,使用任意一个支持AlphaTest的shader都能完成。

这种方法存在两个弊端:

第一是AlphaTest的阈值是对全图生效的,也就是说表现环形用的镂空也会被一并剪裁,而这些地方在作图的时候往往都存在一个羽化效果,边界并不是严格的01二值化,这就导致调整进度变化的同时环形的边缘也会有细微变化。

第二是由于是对纹理进行采样,在纹理尺寸较小的时候剪裁边缘会出现比较明显的锯齿。使用效果图如下

【shader】Unity/Laya圆形进度条的实现_第2张图片

改进:计算宽度

下面通过计算uv值剪裁来动态调整圆环的宽度,同时也避免了边缘的抖动。

本文所使用的网格是默认的Quad,大部分计算都放在了片段着色器中。

首先通过模型uv得到片段在uv空间的坐标,这个坐标是从0~1的,我们要把它映射到-1~1(线性映射就是求直线解析式y=kx+b然后解方程组)。这样uv坐标的原点就到了中心位置,通过点乘即可得到每个片段到中心的距离的平方了。

float2 uv11 = i.uv * 2 - 1;
float uvLength2 = dot(uv11,uv11);

然后声明两个属性,外半径和内半径,使用1-得到正确的方向,它和距离相加后向下取整,得到一个01值,距离不足指定半径的=0,超过的=1。1-这个值给它取反,表示距离超出半径的=0,不足的=1,这样内外相乘即可得到环形区域(相乘=1的部分)。

_MaxRadius("MaxRadius", Range(0,1)) = 1
_MinRadius("MinRadius", Range(0,1)) = 0.5

float uv_final = (1-floor(uvLength2 + (1-_MaxRadius))) * floor(uvLength2 + (1-_MinRadius));

把这个值与固定值0.5做剪裁,再加入常规的AlphaTest再次剪裁得到最终效果。再次剪裁的目的的避免Value值影响环形。

fixed4 col = tex2D(_MainTex, i.uv);
clip(uv_final - 0.5);
clip(_Value - col.a);
return col;

【shader】Unity/Laya圆形进度条的实现_第3张图片

完整代码如下

Shader "A/RoundProgressBar_1"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		//_Color("Color", Color) = (1,1,1,1)
		_MaxRadius("MaxRadius", Range(0,1)) = 1
		_MinRadius("MinRadius", Range(0,1)) = 0.5
		_Value("Value", Range(0,1)) = 1
	}
	SubShader
	{
		Tags 
		{ 
			"RenderType"="Opaque"
		}

		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

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

			sampler2D _MainTex;
			float4 _MainTex_ST;

			//float4 _Color;

			float _MaxRadius;
			float _MinRadius;
			float _Value;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}
			 
			fixed4 frag (v2f i) : SV_Target
			{
				//重映射
				float2 uv11 = i.uv * 2 - 1;
				//得到每个片段到中心的距离
				float uvLength2 = dot(uv11,uv11);
				//计算环形范围
				float uv_final = (1-floor(uvLength2 + (1-_MaxRadius))) * floor(uvLength2 + (1-_MinRadius));

				fixed4 col = tex2D(_MainTex, i.uv);
				clip(uv_final - 0.5);
				clip(_Value - col.a);
				return col;
			}
			ENDCG
		}
	}
}

完全计算实现

为了避免出现锯齿或者被纹理占用过多空间,我们可以使用完全数学计算的方法来得到这个图形。

在Shader Forge中

【shader】Unity/Laya圆形进度条的实现_第4张图片它来源于SF的官方教程 http://www.youtube.com/watch?v=VnBNBMfk9HM

在Unity中直接编写

我们已经实现了画圆环,下面只要计算出扇形角度就可以了。我们在得到重映射的uv后使用`atan2(y,x)`函数进行计算。

atan2(y,x) 计算y/x 的反正切值。实际上和 atan(x)函数功能完全一样,至少输入参数不同。atan(x) = atan2(x, float(1))

得到一个绕中心点逆时针增加的函数值,取值范围是-π~π,它和一个阈值相减即可实现剪裁。这个阈值就是最终的进度值Value,只不过Value是0~1的,要把它映射到-π~π才能保证值的匹配。我们对其向上取整得到整数部分,再用1-除去不足1(被Value剪裁掉)的部分。

float angle = 1 - ceil(atan2(uv11.g,uv11.r) - (6.2831 * _Value - 3.1415));

最后是剪裁,因为两个要被剪裁的值都取0,所以相乘剪裁一次0.5即可。

clip(uv_final*angle - 0.5);

效果:

【shader】Unity/Laya圆形进度条的实现_第5张图片

这个开口是向左的,想要得到向上的开口,并且避免给物体增加旋转,就要在shader中对uv坐标进行一次旋转。旋转要按照先平移再旋转的规则进行。

_RotAngle("RotAngle (degree)", Range(0,360)) = 270
_RotCenter("RotCenter (XY)", Range(0,1)) = 0.5

float Rote = (_RotAngle * 3.1415926)/180;
float sinNum = sin(Rote);
float cosNum = cos(Rote);
//将uv中心平移至旋转中心
float2 uv01 = i.uv - float2(_RotCenter,_RotCenter);
//计算旋转之后的坐标,需要乘旋转矩阵,然后返回原位
uv01 = mul(uv01,float2x2(cosNum,-sinNum,sinNum,cosNum)) + float2(_RotCenter,_RotCenter);

最终效果

【shader】Unity/Laya圆形进度条的实现_第6张图片

完整代码

Shader "A/RoundProgressBar_2"
{
	Properties
	{
		//_MainTex ("Texture", 2D) = "white" {}
		_Color("Color (RGBA)", Color) = (1,1,1,1)
		_MaxRadius("MaxRadius", Range(0,1)) = 1
		_MinRadius("MinRadius", Range(0,1)) = 0.5
		_Value("Value", Range(0,1)) = 1
		_RotAngle("RotAngle (degree)", Range(0,360)) = 270
		_RotCenter("RotCenter (XY)", Range(0,1)) = 0.5
	}
	SubShader
	{
		Tags 
		{ 
			"RenderType"="Opaque"
		}

		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

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

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

			//sampler2D _MainTex;
			//float4 _MainTex_ST;

			float4 _Color;

			float _MaxRadius;
			float _MinRadius;
			float _Value;
			float _RotAngle;
			float _RotCenter;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}
			 
			fixed4 frag (v2f i) : SV_Target
			{
				float Rote = (_RotAngle * 3.1415926)/180;
				float sinNum = sin(Rote);
				float cosNum = cos(Rote);
				//将uv中心平移至旋转中心
				float2 uv01 = i.uv - float2(_RotCenter,_RotCenter);
				//计算旋转之后的坐标,需要乘旋转矩阵,然后返回原位
				uv01 = mul(uv01,float2x2(cosNum,-sinNum,sinNum,cosNum)) + float2(_RotCenter,_RotCenter);

				//重映射
				float2 uv11 = uv01 * 2 - 1;

				float angle = 1 - ceil(atan2(uv11.g,uv11.r) - (6.2831 * _Value - 3.1415));
				// float angle = 1 - clamp(atan2(uv11.g,uv11.r) - (_Value-0.5)*8,0,0.5);
				// float angle = 1 - (atan2(uv11.g,uv11.r) - (_Value-0.5)*8);

				//return fixed4(0,angle,0,1);

				//得到每个片段到中心的距离
				float uvLength2 = dot(uv11,uv11);
				//计算环形范围
				float uv_final = (1-floor(uvLength2 + (1-_MaxRadius))) * floor(uvLength2 + (1-_MinRadius));
				// clip(uv_final - 0.5);

				//颜色剪裁
				// clip(angle - 0.5);
				clip(uv_final*angle - 0.5);
				return (min(1,angle+1) * _Color);
			}
			ENDCG
		}
	}
}

移植到LayaAir中

注意事项:

  1. 所有参与运算的整数都要转换成float,如1.0
  2. 旋转角度是90度而不是270度,坐标系轴向不同
  3. cg里面的`atan2`函数对应glsl的`atan`
  4. glsl没有clip函数,需要手动调用discard
  5. `customMaterial.alphaTest`和`customMaterial.alphaTestValue`并没有什么用,估计只是用来给`u_AlphaTestValue`传值,实测customMaterial.alphaTest = false;也能进行alphatest。所以如果阈值固定,那也就不需要`u_AlphaTestValue`了

 

顶点着色器

attribute vec4 a_Position;
attribute vec2 a_Texcoord;

uniform mat4 u_MvpMatrix;

varying vec2 v_Texcoord;

void main()
{
    gl_Position = u_MvpMatrix * a_Position;
    v_Texcoord = a_Texcoord;
}

片段着色器

#ifdef FSHIGHPRECISION
    precision highp float;
#else
    precision mediump float;
#endif

uniform vec4 u_Color;
uniform float u_MaxRadius;
uniform float u_MinRadius;
uniform float u_Value;
// uniform float u_RotAngle;
// uniform float u_RotCenter;

varying vec2 v_Texcoord;

void main()
{
    //gl_FragColor = vec4(1.0,0.0,1.0, 1.0);

    //旋转角度是90度而不是270度,坐标系轴向不同,在此写固定值
    float Rote = (90.0 * 3.1415926)/180.0;
    float sinNum = sin(Rote);
    float cosNum = cos(Rote);
    //将uv中心平移至旋转中心
    vec2 uv01 = vec2(v_Texcoord.x, 1.0-v_Texcoord.y);
    uv01 = uv01 - vec2(0.5,0.5);
    //计算旋转之后的坐标,需要乘旋转矩阵,然后返回原位
    uv01 = uv01 * mat2(cosNum,-sinNum,sinNum,cosNum) + vec2(0.5,0.5);

    vec2 uv11 = uv01 * 2.0 - 1.0;

    //cg里面的atan2对应glsl的atan
    float angle = 1.0 - ceil(atan(uv11.g,uv11.r) - (6.4 * u_Value-3.2));

    //环形剪裁
    float uvR = dot(uv11,uv11);
    float uv_final = (1.0-floor(uvR + (1.0-u_MaxRadius))) * floor(uvR + (1.0-u_MinRadius));

    //颜色剪裁
    //glsl没有clip函数
    //customMaterial.alphaTest = true;customMaterial.alphaTestValue = 0.5;这两句并没有什么用,估计只是用来给u_AlphaTestValue传值
    //实测customMaterial.alphaTest = false;也能进行alphatest

    if(uv_final * angle < 0.5) discard;

    gl_FragColor = vec4(min(angle+1.0,1.0) * u_Color.rgb, u_Color.a);
}

 传值

class RoundProgressBarMaterial extends Laya.BaseMaterial
{
    //public static MAIN_TEX: number = 1;
    public static COLOR: number = 2;
    public static MAXRADIUS: number = 3;
    public static MINRADIUS: number = 4;
    public static VALUE: number = 5;
}

let attributeMap = 
{
    'a_Position': Laya.VertexElementUsage.POSITION0,
    'a_Texcoord': Laya.VertexElementUsage.TEXTURECOORDINATE0
};
let uniformMap = 
{
    'u_MvpMatrix': [Laya.Sprite3D.MVPMATRIX, Laya.Shader3D.PERIOD_SPRITE],
    'u_Color': [RoundProgressBarMaterial.COLOR, Laya.Shader3D.PERIOD_MATERIAL],
    'u_MaxRadius': [RoundProgressBarMaterial.MAXRADIUS, Laya.Shader3D.PERIOD_MATERIAL],
    'u_MinRadius': [RoundProgressBarMaterial.MINRADIUS, Laya.Shader3D.PERIOD_MATERIAL],
    'u_Value': [RoundProgressBarMaterial.VALUE, Laya.Shader3D.PERIOD_MATERIAL],
};

参考文献

  • Unity3D中灵活绘制进度条

  • ShaderForge的官方教程

  • Unity Shader 常用函数列表

  • Unity Shader中的平移、缩放、旋转

  • GLSL 中文手册

你可能感兴趣的:(Unity)