[SOLVED] Help with spiral timer shader

I’ve made a shader that mimics the sprial cooldown/timer effect that many games use to indicate that an ability or action cannot be used until the timer ends.

I thought I got it working, but there appears to be some inconsistency in the speed of the spiral

In this gif you will notice that the spiral drastically speeds up for just a moment when it is just past the half way mark (from 6 to 7 o clock approximately) and slows down towards the end as well.

Here’s the code, I figured out a hacky way to do this using the dot product function as opposed to some other math related to triangles/circles that was beyond my math skills.

Unless my code is wrong and I’m overlooking something, then I’m starting to believe that using dot product here is not correct and doesn’t produce visually linear results even though it technically works. In which case I could use help doing this in a more mathematical way :slightly_smiling_face:

    vec3 centerPoint = vec3(0.0, 0.0, 0.0);
    
    vec3 dirToPixelPoint = normalize(vertCoords.xyz - centerPoint.xyz);        
    vec3 upVec = normalize(vec3(0, 0, 1));        
    
    float dotValue = dot(dirToPixelPoint, upVec);
    
    
    dotValue ++; //put all dot values in range of 0-2
    
    if(vertCoords.x < 0){ //put values on left side in range of 2-4
        
        dotValue = 2 - dotValue;
        dotValue += 2;
        
    } 
    
    
    dotValue *= 0.25; //put values back into range of 0-1
  
    if(dotValue > m_CurrentCooldownPercent){
        color.rgba = vec4(0.0004, 0.0004, 0.0005, 0.98);
    }else{
        color.rgba = vec4(0.0);
    }

I tried using vert coords as well as tex coords, both produce the same result. Also here’s the java code sending in the CurrentCooldownPercent value just to assure that I’m sending a proper value to the shader.

public void update(float tpf){
    float cdPctVal = 0f;
    if(bookItem != null){
        float currCd = bookItem.getCd();
        float totalCd = bookItem.getTotalCoolDown();
        
        if(currCd > 0){
            cdPctVal =  currCd / totalCd;
        }
    }
    
    if(actionOverlayMat != null){
       actionOverlayMat.setFloat("CurrentCooldownPercent", 1 - cdPctVal);
    }
}

Any help is appreciated :slightly_smiling_face: I also hope this code can maybe be useful to some other monkeys, especially once the speed inaccuracy is fixed.

1 Like

I’m pretty sure the dot product is not appropriate here (and that makes me sad because it’s my very favorite math thing). Admittedly, I do now know how you got where you are so maybe you derived this somehow… I’ve been heads down in “work breakdown structures” for the past two days and can’t properly math at the moment.

However, some red flags:

This vector will already be normalized by definition.

This will always be just dirToPixelPoint.z

So to be honest, unless there is code missing, I have trouble seeing how this works at all. For this code to produce what’s in your video, I’m definitely missing something.

I believe you are better off doing the trig functions here.

Something like:

vec3 dirToPixelPoint = normalize(vertCoords.xyz - centerPoint.xyz);  
float rads = atan2(dirToPixelPoint.y, dirToPixelPoint.x); // though your code indicates maybe switch .z for .y
float coolDownRads = m_CurrentCoolDownPercent * TWO_PI;  // make a TWO_PI constant
if( rads > coolDownRads ) {
    color.rgba = vec4(0.0004, 0.0004, 0.0005, 0.98);
} else {
    color.rgba = vec4(0.0);
}

There doesn’t seem to be a strong reason to worry about the cost of atan2 here but if you were really worried about it then you could precalculate a texture where each texel had the rads / TWO_PI gradient in it. Then just sample the texture and compare it to cool down percent.

Edit: I did a quick google for radial gradient texture for an illustration of what I mean above:

The upper left corner of this image:

2 Likes

Ok, I did one better. I was 99% sure that photoshop had this. So I made on for you.

This seems more linear than the one in the image I quoted above and it’s already clipped for you.

I still recommend trying the atan2() approach just for understanding. And no doubt, that’s what photoshop is using for this texture.

1 Like

Thank you the aTan2 function is exacly what I needed. It took me a bit to implement it and understand radians better, but everything is working now and the sprial is finally smooth. It probably would’ve been quicker to use the image based spiral, but I figured I’d use this as a learning opportunity as well.

I had to add some more code to convert negative radians to positive and put them in proper range to make the sprial cover the full 360 degree, rather than just 180. I think this is because the vec3 dirToPixelPoint I’m calculating from the models vert coords can be negative and returns a range of negative radians. So instead of getting a range of radians representing 0 through 360 degrees for the float value rads, I think I was getting a range of radians representing -180 through 180 and that was causing only half the spiral to work.

