写在前面
这两天在shadertoy上看一个环形的进度条效果(原地址),觉得挺不错的,想学习一下其中的原理。原版的效果是这样的
正文
我在没接触shadertoy前,如果要用shader实现上面的效果至少都要用两张图片:一张完整的环形图与一张用来遮罩的半透明图。利用半透明图alpha通道的渐变来遮罩显示环形图,实现起来的效果是这样的
虽然利用遮罩来做代码实现十分简单,但显示的渐变效果明显没有上面的好,而且十分依赖UI的制作水平,顺便也贴上个代码
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"
}
遮罩的图的alpha值也是会根据角度渐变,如下图所示。
今天要说的用shadertoy来实现这个效果会好很多。不仅不需要贴图,还能修改颜色,设置进度条起始位置,循环周期等。先贴上原代码
// 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 );
*/
}
注释全是英语看着难受,自已理解一遍后优化了下代码,并添上了中文注释
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;
}
}
在代码中用实际运行时间 iTime来控制当前进度,代码中_Duration为一圈进度的时间,( mod(iTime,_Duration))/_Duration操作则用来获取当前的进度。比较难理解的是作者用当前uv的长度与环形进度条内外两个半径作比较得出的值来与输出的颜色进行混合,其中还涉及了线性插值fwidth()的一些操作。这里详细说明一下。
首先来说下fwidth()方法,fwidth(a) 返回x和y方向偏导数的绝对值的和。
即fwidth(a)=abs(ddx(a.x))+abs(ddy(a.y))
fwidth(a)会返回a在当前像素与下一个像素之间的差值,可以理解成直线的线性差值。
在这里,代码先获取了uv相对圆心的长度d=length(uv),然后对距离d作了差值操作w=fwidth(d),这个w可以理解成当前像素距离与下一像素之间距离的的差值。
然后就是smoothstep()方法,smoothstep方法会强行将值返回0-1。
即smoothstep(min,max,x),当x<min,x=0;当x>max,x=1;当min<x<max时,x的范围为
有趣的是,在这里作者用的smoothstep()方法,min跟max是反过来的
float c = smoothstep( outerRadius + w, outerRadius - w, d );
c-=smoothstep( innerRadius + w, innerRadius - w, d );
outerRadius + w>outerRadius - w。
在unity交换这个min跟max的位置可以发现,这样处理的smoothstep()得到的结果也是反过来的。即,当x<min返回1,当x>max返回0。
所以当uv点在outerRadius内时,c返回1,在outerRadius外时返回0。
同理,对于smoothstep( innerRadius + w, innerRadius - w, d ),当uv在innerRadius内时返回1,在innerRadius外时返回0。
即当两者相减时,只有w(也就是uv的长度)在innerRadius和outerRadius之间时,c会返回1,最终与输出的颜色相乘,当为1时才会返回颜色结果;不然当返回0时,与任何颜色相乘只会返回黑色的结果。(想这个东西的时候快绕死我了)
fragColor = vec4(ioColor.xyz * vec3(c,c,c),1.0);
画环形这个难点已经解决,最后就是进度条的效果了。其中
float angle = (atan2(uv.y,uv.x)) + _AngleOffset;
atan2(uv.y,uv.x)方法返回当前uv的弧度(不是角度),加上个_AngleOffset表示偏移的弧度(不是角度)。
而endAngle对应了当前时间的弧度。
float progress = (iGlobalTime%_Duration)/_Duration;
float endAngle = progress*pi*2.0;
用(iGlobalTime%_Duration)/_Duration获得当前的周期的百分比,然后与progress*pi*2.0相乘得到当前弧度。这样的话。对于
float a = smoothstep(_Antialias,w*2.0,abs(endAngle - angle) );
当判断当前的uv点在对应时间周期内时,a会平滑返回0~1,否则返回0不显示。
以上就是shadertoy代码的分析。
顺便附上shader部分的代码
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"
}
总结
学shadertoy感觉又回到了大学里学数学的年代。当遇到某个数学问题时,解决起来十分有难度,百度不好找,身边又没有懂这个的人,结果一个小问题都可能卡个几天,但当灵光一闪或者解决问题时,喜悦的心情也是难以表达的,就这样痛并快乐着吧。。。