**Premise**

Today we’ll talk about the Lerp optimization on structs (which could be Vector3.Lerp, Color.Lerp and so on) and talk about how to optimize easing functions.

1 2 3 4 5 6 |
static float Lerp(float start_value, float end_value, float pct) { return (start_value + (end_value - start_value) * pct); } |

We already know the “float” Lerp function, which is the following:

We could encounter problems when we want to apply it to our custom types, such as a **Vector3**, a **Vector2** or a **Color**.

There are four different ways to implement it, today we’re going to describe all of them and see which has a better performance.

**Please**, remember that I’m profiling with **Unity** and I enabled “**Deep Profiling**” , which takes **a lot** of resources. These tests are under extreme conditions and will never occur in your game… unless you like to play it on a **PotatoMachine5000** and have **500000** objects calling those functions per each frame…which..I mean.. it’s not the best situation.

**Lerp Implementation**

Let’s use **Vector3** as examples; be aware that it’s the same for other struct types (more or less resources needed depending on which one you use).

The different ways to implement “Lerp” with our custom types are the following:

1 2 3 4 5 6 7 8 |
//Type "1", we apply operations to our struct types Vector3 Lerp(Vector3 start_value, Vector3 end_value, float t) { return start_value + (end_value - start_value) * t; } |

1 2 3 4 5 6 7 8 9 10 11 |
//Type "2", we lerp each value individually, calling a function Vector3 Lerp2(Vector3 start_value, Vector3 end_value, float t) { return new Vector3( Lerp(start_value.x, end_value.x, t), //Interpolates the X value Lerp(start_value.y, end_value.y, t), //Interpolates the Y value Lerp(start_value.z, end_value.z, t) //Interpolates the Z value ); } |

1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Type "3", which is the same as Unity's "Vector3.Lerp", we lerp individually but without calling the function, we also clamp the percentage value public Vector3 Lerp3(Vector3 start_value, Vector3 end_value, float t) { t = Mathf.Clamp01(t); return new Vector3( start_value.x + (end_value.x - start_value.x) * t, start_value.y + (end_value.y - start_value.y) * t, start_value.z + (end_value.z - start_value.z) * t ); } |

1 2 3 4 5 6 7 8 9 10 11 |
//Type "4", which is the same as Unity's "Vector3.LerpUnclamped", we basically removed the "clamp" (as the name says) public Vector3 Lerp4(Vector3 start_value, Vector3 end_value, float t) { return new Vector3( start_value.x + (end_value.x - start_value.x) * t, start_value.y + (end_value.y - start_value.y) * t, start_value.z + (end_value.z - start_value.z) * t ); } |

**Lerp1 vs Lerp2**

Let’s compare the first two types.

You can see the main difference from the profiler, the **first type **(left) takes more resources because we’re multiplying, adding and subtracting **Vector3**s. With **75000 Lerps per frame**, it takes up almost **70ms **of response time**.**

The** second type** (right) instead doesn’t need to operate on Vectors and is faster, with **75000 Lerps** **per frame** it takes up almost **50ms**.

**Lerp3 vs Lerp4**

What aboutÂ **Lerp3** (Unity’s **Vector3.Lerp, **left) and **Lerp4** (Unity’s **Vector3.LerpUnclamped, **right) ?

As said before, the difference between**Vector3.LerpUnclamped** and **Vector3.Lerp** is that Unity clamps the **“t”** value in the last one, assuring that it’s **included between [0,1]**. It simply adds this
t = Mathf.Clamp01(t); before the return. You can use **Vector3.Lerp** if you’re not sure your **“t” **is included between **[0,1]**, otherwise you can use **Vector3.LerpUnclamped** and gain even more performance.

**Vector3.LerpUnclamped** is the winner here, with only 20s of response time. (I must remember to you that I’m “deep profiling” and also calling a “Lerp” 75000 times per frame).

**Profiler Tables**

Let’s build some tables and see the main differences with also other types:

Vector3 |
Response time |
Performance |

Lerp1 | 70ms |
28% |

Lerp2 | 50ms |
40% |

Lerp3 (Or Unity’s Vector3.Lerp) | 30ms |
66% |

Lerp4 (Or Unity’s Vector3.LerpUnclamped) | 20ms |
100% |

Color |
Response time |
Performance |

Lerp1 | 70ms |
28% |

Lerp2 | 60ms |
33% |

Lerp3 (Or Unity’s Color.Lerp) | 30ms |
66% |

Lerp4 (Or Unity’s Color.LerpUnclamped) | 20ms |
100% |

Vector2 |
Response time |
Performance |

Lerp1 | 65ms |
30% |

Lerp2 | 40ms |
50% |

