[Shader and ShaderToy] Circular progress bar

Write in front 

        In the past two days, I saw the effect of a circular progress bar ( original address ) on shadertoy . I think it's pretty good. I want to learn the principle. The original effect is like this

text

        Before I touched the shadertoy, if I wanted to use the shader to achieve the above effect, I had to use at least two pictures: a complete ring graph and a translucent image for masking. Use the gradient of the alpha channel of the translucent image to mask and display the ring image. The effect achieved is like this

Although it is very simple to use the mask to make the code, the displayed gradient effect is obviously not as good as the above, and it is very dependent on the production level of the UI. By the way, I also posted a code

Shader "Custom/AlphaMask" {
   Properties 
    {
    _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}//原图
    _MaskTex ("Mask (A)", 2D) = "white" {}//遮罩图
    _Progress ("Progress", Range(0,1)) = 0.5//遮罩的百分比 
    }

    Category 
    {
        Lighting Off
        ZWrite Off
        Cull back
        Fog { Mode Off }
        Tags {"Queue"="Transparent" "IgnoreProjector"="True"}
        Blend SrcAlpha OneMinusSrcAlpha//设置成半透明
        SubShader 
        {
            Pass 
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                sampler2D _MainTex;
                sampler2D _MaskTex;
                float _Progress;
                struct appdata
                {
                    float4 vertex : POSITION;
                    float4 texcoord : TEXCOORD0;
                };
                struct v2f
                {
                    float4 pos : SV_POSITION;
                    float2 uv : TEXCOORD0;
                };
                v2f vert (appdata v)
                {
                    v2f o;
                    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);//矩阵转换获得顶点
                    o.uv = v.texcoord.xy;//获取uv值
                    return o;
                }
                half4 frag(v2f i) : COLOR
                {
                    fixed4 c = tex2D(_MainTex, i.uv);//获取原图像素上的颜色取样
                    fixed ca = tex2D(_MaskTex, i.uv).a;//获取遮罩图上颜色的alpha值
                    c.a *= ca >= _Progress ? 0: 1;//判断遮罩图上的alpha值是否大于_Progress的设置,如果小于则为0,透明不显示;如果大于则显示
                    return c;//返回像素颜色值
                }
                ENDCG
            }
        }
        
    }
    Fallback "Transparent/VertexLit"
}

        The alpha value of the masked image will also change according to the angle, as shown in the figure below.

 

        Today I want to talk about using shadertoy to achieve this effect will be much better. Not only do you need no texture, but you can also modify the color, set the starting position of the progress bar, cycle period, etc. Paste the original code first

// static values
const float PI=3.14159265358979323846;
const float TAU = 6.28318530717958647692;
const float STEP_LENGTH = 0.01;
const float ANGLE_OFFSET = PI*0.5;				// angle of dial
const vec4 color1 = vec4(1.0, 0.0, 0.0, 1.0);
const vec4 color2 = vec4(1.0, 1.0, 0.0, 1.0);
const float duration = 3.0;						// duration of dial


