Search code examples
c#algorithmunity-game-engineanimationmath

How to implement this Desmos roll function in Unity


I was looking for a way to create a roll out effect just like this in a 2D Unity project https://www.desmos.com/calculator/mrfrugwvm5

The problem is I'm not that goot at math, so I'm not sure how to translate the function to say a line renderer's points.

Can someone help me with that?

Thanks!


Solution

  • So to break this down a bit there are actually two functions involved.

    The first

    y = -1 { 0 <= X <= p }
    

    is quite trivial: In the range 0 to P paint a straight line with fixed y = -1.

    The other one is returning x and y coordinates and basically translates to

    var x = (1 - t / totalLength) * Mathf.Sin(t) + p;
    var y = -(1 - t / totalLength) * Mathf.Cos(t);
    

    There are also some ranges involved such as

    • {0 <= p <= 14} => p lies within 0 and 14

      You will see the number 14 at various places => This is the maximum total length of the line when it is fully unrolled

    • {0 <= t <= 30} => Not sure why they used a 30 here - the result is actually the same as with 14, again the total length

    • {t < 14 - p} => This limits t further down so the actual range is {0 <= t <= 14 - p}

    The p basically determines at which point the swirl starts and how big the range for t is.

    So as far as I can tell in a LineRenderer this somewhat boils down to e.g.

    public static class LineRendererExtensions
    {
        public static void DrawRolledLine(this LineRenderer line, float totalLength, float p, int maxPoints = 51, float swirlRadius = 1f)
        {
            // ensure p is between 0 and 1
            p = Mathf.Clamp01(p);
    
            // As a performance boost do not use more points than necessary
            // The higher p the less swirl => requires less points to draw
            var resolution = Mathf.CeilToInt(Mathf.Lerp(0, maxPoints, 1 - p));
            // ensure we have at least 2 points to draw a line
            resolution = Mathf.Max(2, resolution);
            
            // map p to the total length
            p *= totalLength;
            
            // calculate the range for the t value
            var maxT = (totalLength - p) / swirlRadius;
    
            // prepare the points array
            var points = new Vector3[resolution];
            
            // begin of straight line - for convenience I move the whole thing up by 1
            points[0] = new Vector3(0, 0, 0);
    
            // calculate the points of the swirl
            for (var i = 1; i < resolution; i++)
            {
                // spread the points equally over the range of t
                var t = (i - 1f) / (resolution - 1f) * maxT;
    
                // use given functions to calculate x and y positions
                var x = (1 - t / totalLength) * Mathf.Sin(t) * swirlRadius + p;
                var y = (-(1 - t / totalLength) * Mathf.Cos(t) + 1) * swirlRadius; // moving up by 1 * radius
    
                points[i] = new Vector3(x, y);
            }
    
            // finally apply the calculated points to the line renderer
            line.positionCount = resolution;
            line.SetPositions(points);
        }
    }
    

    As you can see as parameters I added a bit more flexibility

    • totalLength: The maximum length of the line when unrolled

    • p: Factor in the range {0, 1} => This will be mapped into {0, totalLength}

    • maxPoints: Up to how many points to use to draw the result

      If there is lesser swirl this resolution is reduced for better performance

    • swirlRadius: In case you want the swirl part to have a different radius than the default of 1

    I also modified the functions slightly so that the line is always at 0 instead.


    As a little demo use e.g. this component

    [ExecuteAlways] // <- also run in edit mode
    public class Example : MonoBehaviour
    {
        [SerializeField]
        private LineRenderer m_LineRenderer;
    
        [SerializeField]
        [Range(0f, 1f)]
        private float m_P = 0.5f;
    
        [SerializeField]
        [Min(0)]
        private int m_Resolution = 51;
    
        [SerializeField]
        private float m_TotalLength = 14.0f;
    
        [SerializeField]
        [Min(Mathf.Epsilon)]
        private float m_Radius = 1.0f;
    
        private void Update()
        {
            if (!m_LineRenderer)
            {
                m_LineRenderer = GetComponent<LineRenderer>();
    
                if (!m_LineRenderer)
                {
                    m_LineRenderer = gameObject.AddComponent<LineRenderer>();
                    m_LineRenderer.startWidth = 0.1f;
                    m_LineRenderer.endWidth = 0.1f;
                }
            }
    
            m_LineRenderer.DrawRolledLine(m_TotalLength, m_P, m_Resolution, m_Radius);
        }
    }
    

    enter image description here