8
\$\begingroup\$

I am stuck on creating a brightness gradient that looks as below(the values are continuous from the inner circle). What formulae can create such brightness gradient? So that when _Time is added to it, it will slowly brighten up from the inner circle in a spiral form? enter image description here

Tried a long time to modify from codes below but fail.

Shader "Unlit/testDisplay"
    {
      Properties
     {
        _MainTex ("Texture", 2D) = "white" {}
     }
    SubShader
    {
    // Render with transparent objects, after the opaque pass.
    Tags { "RenderType"="Transparent" "Queue"="Transparent"}
    LOD 100

    // Don't write to the depth buffer.
    ZWrite Off
    // Additive blending (add light/glow).
    Blend One One

    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;

        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            // Shift our texture coordinates so 0 is in the center,
            // and we go to -2 ... +2 at the edges.
            o.uv = (v.uv -0.5f) *4.0f;
            return o;
        }

        fixed4 frag (v2f i) : SV_Target
        {
            //Define boundary value for radius
            float radius=length(i.uv);
           radius = radius - (radius % 0.5) + 0.5;
            

            //convert to polar coordinates
            float angle=atan2(i.uv.y,i.uv.x);
            float angle1=frac(angle/(2*3.142f));
            float result=-4*radius+angle1+13*frac(_Time.x);  
            return result;
            
        }
        ENDCG
    }
}
}
```
\$\endgroup\$
3
  • \$\begingroup\$ What specific failure symptoms do you observe with this code? \$\endgroup\$
    – DMGregory
    Feb 2 at 19:22
  • \$\begingroup\$ I meant I couldn't figure out how to define the spiral boundary and this code looks as if I can modify from it as the effect is close to my desired graphic effect. \$\endgroup\$
    – chuackt
    Feb 3 at 7:29
  • 1
    \$\begingroup\$ Two things: 1) the spiral you implicitly consider (as well as the spirals in the answers) are called "Archimedean spirals" ; there are some other kind of spirals, in particular the logarithmic spiral. 2) A good (easy to program) approximation of the Archimedean spiral is the Theodorus spiral ; see my question here. \$\endgroup\$ Feb 5 at 13:51
17
\$\begingroup\$

It looks to me like you want something like this:

White spiral growing from inside out

Shader "Unlit/BrightSpiral"
{
    Properties
    {
        _Turns ("Turns", float) = 3
    }
    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;
            };

            float _Turns;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Place (0, 0) at the center of the coordinate space/quad
                // and scale the extremes to accommodate n turns of the spiral.
                o.uv = (v.uv - 0.5f) * 2.0f * _Turns;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // Convert to polar coordinates, with angle normalized to 1 per turn.
                float radius = length(i.uv);
                float angle = atan2(i.uv.y, i.uv.x) / (2.0 * 3.141592653589);

                // Add the number of full turns we need to reach this radius.
                angle += floor(radius - angle);

                // Shape a gradient from bright interior to dark outside,
                // that grows to fill the spiral over time.
                return 4.0f * (frac(3.0f * _Time.x) * (_Turns + 1.0f) - angle - 1.0f);
            }
            ENDCG
        }
    }
}

The version above is simple, but it has a little ugliness. The brightness ramps up linearly with angle, which means you get a harsh Mach band where it hits white - looking like a sharp corner, not a smooth blend. It also moves at a constant angular rate, meaning it moves faster in pixel distance as we go further out in radius, and the gradient ramp gets longer.

A few tweaks to the code can smooth that out, if you prefer:

Smoother spiral

fixed4 frag(v2f i) : SV_Target
{
    float radius = length(i.uv);
    float angle = atan2(i.uv.y, i.uv.x) / (2.0 * 3.141592653589);
    angle += floor(radius - angle) + 1.0f;              
    angle = angle * angle;

    float t = frac(3.0f * _Time.x);

    float gradient = saturate(1.0f - (0.25f + 0.5f * t) * (t * (_Turns * _Turns + 1.0f) - angle - 1.0f) );
    gradient *= gradient;
    return 1.0f - gradient * gradient;
}

This version still has a sharp aliased edge between turns of the spiral, but that too can be smoothed if you need for your application.

\$\endgroup\$
3
\$\begingroup\$

You can analytically generate the same shader effect. This isn't unity or HLSL code, but you should be able to translate this easily to HLSL, plus it runs in shader toy and you can play around with it, here is the shader toy link:

https://www.shadertoy.com/view/tl3fRM

Here is a gif (left spiral length based time variation, right is theta based).

enter image description here

here is the code (produces above gif on shadertoy):

const float pi  = 3.1415926535897932384626433832795;
const float sqrt3 = 1.7320508075688772935274463415059;
const float sqrt2 = 1.4142135623730950488016887242096;
const float inf = uintBitsToFloat(0x7F800000u);


//takes uv in 0->1 coordinates and turns it into normalized cordinates centered at 0,0. 
vec2 center_corrected_uv(vec2 uv, vec2 resolution){
    vec2 uvn = (uv - 0.5);
    //this accounts for aspect ratio, so the spiral is not skewed. 
     if(iResolution.y > iResolution.x){
        uvn.y *= resolution.y/resolution.x;
    }else{
        uvn.x *= resolution.x/resolution.y;
    }
    return uvn; 
}
//"atan + normalized" (0 -> 2*pi)
float atann(float y, float x){
    float theta = atan(y, x);
    //we add 2*pi because the result of atan in GLSL is pi -> -pi, and we want 0 -> 2*pi
    if(theta < 0.0){
        theta += 2.0*pi;
    }
    return theta; 
}


bool approx_eq(float a, float b, float threshold){
    return abs(a - b) < threshold; 
}



//this is the same thing as the spiral length function, mathematically, I just wasn't sure if this was more or less performant. 
//these functions are basically the analytic integral of r = coef*theta, the equation of an archimedes spiral (a spiral with evenly spaced rings). 
float spiral_length_sinh(float theta, float coef){
    //integral from https://math.stackexchange.com/a/424722/
    //and from wolfram alpha
    return coef*(1.0/2.0) * (sqrt(theta*theta + 1.0)*theta + asinh(theta));
}

float spiral_length(float theta, float coef){
    //integral from https://math.stackexchange.com/a/424722/
    //and from wolfram alpha
    float sqrt_theta_1 = sqrt(theta*theta + 1.0);
    return coef*(1.0/2.0) * (sqrt_theta_1*theta + log(sqrt_theta_1 + theta));
}




void mainImage( out vec4 fragColor, in vec2 fragCoord )
{

    //increase this to "zoom out"
    const float image_scale = 100.0;
    //increase this to increase the speed of the spiral. 
    const float speed = 100.0; 
    //*decrease* this to INCREASE fade length
    const float left_fade = 0.3; 
    //need a seperate additional fade modifier for the right side
    //otherwise will be transparent. 
    const float right_fade = 4.0*left_fade; 
    //decrease this to increase fade spacing. 
    //pi gaurantees filled in distance, going smaller will make a "gapped" spiral. 
    const float swirl_radius = pi;
    //this controls the swirl of the marker (the white line that shows up periodically)
    const float marker_swirl_radius = 0.05*pi; 
    //time before whole animation restarts. 
    const float restart_interval_s = 24.0;
    //time interval for blinking spiral line. 
    const float blink_interval_s = 4.0; 
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    // Time varying pixel color (this is what will display as the background if we make swirl radius even smaller). 
    vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
    
    
    //I want to display two different types of swirls, one which increases with spiral length, and one that increases with theta. 
    bool display_right = uv.x > 0.5; 
    if(display_right){
        uv.x -= 0.5; 
    }
    uv.x /= 0.5;
    //divide resolution by two to account for split screen. 
    vec2 uvn = center_corrected_uv(uv, vec2(iResolution.x/2.0, iResolution.y)) * image_scale; 
    float r = length(uvn)*1.0;
    float theta = 1.0*atann(uvn.y, uvn.x); 

    //theta needs to be incresed to with r increasing value, ever 2pi r we need to add 2 pi to theta, since it will reset to zero. 
    // we assume every 2*pi r we want to wrap around, our r = coef*theta is actually r = 1.0*theta.  
    // This would need to change to account for a different coeff. 
    float theta_fixed = theta + 2.0*pi*floor(r/(2.0*pi)); 

    float time = mod(iTime,restart_interval_s) * speed; 
    //right side too fast if we don't do this
    if(display_right){
        time *= 0.1; 
    }
    //if we only check for the distance from the adjusted theta we'll get this odd semi circular pattern,
    //because inner radii will not pass the threshold for the next theta, so we need to look at 2*pi back 
    //and forward to actually get the proper distance to spiral line from current pixel 
    bool theta_the_eq = approx_eq(r, theta_fixed, swirl_radius);
    bool theta_min_eq = approx_eq(r, theta_fixed - 2.0*pi,swirl_radius);
    bool theta_max_eq = approx_eq(r, theta_fixed + 2.0*pi,swirl_radius);
    bool within_swirl_radius = theta_the_eq || theta_min_eq || theta_max_eq;
    float relative_theta; 
    if(theta_the_eq){
        relative_theta = theta_fixed;
    }else if(theta_min_eq){
        relative_theta = theta_fixed - 2.0*pi;
    }else if(theta_max_eq){
        relative_theta = theta_fixed + 2.0*pi;
     }
    
    if(within_swirl_radius){
        if(display_right){
        //on the right, we show theta used as the time/distance modifier.  
        // Notice that it appears to get "faster" the further out it goes.  
        // This is because it is covering larger amounts of spiral length in the same angle space (theta). 
        // this may or may not be the effect you want. 
        //Notice, we have to use the appropriate theta which passed the approximate equal threshold, for similar reasons to why
        //we needed to test three different distance metrics in the first place. 
        
            //we use log in addition to the fade coefficient to create a more athsetically pleasing fade, but this isn't actually necessary. 
            //using only fade will still work, just with a linear ramp up to color. 
            col = vec3(right_fade*log(-(relative_theta - time)));
        }else{ 
        //on the LEFT, we show the actual spiral distance used as the time/distance modifier.  
        // Notice that it appears to get "slower" the further out it goes.  
        // This is because it is covering larger amounts of spiral length, and as it gets further out theres more spiral to cover per angle area. 
        // this may or may not be the effect you want. 
        //Notice, we have to use the appropriate theta which passed the approximate equal threshold, for similar reasons to why
        //we needed to test three different distance metrics in the first place. 

            //we use log in addition to the fade coefficient to create a more athsetically pleasing fade, but this isn't actually necessary. 
            //using only fade will still work, just with a linear ramp up to color. 
            col = vec3(left_fade*log(-(spiral_length(relative_theta, 1.0) - time)));
        }
    }

    // this is used to display the white line, so you can be sure this is an archimedes spiral (again, an equally ringed distance spiral), 
    //if you removed this code, it would not display the line. 
    if(approx_eq(r, theta_fixed, marker_swirl_radius) 
    || approx_eq(r, theta_fixed - 2.0*pi,marker_swirl_radius) 
    || approx_eq(r, theta_fixed + 2.0*pi, marker_swirl_radius)){
    //we blink this to show this is only temporary. 
        if(mod(mod(iTime,restart_interval_s), blink_interval_s) < 2.0){
            col = vec3(1.0);
        }
    }
    // Output to screen
    fragColor = vec4(col,1.0);
}

The code is well documented, but basically we can analytically represent the same thing using our knowledge of the function for an Archimedes spiral, r = a + b*theta though in this case I simplify it to r = 0.0 + 1.0*theta. We get the polar coordinate for each fragment (which can be based off of vertex attribute data instead of screen coordinates, ie like texture coordinates). This results in a r, and theta. We then figure out if our given theta is close enough to the associated radius r (remember, r = theta). We can then vary the width, speed, fade, and many other factors of the spiral arbitrarily. We fade based on the current time - the theta or the current spiral length.

On the left side, there is some additional math going on. To actually get the spiral length, you need an integral. Luckily the analytic integral is pretty simple, you can see more information about this integral here: https://math.stackexchange.com/a/424722/. My equation simplification makes this integral equivalent to https://www.wolframalpha.com/input/?i=integral+sqrt%28%28x%5E2%29+%2B+1%29, or

enter image description here

This indefinite integral is for the equation sqrt(x^2 + 1), which doesn't match the integral used in the mathematics stack exchange post, because of our simplification, we remove the a term, and with our b term the equation was integral sqrt((b*x)^2 + b^2). We can extract b from the inner term like so:

sqrt(b^2*(x^2 + 1))

and then extract it from the sqrt like so:

sqrt(b^2)*sqrt(x^2 + 1) b*sqrt(x^2 + 1)

then we can fully remove it from the integral and put it on the outside due to the constant multiple rule. so now we have

b * integral sqrt(x^2 + 1)

the indefinite analytical integral we generated earlier will need to be turned into a definite integral. But because the our integral(0) is 0, we only need to evaluate the integral with out subtracting to evaluate the total integral length from the start to the end.

\$\endgroup\$
3
  • \$\begingroup\$ Thank for the extra reference. Actually I just started learning programming few months ago, I'm not sure what language is this, I place the above tags since I using the same code(half of the codes) from here gamedev.stackexchange.com/questions/188578/… . I just modify it to check the effect and understand the codes for self learning purpose. Looks like the main recipe "angle += floor(radius - angle)" to build the spiral is this equation from the answer given by Gregory. \$\endgroup\$
    – chuackt
    Feb 4 at 7:25
  • \$\begingroup\$ Your codes is longer but looks like it provide more control to adjust the properties of the spiral? it even involve the calculation of line integral that is not taken consideration in unity shader. \$\endgroup\$
    – chuackt
    Feb 4 at 7:30
  • \$\begingroup\$ @chuackt the language is GLSL, though shader toy makes some other modifications and provides some built in variables like iTime and iResolution, and makes the code fragment shader only (fragment shader is the same as pixel shader in HLSL). Specifically this is WebGL's version of GLSL, which is GLSL ES 3.0 I think. \$\endgroup\$
    – Krupip
    Feb 4 at 14:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.