// Get the color value based on where in the circle the uv is
vec4 getGradientValue(in vec2 uv)
{
    vec2 dist =	vec2(1.0, 0.0) - vec2(-1.0, 0.0);
	float val = dot( uv - vec2(-1,0), dist ) / dot( dist, dist );
	clamp( val, 0.0, 1.0 );
    
	vec4 color = mix( color1, color2, val );
	// clamp depending on higher alpha value
	if( color1.a >= color2.a )
		color.a = clamp( color.a, color2.a, color1.a );
	else
		color.a = clamp( color.a, color1.a, color2.a );
	return color;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    float progress = mod(iTime, duration) / duration;
    float innerRadius = 0.5;
    float outerRadius = 0.65;
    float startAngle = 0.0;
    float endAngle = progress* TAU;
    vec2 uv = (2.0*fragCoord.xy - iResolution.xy)/iResolution.y;
    float d = length( uv );
    vec4 ioColor = getGradientValue(uv);    

    // Perform adaptive anti-aliasing.
    float w = fwidth( d ) * 1.0;
    float c = smoothstep( outerRadius + w, outerRadius - w, d );
    c -= smoothstep( innerRadius + w, innerRadius - w, d );
    
    // set color for the area within inner and outer radius
    fragColor = vec4(ioColor.rgb * vec3(c), 1.0);
	
    // limit to active progress
    float angle = (atan(uv.y,uv.x)) + ANGLE_OFFSET;
	if( angle < 0.0 ) angle += PI * 2.0;
    if( angle > endAngle){
        float a = smoothstep( 0.75, -w*2.0,  abs(endAngle - angle) );        
        //float a = smoothstep( 0.0, -w*2.0,  abs(endAngle - angle) );        
        fragColor *= a;
    }
    if(angle - w*2.0 < startAngle ){
        float a = smoothstep(  -w*2.0, w*2.0, (abs(startAngle - angle)) );
        fragColor *= a;    
    }
    
    /*
    // round butt stuff
	float lineWidth = (outerRadius - innerRadius) * 0.5;
    float midRadius = innerRadius + lineWidth;
    
   	// distance from pt at end angle
    vec2 endAnglePos = vec2( cos(endAngle-ANGLE_OFFSET), sin(endAngle-ANGLE_OFFSET)) * vec2(midRadius);
    float dist = length( uv - endAnglePos );
    float buttAlpha = smoothstep( lineWidth + w, lineWidth - w, dist );
    fragColor = mix(fragColor, ioColor, buttAlpha );

    // distance from pt at start angle
    vec2 startAnglePos = vec2( cos(startAngle-ANGLE_OFFSET), sin(startAngle-ANGLE_OFFSET)) * vec2(midRadius);
    dist = length( uv - startAnglePos );
    buttAlpha = smoothstep( lineWidth + w, lineWidth - w, dist );
    fragColor = mix(fragColor, ioColor, buttAlpha );
	*/
}

        The comments are all in English. It looks uncomfortable. After I understand it, I optimized the code and added Chinese comments.

vec4 _Color1=vec4(1.0,0.0,1.0,1.0);
vec4 _Color2=vec4(0.0,1.0,1.0,1.0);
//循环时间  
float _Duration=5.0;
//开始位置
float _AngleOffset=0.0;
//平滑区间
float _Antialias=1.0;
float pi=3.14;

