Blom like an artist:
Circular Progress Bar
https://bgolus.medium.com/progressing-in-circles-13452434fdb9
ATan vs. ATan2
$$ \tan{\alpha} = \frac{\sin{\alpha}}{\cos{\alpha}} $$
and we have
Quadrant Angle sin cos tan
-------------------------------------------------
I 0 < α < π/2 + + +
II π/2 < α < π + - -
III π < α < 3π/2 - - +
IV 3π/2 < α < 2π - + -
Only knowing tangent doesn’t give exact quadrant, so we need atan2().
atan2(() takes 2 arguments, which is the projection of a vector with length v and angle $\alpha$ on the y and x axis
$$
y = v \cdot \sin{\alpha}
\
x = v \cdot \cos{\alpha}
\
y/x = \tan{\alpha}
$$
atan2() can tells 4 quadrant.
To implement a circular progress bar, we can use atan2().
First, we need to calculate UV to angle:
half angle = atan2(i.uv.x * -2.0 + 1.0, i.uv.y * -2.0 + 1.0);
This will make uv to 4 quadrants and calculate the current position’s angle.
atan2() returns the radian between -pi to pi, we should convert angle into [0.0, 1.0].
half range = angle / (UNITY_PI * 2.0) + 0.5;
Finally we can compare it with the progress value.
Another approach
Remap uv to -1 to +1 range so 0 is centered. Then we calculate progress to radian angle and create a rotation matrix.
The idea behind vertical and diagonal mask was to compare if the value is below 0. If it’s below 0, then it is filled part.
To figure this out, think both vertical and diagonal masks as a complete image, not just one pixel. When I increase or decrease the progress, shader rotates the diagonal mask. By combining vertical and diagonal masks, some values will become zero.
It worth to mention that the rotation matrix in 2D is :
$$ \begin{bmatrix}
\cos{\theta} & \sin{\theta} \
-\sin{\theta} & \cos{\theta}
\end{bmatrix} $$
"CircularBarRotatedMask" {
Properties {
_Frac ("Progress Bar Value", Range(0,1)) = 1.0
[NoScaleOffset] _AlphaTex ("Alpha", 2D) = "White" {}
_FillColor ("Fill Color", Color) = (1,1,1,1)
_BackColor ("Back Color", Color) = (0,0,0,1)
[Toggle(SMOOTHSTEP)] _Smoothstep ("Use Smoothstep", Float) = 0.0
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature SMOOTHSTEP
#include "UnityCG.cginc"
// Direct3D compiled stats:
// vertex shader:
// 12 math
// fragment shader:
// 13 math, 1 texture w/o smoothstep
// 18 math, 1 texture w/ smoothstep
half _Frac;
fixed4 _FillColor;
fixed4 _BackColor;
sampler2D _AlphaTex;
struct v2f {
float4 pos : SV_POSITION;
float3 uvMask : TEXCOORD0;
};
v2f vert (appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// rescale default quad UVs to a -1 to +1 range so 0 is centered
o.uvMask.xy = v.texcoord.xy * 2.0 - 1.0;
// flip so bar is clockwise to be consistent with other examples
o.uvMask.x = -o.uvMask.x;
// calculate radian angle from progress bar value
float angle = _Frac * (UNITY_PI * 2.0) - UNITY_PI;
// get sine and cosine value for angle
float sinX, cosX;
sincos(angle, sinX, cosX);
// construct 2D rotation matrix and rotate centered uvs to get angled mask
float2x2 rotationMatrix = float2x2(cosX, -sinX, sinX, cosX);
o.uvMask.z = mul(o.uvMask.xy, rotationMatrix).x;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half diag = i.uvMask.z;
half vert = i.uvMask.x;
// sharpen masks with screen space derivates
diag = i.uvMask.z / fwidth(i.uvMask.z);
vert = i.uvMask.x / fwidth(i.uvMask.x);
// "flip" the masks depending on progress bar value
half barProgress = 0.0;
if (_Frac < 0.5)
barProgress = max(diag, vert);
else
barProgress = min(diag, vert);
// mask bottom of progress bar when below 20%
if (_Frac < 0.2 && i.uvMask.y < 0.0)
barProgress = 1.5;
#if defined(SMOOTHSTEP)
barProgress = smoothstep(0.25, 1.25, barProgress);
#else
barProgress = saturate(barProgress);
#endif
// lerp between colors
fixed4 col = lerp(_FillColor, _BackColor, barProgress);
fixed alpha = tex2D(_AlphaTex, i.uvMask.xy * 0.5 + 0.5).a;
col.a *= alpha;
return col;
}
ENDCG
}
Unpack Normal
Any slight change in the channels will change the normal map’s result. DXT1’s RGB channels has 5,6,5 bits, respectively. The original value is 8 bits, so the precision is lost. DXT5 uses 5, 6, 5, 8 for RGBA channels. A is 8 bits and functions as R, G remains the same. B is calculated on the fly. Normal maps are normalized, so the recreated blue channel should be fine. This is called “swizzling” and it sacrificed one channel to giving less compression artifacts and better normal maps.
Mobile devices use unswizzled normal because sqrt
is expensive.
More Discussion Thread:
- https://forum.unity.com/threads/question-about-normal-map-quality-and-channel-packing.527947/
- https://forum.unity.com/threads/normal-map-looks-red-in-shader-graph.1071962/
Some Reading: https://substance3d.adobe.com/tutorials/courses/the-pbr-guide-part-1