Wiered Lerp() with a static Method

Greetings,
It’s me, with my camera again! :joy:

I’m currently trying to write a simple Method to give my camera some move and zoom animation. I just give it a target Position, a target Zoom value and the speed that I want for the animation.

public void AnimateTransformTo(Vector2 position, float zoom, float speed, bool linear = true)

Then My Camera does the Job by updating the Transform each frame until Position and Zoom are reached. I do this by simply doing Vector2.Lerp with the Position and MathHelpler.Lerp() with the Zoom. After that I update the Transform of my Camera.

protected virtual void LerpTransformLinear(Vector2 startpos, Vector2 endpos, 
      float startzoom, float endzoom, float ammount)

This is the local definition of the Method I wrote to Lerp my Transform. And it is working fine.
However I somehow think it would be nice to have this method not only public but also static.

public static void TransformLinear(out Matrix matrix, Vector2 viewsize,
      Vector2 startpos, Vector2 endpos, float startzoom, float endzoom,
      float ammount)

But this one, for some reason, is only working correctly in one direction.

I signed the Method to start my animation with two clickable objects in my game. One of them zooming in and focusing other objects in the game. And the other returns to the initial position and zoom.
Now with the local Method I get a smooth animated zoom in, when clicking the first button. And when I click the second button I get a smooth animated zoom out.
But If I do this with the static Method I can only zoom in smooth. When I want to zoom out, I instantly get to the end of the animation. Wich basicly means that I don’t have a animation.

Funny thing: I’ve added Events to the camera to keep track of the camera’s status. I get a message when the animation starts and another one when the animation ends. The local approach comletes the zoom out in the same time in wich the static approach. So visually the animation doesn’t really happen, but technically it does? The Events are telling me so atleast. :confused:

As it is, I probably don’t really need the static approach. But anyway, I really would like to know why this thing I wrote is not working. So I would be happy if you can tell me what any of you thinks about this.

Code of the two Methods:
https://pastebin.com/26TfCPXg

Code of Update() and AnimateTransformTo():
https://pastebin.com/KTiSBSEA

In your local version you’re setting m_position and m_zoom. In your static version those are just stack variables.

So your else case to if (animateLinear) is definitely wrong as it uses those members.

:worried: oh dear. Yea I actually need to update the position and zoom field, or they will still have the initial value when I start to zoom out again. now I feel bad :sweat: Thanks for your help. :heart_eyes:

Also, your non-linear is doing a pseudo-Zeno’s Paradox, what you probably want is an MKCB curve, the mx+b stuff from Algebra. In which case you always use the same 0-1 range and just use the curve to remap the value for use in your Lerp. To replicate what you’re doing you’d use a X & Y-flipped quadratic.

M slope
K exponent
C X-intercept
B Y-intercept

Here’s the curve class I use. 130ns on a potato at the slowest (PDF/normal-distribution). It was developed for utility theory AI where I was running these curves 1000s of times a frame. (Mathf is just a helper for single instead of double math)

It’s meant to be used with a GUI to alter the curve but the raw formulas are written above each curve’s execution so they can be graphed in Desmos easily enough and the default values are sane (though Logistic doesn’t perfectly hit 0 and 1 … that’s tricky).

There’s a PDF with visual illustration of what each parameter does for the curve types.

public enum CurveType
{
    Constant,               // Fixed value
    Linear,                 // Algebra standard MX+B
    Quadratic,              // Exponential
    Logistic,               // Sigmoid
    Logit,                  // 90 degree Sigmoid (biology/psych origins)
    Threshold,              // Boolean/stair
    Sine,                   // Sine wave
    Parabolic,              // Algebra standard form parabola
    NormalDistribution,     // Probability density function
    Bounce,                 // Bouncing degrading pattern, effectively decaying noise
}

[Serializable]
public class ResponseCurve
{
    public CurveType CurveShape { get; set; } = CurveType.Linear;

    public float XIntercept { get; set; } = 0.0f;

    public float YIntercept { get; set; } = 0.0f;

    public float SlopeIntercept { get; set; } = 1.0f;

    public float Exponent { get; set; } = 1.0f;

    /// Flips the result value of Y to be 1 - Y (top-bottom mirror)
    public bool FlipY { get; set; } = false;

    /// Flips the value of X to be 1 - X (left-right mirror)
    public bool FlipX { get; set; } = false;

