Blom like an artist:

Bloom 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:

  1. https://forum.unity.com/threads/question-about-normal-map-quality-and-channel-packing.527947/
  2. 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