Lerp3 (Or Unity’s Vector2.Lerp) | 30ms |
66% |

Lerp4 (Or Unity’s Vector2.LerpUnclamped) | 20ms |
100% |

**Lerp Result**

You can use **Vector3.Lerp** and be sure that your percentage value is between **[0,1]**, or use **Vector3.LerpUnclamped** to maximize the performance.

**Don’t worry too much** about Lerp or LerpUnclamped if you’re at the beginning of your project! “**Premature optimization is the root of all evil**“, as every google search tells to you. Aaaand remember that these tests are under extreme conditions, always.

For info, I used this code to debug/profile the Vector3 (and then changed it for each struct type):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
using UnityEngine; public class Test: MonoBehaviour { float Lerp(float start_value, float end_value, float pct) { return (start_value + (end_value - start_value) * pct); } //Type "1" Vector3 Lerp(Vector3 start_value, Vector3 end_value, float t) { return start_value + (end_value - start_value) * t; } //Type "2" Vector3 Lerp2(Vector3 start_value, Vector3 end_value, float t) { return new Vector3( Lerp(start_value.x, end_value.x, t), //Interpolates the X value Lerp(start_value.y, end_value.y, t), //Interpolates the Y value Lerp(start_value.z, end_value.z, t) //Interpolates the Z value ); } //Type "3", which is the same as "Vector3.Lerp" public Vector3 Lerp3(Vector3 start_value, Vector3 end_value, float t) { t = Mathf.Clamp01(t); return new Vector3( start_value.x + (end_value.x - start_value.x) * t, start_value.y + (end_value.y - start_value.y) * t, start_value.z + (end_value.z - start_value.z) * t ); } [Range(1,4)] public int type = 1; public int count = 500000; Vector3 start_pos = new Vector3(0, 0, 0); Vector3 end_pos = new Vector3(1, 1, 1); private void Update() { float time = Mathf.Abs(Mathf.Sin(Time.time)); switch (type) { case 1: for (int i = 0; i < count; i++) Lerp(start_pos, end_pos, time / 1f); break; case 2: for (int i = 0; i < count; i++) Lerp2(start_pos, end_pos, time / 1f); break; case 3: for (int i = 0; i < count; i++) Vector3.Lerp(start_pos, end_pos, time / 1f); //Or also Lerp3 break; case 4: for (int i = 0; i < count; i++) Vector3.LerpUnclamped(start_pos, end_pos, time / 1f); break; } } } |

**Easing Functions**

Now that we know which **Lerp** is the best for us when we’re using structs we have to choose between **easing the value directly** **(giving a linear percentage as a parameter)** or **interpolate linearly the value based on the eased percentage**.

Let’s take “**EaseIn**” as an example (which is a simple **power of two** function) and also set the **duration** and the **end value** to **1** and the **start value** to **0**.

Let’s also increment the calls per frame to something like **500000**!

1 2 3 4 5 6 7 8 9 10 |
//Eases the value directly (given a linear percentage as a parameter) //Robert Penner's EaseIn equation float EaseIn(float start_value, float target_value, float time_elapsed, float duration) { time_elapsed /= duration; return target_value * time_elapsed * time_elapsed + start_value; } |

1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Later in your code (the "t" is increasing based on the time elapsed) value = EaseIn(0,1,t,1); //Interpolates only the given percentage, which is included between [0,1] float EaseIn(float t) { return t * t; } //Later in your code (the "t" is increasing based on the time elapsed) value = Lerp(0,1,EaseIn(t/1f)); |

Both scripts return the same value, but the last one eases only the percentage.

Well, that’s a huge difference! From 70ms to almost nothing, tweening only the percentage.

This is the script that I used to profile:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class Test: MonoBehaviour { static float Lerp(float start_value, float end_value, float pct) { return (start_value + (end_value - start_value) * pct); } //Simple ease in (tweaks only the given percentage, which is included between [0,1]) float EaseIn(float t) { return t * t; } //Robert Penner's EaseIn equation float EaseIn(float start_value, float target_value, float time_elapsed, float duration) { time_elapsed /= duration; return target_value * time_elapsed * time_elapsed + start_value; } [Range(1, 2)] public int type = 1; public int count = 500000; private void Update() { float time = Mathf.Abs(Mathf.Sin(Time.time)); switch (type) { case 1: for (int i = 0; i < count; i++) EaseIn(0, 1f, time, 1f); break; case 2: Lerp(0, 1f, EaseIn(time / 1f)); break; } } } |

That’s it for now!

To sum up things, I can say that you can use without worrying Lerp3 (**Vector3.Lerp**) and Lerp4(**Vector3.LerpUnclamped**). **When easing, you can use a Lerp for the value and provide an “Eased” percentage instead.**