    public float GetValue(float x)
    {
        if (FlipX)
            x = 1.0f - x;

        // Evaluate the curve function for the given inputs.
        float value = 0.0f;
        switch (CurveShape)
        {
            case CurveType.Constant:
                value = YIntercept;
                break;
            case CurveType.Linear:
                // y = m(x - c) + b ... x expanded from standard mx+b
                value = (SlopeIntercept * (x - XIntercept)) + YIntercept;
                break;
            case CurveType.Quadratic:
                // y = mx * (x - c)^K + b
                value = ((SlopeIntercept * x) * Mathf.Pow(Mathf.Abs(x + XIntercept), Exponent)) + YIntercept;
                break;
            case CurveType.Logistic:
                // y = (k * (1 / (1 + (1000m^-1*x + c))) + b
                value = (Exponent * (1.0f / (1.0f + Mathf.Pow(Mathf.Abs(1000.0f * SlopeIntercept), (-1.0f * x) + XIntercept + 0.5f)))) + YIntercept; // Note, addition of 0.5 to keep default 0 XIntercept sane
                break;
            case CurveType.Logit:
                // y = -log(1 / (x + c)^K - 1) * m + b
                value = (-Mathf.Log((1.0f / Mathf.Pow(Mathf.Abs(x - XIntercept), Exponent)) - 1.0f) * 0.05f * SlopeIntercept) + (0.5f + YIntercept); // Note, addition of 0.5f to keep default 0 XIntercept sane
                break;
            case CurveType.Threshold:
                value = x > XIntercept ? (1.0f - YIntercept) : (0.0f - (1.0f - SlopeIntercept));
                break;
            case CurveType.Sine:
                // y = sin(m * (x + c)^K + b
                value = (Mathf.Sin(SlopeIntercept * Mathf.Pow(x + XIntercept, Exponent)) * 0.5f) + 0.5f + YIntercept;
                break;
            case CurveType.Parabolic:
                // y = mx^2 + K * (x + c) + b
                value = Mathf.Pow(SlopeIntercept * (x + XIntercept), 2) + (Exponent * (x + XIntercept)) + YIntercept;
                break;
            case CurveType.NormalDistribution:
                // y = K / sqrt(2 * PI) * 2^-(1/m * (x - c)^2) + b
                value = (Exponent / (Mathf.Sqrt(2 * 3.141596f))) * Mathf.Pow(2.0f, (-(1.0f / (Mathf.Abs(SlopeIntercept) * 0.01f)) * Mathf.Pow(x - (XIntercept + 0.5f), 2.0f))) + YIntercept;
                break;
            case CurveType.Bounce:
                value = Mathf.Abs(Mathf.Sin((6.28f * Exponent) * (x + XIntercept + 1f) * (x + XIntercept + 1f)) * (1f - x) * SlopeIntercept) + YIntercept;
                break;
        }

        // Invert the value if specified as an inverse.
        if (FlipY)
            value = 1.0f - value;

        // Constrain the return to a normal 0-1 range.
        return Mathf.Clamp01(value);
    }

    /// <summary>
    /// Constructs a response curve from a formatted string
    /// Format: CurveType X Y Slope Exponent <FLIPX> <FLIPY>
    /// </summary>
    /// <remarks>
    /// Examples:
    /// Linear 0 0 1 1
    /// Quadratic 0.5 0 0.23 1.3 flipx
    /// Logit -0.15 -0.25 0.3 2.3 flipx flipy
    /// </remarks>
    /// <param name="inputString">String to process</param>
    /// <returns>A response curve created from the input string</returns>
    public static ResponseCurve FromString(string inputString)
    {
        if (string.IsNullOrEmpty(inputString))
            throw new ArgumentNullException("inputString");

        string[] words = inputString.Split(splitChar, StringSplitOptions.RemoveEmptyEntries);
        if (words.Length < 5)
            throw new FormatException("ResponseCurve.FromString requires 5 SPACE seperated inputs: CurveType X Y Slope Exponent <FLIPX> <FLIPY>");

        ResponseCurve ret = new ResponseCurve();
        ret.CurveShape = (CurveType)Enum.Parse(typeof(CurveType), words[0]);

        float fValue = 0.0f;
        if (float.TryParse(words[1], out fValue))
            ret.XIntercept = fValue;
        else
            throw new FormatException("ResponseCurve.FromString; unable to parse X-Intercept: CurveType X Y Slope Exponent <FLIPX> <FLIPY>");

        if (float.TryParse(words[2], out fValue))
            ret.YIntercept = fValue;
        else
            throw new FormatException("ResponseCurve.FromString; unable to parse Y-Intercept: CurveType X Y Slope Exponent <FLIPX> <FLIPY>");

        if (float.TryParse(words[3], out fValue))
            ret.SlopeIntercept = fValue;
        else
            throw new FormatException("ResponseCurve.FromString; unable to parse SLOPE: CurveType X Y Slope Exponent <FLIPX> <FLIPY>");

        if (float.TryParse(words[4], out fValue))
            ret.Exponent = fValue;
        else
            throw new FormatException("ResponseCurve.FromString; unable to parse EXPONENT: CurveType X Y Slope Exponent <FLIPX> <FLIPY>");

        // If there are more parameters then check to see if they're FlipX/FlipY and set accordingly
        for (int i = 5; i < words.Length; ++i)
        {
            string lCase = words[i].ToLowerInvariant();
            if (lCase.Equals("flipx"))
                ret.FlipX = true;
            else if (lCase.Equals("flipy"))
                ret.FlipY = true;
        }

        return ret;
    }

    public override string ToString()
    {
        string ret = string.Format("{0} {1} {2} {3} {4}", CurveShape.ToString(), XIntercept, YIntercept, SlopeIntercept, Exponent);
        if (FlipX)
            ret += " flipx";
        if (FlipY)
            ret += " flipy";
        return ret;
    }

    // For the above string parsing split
    private static char[] splitChar = { ' ' };

    public ResponseCurve Clone()
    {
        ResponseCurve ret = new ResponseCurve();
        ret.XIntercept = XIntercept;
        ret.YIntercept = YIntercept;
        ret.SlopeIntercept = SlopeIntercept;
        ret.Exponent = Exponent;
        ret.CurveShape = CurveShape;
        ret.FlipX = FlipX;
        ret.FlipY = FlipY;
        return ret;
    }
}

Edit: there’s also some easing functions somehwere buried away in Monogame that would be appropriate.

1 Like

Oh wow, this awesome. I’m not too good with math. This will be nice to play arround with, thanks alot Sir! And I will totally need it in some more situations. Thank you thank you thank you :heart_eyes:

I didn’t really know what I was doing there, when I wrote my ‘Linear’ method. I only noticed that lerping from the current position to the end position was producing some kind of a curve. So I just made it save and use the starting position.