So I added this code after calculating rads value using the atan2 function, and everything is working now.

       if(rads < 0){
            rads += PI;
            rads *= 0.5;
            
            rads = PI - rads;
            
        }else{
            rads = PI - rads;
            rads *= 0.5;
        }

Thank you for the help, you never fail to help me solve and learn things that I previously thought were beyond my capabilities :slightly_smiling_face:

Here’s the full snippet of useful code in case anyone else ever needs the same functionality. And of course let me know if anything else looks like it can still be improved.

#define PI 3.14159
float ATan2(vec2 dir)
{
    float angle = asin(dir.x) > 0 ? acos(dir.y) : -acos(dir.y);
    return angle;
}

      ...
                
        vec3 dirToPixelPoint = normalize(vertCoords.xyz);  
        float rads = ATan2(vec2(dirToPixelPoint.x, dirToPixelPoint.z)); // though your code indicates maybe switch .z for .y
        
        if(rads < 0){
            rads += PI;
            rads *= 0.5;
            
            rads = PI - rads;
            
        }else{
            rads = PI - rads;
            rads *= 0.5;
        }    
        
        
        
        float coolDownRads = m_CurrentCooldownPercent * PI;  
        
        
        if( rads > coolDownRads ) {
            color.rgba = vec4(0.0004, 0.0004, 0.0005, 0.98);
        } else {
            color.rgba = vec4(0.0);
        }
1 Like

It’s true that atan2 will return -PI to PI. Once you divide by TWO_PI you can just add 0.5. Whether you want 0 degrees to be “up” or not is often a matter of what values you pass to atan2. But your math should be fine also.

I guess I didn’t realize that glsl does not have an atan2()… but it’s good you figured it out.

In the end, the texture based approach will be superior but not as much of a learning experience.

1 Like

I’m unfortunately having trouble with this still. The Atan approach worked perfect but lowered the framerate by a bit too much.

So now I’m trying to get the gradient texture based approach to work, but that is having a similar issues as I got with using the dot product, where the spiral’s position is not matching up to the value I set, but in a different way this time.

The texture you provided appears to be setup perfectly, and I double checked this by using the eye dropper tool by checking the 25%, 50%, and 75% positions for the proper color values.

I have also debugged the gradient value in-action using the eye dropper on a screenshot, and again, the gradient value appears to be correct - and this screenshot shows that the point just above the the 75% mark has an expected color values of 72.2, so that is indeed the 3/4ths mark.

But the issue here is that this screenshot was taken with the value hard coded to 0.5 so it should be at the 50% mark, not 75%…

      float gradientVal = texture2D(m_GradientTexture, texCoord1).r;

             //hard coded at 50%
        if( 0.5 >=  gradientVal) {
            color.rgba = vec4(gradientVal, gradientVal, gradientVal, 1.0); //display the gradient value on screen
            
        } else {
            color.rgba = vec4(0.0); 
        }


     gl_FragColor = color;
}

So something is going on that makes no sense at all to me. It’s like the gradient value I’m displaying on screen at a specific point is not the same as the value that is being compared to 0.5 in my if statement…

How can it be displaying the gradient value at a value of 0.72 inside an if statement where I am checking that value for being under 0.5? It should trigger the else and set color to vec4(0) instead, but its not.

I am completely lost here. I tried increasing the texture size and bit depth, and also made the mesh higher poly incase that was the issue, but neither thing fixed the issue.

Here is also a video showing how the spiral never accurately represents the count down

So I created a horizontal gradient (since I can handle that easily) and that works fine.

So then I re-downloaded the spiral image you provided, and suddenly it works… so apparently I messed up the first version I downloaded. It was 2 months ago so I can’t remember exactly but I think I actually took a screenshot of the image instead of downloading it the first time… so that could very well be what caused it.

Sorry for re-opening this thread only to figure out the solution was as simple as re-downloading the gradient image. I Still have no clue how the first copy that I cropped out of a screenshot managed to display out all the right colors to appear a proper spiral, even with the eye dropper tool, but did not work in the shader. I just know I will not be lazy and take screenshots instead of downloading the image in the future.

1 Like

To add to my list of mistakes and mess ups this past day, I was also wrong about this:

The lowered framerate I was experiencing was a result of sending a float param into the Material every frame for each spiral… I forgot I never optimized this shader to pack the float value into a vector object, and only send the vector param to the material once.

So atan2 would still be a viable alternative probably. Both approaches now work at atleast 60 fps for about a dozen spirals