// 颜色混合后转出 uv控制alpha值
vec4 getGradientValue(in vec2 uv)
{
			vec2 dist =	vec2(1.0, 1.0) - vec2(-1.0, 0.0);
			float val = dot( uv - vec2(-1,0), dist ) / dot( dist, dist );
			//限制val的范围为0到1
			val= clamp( val, 0.0, 1.0 );
			//将两个颜色混合输出
			vec4 color = mix( _Color1, _Color2, val );
			return color;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{

            vec2 uv =(2.0* fragCoord-iResolution.xy)/iResolution.y;
			//获取当前的进度 
			float progress = ( mod(iTime,_Duration))/_Duration;
			//设置内外圆半径
			float innerRadius = 0.5;
			float outerRadius = 0.65;
			//获得当前弧度
			float endAngle = progress*pi*2.0;
			//uv的长度 原点与圆相同
			float d = length(uv);
			//获取颜色
			vec4 ioColor = getGradientValue(uv);    

			//抗锯齿处理 fwidth(a)=abs(ddx(a))+abs(ddy(a))
			float w = fwidth( d ) * 1.0;
			//uv与外半径相比 大于半径返回1 小于半径返回0 
			float c = smoothstep( outerRadius + w, outerRadius - w, d );
			//smoothstep函数中 与内半径相比 大于半径返回1 小于返回0 
			c-=smoothstep( innerRadius + w, innerRadius - w, d );//最后得到的结果:如果uv在两个半径的环内,返回1,否则都返回0
			//混合颜色
		    fragColor = vec4(ioColor.xyz * vec3(c,c,c),1.0);
			// 进度条设置
			//获取当前uv相对原点的弧度
			float angle = (atan(uv.y,uv.x)) + _AngleOffset;
			//保证弧度>0
			if( angle < 0.0 ) angle += pi * 2.0;
			if( angle > endAngle){
                //让当前进度平滑显示
				float a = smoothstep(_Antialias,w*2.0,abs(endAngle - angle) );               
				fragColor *= a;
			}
}

        In the code, the actual running time iTime is used to control the current progress. In the code, _Duration is the time of a round of progress, and the (mod(iTime,_Duration))/_Duration operation is used to obtain the current progress. It is more difficult to understand that the author uses the value obtained by comparing the length of the current uv with the two radii inside and outside the circular progress bar to blend with the output color, which also involves some operations of linear interpolation fwidth(). Explain in detail here.

       First, let's talk about the fwidth() method, fwidth(a) returns the sum of the absolute value of the partial derivatives in the x and y directions.

       即fwidth(a)=abs(ddx(a.x))+abs(ddy(a.y))

        fwidth(a) will return the difference of a between the current pixel and the next pixel, which can be understood as the linear difference of a straight line.

        Here, the code first obtains the length of the uv relative to the center of the circle d=length(uv), and then performs the difference operation w=fwidth(d) on the distance d. This w can be understood as the distance between the current pixel distance and the next pixel The difference.

        Then there is the smoothstep() method, the smoothstep method will force the value back to 0-1.

        That is, smoothstep(min,max,x), when x<min,x=0; when x>max,x=1; when min<x<max, the range of x is

 

 

 

 

 

        

        What’s interesting is that in the smoothstep() method used by the author, min and max are reversed

float c = smoothstep( outerRadius + w, outerRadius - w, d );
c-=smoothstep( innerRadius + w, innerRadius - w, d );

        outerRadius + w>outerRadius - w。

       It can be found by exchanging the positions of min and max in unity. The result of smoothstep() processed in this way is also reversed. That is, when x<min returns 1, when x>max returns 0.

        So when the uv point is inside outerRadius, c returns 1, and when it is outside outerRadius, it returns 0.

        Similarly, for smoothstep (innerRadius + w, innerRadius-w, d ), it returns 1 when uv is inside innerRadius, and returns 0 when it is outside innerRadius.

        That is, when the two are subtracted, only when w (the length of uv) is between innerRadius and outerRadius, c will return 1, and finally multiply with the output color, when it is 1, the color result will be returned; otherwise When it returns 0, multiplying with any color will only return the black result. (I almost killed me when I thought about this)

fragColor = vec4(ioColor.xyz * vec3(c,c,c),1.0);

        The difficulty of drawing a circle has been solved, and finally the effect of the progress bar. among them

float angle = (atan2(uv.y,uv.x)) + _AngleOffset;

The atan2(uv.y,uv.x) method returns the radian (not the angle) of the current uv, plus a _AngleOffset representing the offset radian (not the angle).

        And endAngle corresponds to the arc of the current time.

float progress = (iGlobalTime%_Duration)/_Duration;
float endAngle = progress*pi*2.0;

        Use (iGlobalTime%_Duration)/_Duration to get the percentage of the current period, and then multiply it with progress*pi*2.0 to get the current radian. In this case. for

float a = smoothstep(_Antialias,w*2.0,abs(endAngle - angle) ); 

        When it is judged that the current uv point is within the corresponding time period, a will smoothly return to 0~1, otherwise it will return to 0 without displaying.

        The above is the analysis of shadertoy code.

By the way, attach the code of the shader part

Shader "Custom/countdown" {
	Properties {
		_Color1("Color1",COLOR)=(1,1,1,1)
		_Color2("Color2",COLOR)=(1,1,1,1)
		//循环一次的时间
		_Duration("Duration",float)=3
		//开始位置
		_AngleOffset("AngleOffset",float)=1.72
		//平滑区间
		_Antialias ("Antialias Factor", float) = 1
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		Pass{
		CGPROGRAM
		#include "UnityCG.cginc"
		#pragma fragmentoption ARB_precision_hint_fastest   
		#pragma target 3.0
		#pragma vertex vert
		#pragma fragment frag

		#define vec2 float2
		#define vec3 float3
		#define vec4 float4
		#define mat2 float2
		#define mat3 float3
		#define mat4 float4
		#define iGlobalTime _Time.y
		#define mod fmod
		#define mix lerp
		#define fract frac
		#define Texture2D tex2D
		#define iResolution _ScreenParams
		#define pi 3.1415926

		float4 _Color1;
		float4 _Color2;
		float _Duration;
		float _AngleOffset;
		float _Antialias;

		struct v2f{
			float4 pos:SV_POSITION;
			float4 srcPos:TEXCOORD0;
		};

		v2f vert(appdata_base v){
			v2f o;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			o.srcPos=ComputeScreenPos(o.pos);
			o.srcPos=o.pos;
			return o;
		}
		vec4 main(vec2 fragCoord);
		float4 frag(v2f iParam):COLOR{
			vec2 fragCoord=((iParam.srcPos.xy/iParam.srcPos.w)*_ScreenParams.xy);
			return main(fragCoord);
		}
		// 颜色混合后转出
		vec4 getGradientValue(in vec2 uv)
		{
			vec2 dist =	vec2(1.0, 1.0) - vec2(-1.0, 0.0);
			float val = dot( uv - vec2(-1,0), dist ) / dot( dist, dist );
			//限制val的范围为0到1
			val= clamp( val, 0.0, 1.0 );
			//将两个颜色混合输出
			vec4 color = mix( _Color1, _Color2, val );
			return color;
		}
		vec4 main(vec2 fragCoord){
			vec2 uv = fragCoord/iResolution.y;
			//设置转一圈的周期
			float progress = (iGlobalTime%_Duration)/_Duration;
			//设置内外圆半径
			float innerRadius = 0.5;
			float outerRadius = 0.65;
			//获得当前弧度
			float endAngle = progress*pi*2.0;
			//uv的长度 原点与圆相同
			float d = length(uv);
			//获取颜色
			vec4 ioColor = getGradientValue(uv);    

			//抗锯齿处理 fwidth(a)=abs(ddx(a))+abs(ddy(a))
			float w = fwidth( d ) * 1.0;
			//uv与外半径相比 大于半径返回1 小于半径返回0 
			float c = smoothstep( outerRadius + w, outerRadius - w, d );
			//smoothstep函数中 与内半径相比 大于半径返回1 小于返回0 
			c-=smoothstep( innerRadius + w, innerRadius - w, d );//最后得到的结果:如果uv在两个半径的环内,返回1,否则都返回0
			//混合颜色
			vec4 fragColor = vec4(ioColor.xyz * vec3(c,c,c),1.0);
			// 进度条设置
			//获取当前uv相对原点的弧度
			float angle = (atan2(uv.y,uv.x)) + _AngleOffset;
			//保证弧度>0
			if( angle < 0.0 ) angle += pi * 2.0;
			if( angle > endAngle){
				//对当前进度平滑处理
				float a = smoothstep(_Antialias,w*2.0,abs(endAngle - angle) );               
				fragColor *= a;
			}
			return fragColor;
		}
		ENDCG
		}
	}
	FallBack "Diffuse"
}

to sum up

        Learning shadertoy feels back to the age of studying mathematics in college. When a certain mathematical problem is encountered, it is very difficult to solve. Baidu is not easy to find, and there is no person around who understands this. As a result, a small problem may be stuck for a few days, but when the inspiration flashes or the problem is solved, joy It's hard to express his feelings, so just be so painful and happy. . .

Guess you like

Origin blog.csdn.net/ssssssilver/article/details/81217694