Lerp 手法 5 選

はじめに

Lerp というのは 線形補間 を行う関数で、直近の 2 点間を直線で結んだとき、位置 (の比率) t にある点の値を返します。

C# では System.Numerics.Vector3.Lerp .NET 8 から実装される Double.Lerp があります。
これらよりは Unity の Mathf.Lerp のほうがなじみ深いかもしれません。

これを行う手法は複数あり、それぞれ特徴があります。それらの紹介と比較をしていきたいと思います。

5 つの Lerp 手法

Lerpシグネチャは以下であるものとします。

double Lerp(double x, double y, double t) => ...  

1. x + (y - x) * t

多くの方がこれを一番最初に思いつくかと思います。
min + (max - min) * t の形式で見かけた・書いた方は結構いらっしゃるのではないでしょうか。

2. (1 - t) * x + t * y

この式には「 t == 1.0 のときに必ず y と等しくなる」という特徴があります。

これは当たり前のように思えるかもしれませんが、実は 1 番目の式 x + (y - x) * t の場合、浮動小数点数の計算誤差によって t == 1.0 でも結果が y にならない場合があります (具体例は後述します) 。
そのような挙動を避けたい場合に採用されるのがこれです。

3. fma(t, y - x, x)

急に関数が出てきました。
fma()Fused Multiply Add といって、 fma(a, b, c) ~= a * b + c となるような関数です。
~= になっているのがポイントで、 fma は計算中に丸めを 1 回だけ行います。 a * b + c*+ で 2 回丸めが発生しますので、それよりも精度の高い計算を行うことができます。
C# では Math.FusedMultiplyAdd() から利用することができます。

ちなみに、浮動小数点数演算を最適化してくれるサイト Herbie に 1 つめの式を入れるとこれが返ってきます。

4. fma(t, y, (1 - t) * x)

これは 2 つ目の式に対して fma を適用した式です。

5. fma(t, y, fma(-t, x, x))

最後に、 4 つ目の式を変形してさらに fma を適用した式です。
((1 - t) * xx - t * x-t * x + x より)
ついに fma だけになりました。

手法の比較

極端な値を与えた場合の結果 (t == 0.5)

xy にそれぞれ極端な値 ( ±∞ とか NaN とか) を指定した場合で t == 0.5 のとき、直感的には下表のようになってほしい気持ちがあります。
なお、 max は表現可能な最大値の double.MaxValue ~= 1.8e+308eps は絶対値が表現可能な最小値の double.Epsilon ~= 4.9e-324 を指します。

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max -∞ -max -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 0 +∞ NaN
-1 -∞ -9e+307 -1 -0.5 -0.5 -0.5 -0.5 0 +9e+307 +∞ NaN
-eps -∞ -9e+307 -0.5 -eps -0 -0 0 +0.5 +9e+307 +∞ NaN
-0 -∞ -9e+307 -0.5 -0 -0 -0 0 +0.5 +9e+307 +∞ NaN
0 -∞ -9e+307 -0.5 -0 0 0 0 +0.5 +9e+307 +∞ NaN
+eps -∞ -9e+307 -0.5 0 0 0 +eps +0.5 +9e+307 +∞ NaN
+1 -∞ -9e+307 0 +0.5 +0.5 +0.5 +0.5 +1 +9e+307 +∞ NaN
+max -∞ 0 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +max +∞ NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

出力結果を見比べてみましょう。
上の理想の表と違う部分は太字にしてあります。

1. x + (y - x) * t

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -∞ NaN NaN
-1 NaN -9e+307 -1 -0.5 -0.5 -0.5 -0.5 0 +9e+307 NaN NaN
-eps NaN -9e+307 -0.5 -eps -0 0 0 +0.5 +9e+307 NaN NaN
-0 NaN -9e+307 -0.5 -eps 0 0 +eps +0.5 +9e+307 NaN NaN
0 NaN -9e+307 -0.5 -eps 0 0 +eps +0.5 +9e+307 NaN NaN
+eps NaN -9e+307 -0.5 0 0 0 +eps +0.5 +9e+307 NaN NaN
+1 NaN -9e+307 0 +0.5 +0.5 +0.5 +0.5 +1 +9e+307 NaN NaN
+max NaN +∞ +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

これを見ると、 x = ±∞ の場合に NaN になってしまっていることがわかります。
なぜかというと、例えば x = ∞ の場合は、

+ (y - ∞) * t  
∞ + (-∞) * t  
∞ + (-∞)  
NaN  

となってしまうためです。

加えて、 (-max, max) の場合にオーバーフローして ±∞ になってしまっています。

また、微妙な問題ですが、 (x, y) == (-0, -0) のときに +0 を返しています。

2. (1 - t) * x + t * y

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max -∞ -max -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 0 +∞ NaN
-1 -∞ -9e+307 -1 -0.5 -0.5 -0.5 -0.5 0 +9e+307 +∞ NaN
-eps -∞ -9e+307 -0.5 -0 -0 0 0 +0.5 +9e+307 +∞ NaN
-0 -∞ -9e+307 -0.5 -0 -0 0 0 +0.5 +9e+307 +∞ NaN
0 -∞ -9e+307 -0.5 0 0 0 0 +0.5 +9e+307 +∞ NaN
+eps -∞ -9e+307 -0.5 0 0 0 0 +0.5 +9e+307 +∞ NaN
+1 -∞ -9e+307 0 +0.5 +0.5 +0.5 +0.5 +1 +9e+307 +∞ NaN
+max -∞ 0 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +max +∞ NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

これは 1 つめの式とは異なり、概ね直感的な結果を返しています。

これも微妙な問題ですが、 Epsilon 同士のときに 0 を返しています。 x, y の項それぞれで 0.5 倍するため、アンダーフローして 0 になってしまったためです。

3. fma(t, y - x, x)

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -∞ NaN NaN
-1 NaN -9e+307 -1 -0.5 -0.5 -0.5 -0.5 0 +9e+307 NaN NaN
-eps NaN -9e+307 -0.5 -eps -0 -0 0 +0.5 +9e+307 NaN NaN
-0 NaN -9e+307 -0.5 -0 0 0 0 +0.5 +9e+307 NaN NaN
0 NaN -9e+307 -0.5 -0 0 0 0 +0.5 +9e+307 NaN NaN
+eps NaN -9e+307 -0.5 0 0 0 +eps +0.5 +9e+307 NaN NaN
+1 NaN -9e+307 0 +0.5 +0.5 +0.5 +0.5 +1 +9e+307 NaN NaN
+max NaN +∞ +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

1 つめの式と同様、 NaN±∞-0 の問題があります。

4. fma(t, y, (1 - t) * x)

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max -∞ -max -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 0 +∞ NaN
-1 -∞ -9e+307 -1 -0.5 -0.5 -0.5 -0.5 0 +9e+307 +∞ NaN
-eps -∞ -9e+307 -0.5 -0 -0 -0 -0 +0.5 +9e+307 +∞ NaN
-0 -∞ -9e+307 -0.5 -0 -0 0 0 +0.5 +9e+307 +∞ NaN
0 -∞ -9e+307 -0.5 0 0 0 0 +0.5 +9e+307 +∞ NaN
+eps -∞ -9e+307 -0.5 0 0 0 0 +0.5 +9e+307 +∞ NaN
+1 -∞ -9e+307 0 +0.5 +0.5 +0.5 +0.5 +1 +9e+307 +∞ NaN
+max -∞ 0 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +max +∞ NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

こちらは概ね 2 つめの式と同じ雰囲気で、概ね直感的です。

5. fma(t, y, fma(-t, x, x))

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 -9e+307 0 NaN NaN
-1 NaN -9e+307 -1 -0.5 -0.5 -0.5 -0.5 0 +9e+307 NaN NaN
-eps NaN -9e+307 -0.5 -0 -0 -0 -0 +0.5 +9e+307 NaN NaN
-0 NaN -9e+307 -0.5 -0 0 0 0 +0.5 +9e+307 NaN NaN
0 NaN -9e+307 -0.5 0 0 0 0 +0.5 +9e+307 NaN NaN
+eps NaN -9e+307 -0.5 0 0 0 0 +0.5 +9e+307 NaN NaN
+1 NaN -9e+307 0 +0.5 +0.5 +0.5 +0.5 +1 +9e+307 NaN NaN
+max NaN 0 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +9e+307 +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

こちらも ±∞ まわりで NaN 問題が見られます。

かわりに (-max, max) での ±∞ になっていた問題は解消しています。

極端な値を与えた場合の結果 (t == 0.0 および t == 1.0)

さて、 t == 0.0 および t == 1.0 の場合、 NaN が絡まない限り x または y がそのまま返ることが期待されます。
この場合はどうでしょうか?

t == 0.0 の場合 (上表) と t == 1.0 の場合 (下表) の理想値を示します。

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-max -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-1 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-eps -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-0 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
0 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
+eps -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
+1 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
+max -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
+∞ -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN
-max -max -max -max -max -max -max -max -max -max -max NaN
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 NaN
-eps -eps -eps -eps -eps -eps -eps -eps -eps -eps -eps NaN
-0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 NaN
0 0 0 0 0 0 0 0 0 0 0 NaN
+eps +eps +eps +eps +eps +eps +eps +eps +eps +eps +eps NaN
+1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 NaN
+max +max +max +max +max +max +max +max +max +max +max NaN
+∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

1. x + (y - x) * t

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
-max NaN -max -1 -eps -0 0 +eps +1 NaN NaN NaN
-1 NaN -max -1 -eps -0 0 +eps +1 +max NaN NaN
-eps NaN -max -1 -eps -0 0 +eps +1 +max NaN NaN
-0 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
0 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+eps NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+1 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+max NaN NaN -1 -eps 0 0 +eps +1 +max NaN NaN
+∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -max -max -max -max -max -max -∞ NaN NaN
-1 NaN 0 -1 -1 -1 -1 -1 -1 0 NaN NaN
-eps NaN 0 0 -eps -eps -eps -eps 0 0 NaN NaN
-0 NaN 0 0 0 0 0 0 0 0 NaN NaN
0 NaN 0 0 0 0 0 0 0 0 NaN NaN
+eps NaN 0 0 +eps +eps +eps +eps 0 0 NaN NaN
+1 NaN 0 +1 +1 +1 +1 +1 +1 0 NaN NaN
+max NaN +∞ +max +max +max +max +max +max +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

t == 0.0 の場合、 ±∞ が絡むときと (-max, +max) のときに NaN になってしまっています。

また t == 1.0 の場合でも、 x == ±∞ のときに NaN になってしまっています。
加えて、 max が絡む場合に、上下限に含まれていない 0 を返している箇所があります。おそらく桁落ちで情報量を失ってしまったものと思います。

2. (1 - t) * x + t * y

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
-max -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-1 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-eps -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-0 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
0 -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+eps -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+1 -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+max -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -max -max -max -max -max -max -max NaN NaN
-1 NaN -1 -1 -1 -1 -1 -1 -1 -1 NaN NaN
-eps NaN -eps -eps -eps -eps -eps -eps -eps -eps NaN NaN
-0 NaN -0 -0 -0 -0 0 0 0 0 NaN NaN
0 NaN 0 0 0 0 0 0 0 0 NaN NaN
+eps NaN +eps +eps +eps +eps +eps +eps +eps +eps NaN NaN
+1 NaN +1 +1 +1 +1 +1 +1 +1 +1 NaN NaN
+max NaN +max +max +max +max +max +max +max +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

1 つめの式より NaN の量が減っており、概ね x, y それぞれの値を返せています。
それでも NaN 以外から NaN になる行・列があります。
0 * ±∞NaN になってしまうためです。

また、 (x, y) == (-0, -0) の場合にきちんと (?) -0 を返しています。

3. fma(t, y - x, x)

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
-max NaN -max -1 -eps -0 0 +eps +1 NaN NaN NaN
-1 NaN -max -1 -eps -0 0 +eps +1 +max NaN NaN
-eps NaN -max -1 -eps -0 0 +eps +1 +max NaN NaN
-0 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
0 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+eps NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+1 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+max NaN NaN -1 -eps 0 0 +eps +1 +max NaN NaN
+∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -max -max -max -max -max -max -∞ NaN NaN
-1 NaN 0 -1 -1 -1 -1 -1 -1 0 NaN NaN
-eps NaN 0 0 -eps -eps -eps -eps 0 0 NaN NaN
-0 NaN 0 0 0 0 0 0 0 0 NaN NaN
0 NaN 0 0 0 0 0 0 0 0 NaN NaN
+eps NaN 0 0 +eps +eps +eps +eps 0 0 NaN NaN
+1 NaN 0 +1 +1 +1 +1 +1 +1 0 NaN NaN
+max NaN +∞ +max +max +max +max +max +max +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

1 つめの式 x + (y - x) * t と同じ感じで、大きな値の部分で NaN が返りがちです。

4. fma(t, y, (1 - t) * x)

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
-max -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-1 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-eps -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-0 -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
0 -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+eps -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+1 -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+max -∞ -max -1 -eps 0 0 +eps +1 +max +∞ NaN
+∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -max -max -max -max -max -max -max NaN NaN
-1 NaN -1 -1 -1 -1 -1 -1 -1 -1 NaN NaN
-eps NaN -eps -eps -eps -eps -eps -eps -eps -eps NaN NaN
-0 NaN -0 -0 -0 -0 0 0 0 0 NaN NaN
0 NaN 0 0 0 0 0 0 0 0 NaN NaN
+eps NaN +eps +eps +eps +eps +eps +eps +eps +eps NaN NaN
+1 NaN +1 +1 +1 +1 +1 +1 +1 +1 NaN NaN
+max NaN +max +max +max +max +max +max +max +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

2 つめの式 (1 - t) * x + t * y と同じ感じで、 ±∞ 以外は直感的な値を返せています。

5. fma(t, y, fma(-t, x, x))

y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
-max NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
-1 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
-eps NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
-0 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
0 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+eps NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+1 NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+max NaN -max -1 -eps 0 0 +eps +1 +max NaN NaN
+∞ NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
y\x -∞ -max -1 -eps -0 0 +eps +1 +max +∞ NaN
-∞ NaN -∞ -∞ -∞ -∞ -∞ -∞ -∞ -∞ NaN NaN
-max NaN -max -max -max -max -max -max -max -max NaN NaN
-1 NaN -1 -1 -1 -1 -1 -1 -1 -1 NaN NaN
-eps NaN -eps -eps -eps -eps -eps -eps -eps -eps NaN NaN
-0 NaN 0 0 0 0 0 0 0 0 NaN NaN
0 NaN 0 0 0 0 0 0 0 0 NaN NaN
+eps NaN +eps +eps +eps +eps +eps +eps +eps +eps NaN NaN
+1 NaN +1 +1 +1 +1 +1 +1 +1 +1 NaN NaN
+max NaN +max +max +max +max +max +max +max +max NaN NaN
+∞ NaN +∞ +∞ +∞ +∞ +∞ +∞ +∞ +∞ NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

やはり 1. や 3. の式と似た挙動ですが、 ±∞ が絡まないあたりについては正しい値を返せています。

まとめると、 t == 0.0 および t == 1.0 の場合には、 2. と 4. の式が直感に近い挙動をすることがわかりました。
ただ、それでも完璧に x または y の値を返すわけではない (NaN を含まない状態から NaN になりうる) 点に注意が必要です。

そんな値を与えた状態で Lerp しようとするな、はおっしゃる通りですが……

精度

前述したように、 1 つめの式 x + (y - x) * t では、 t == 1.0 でも結果が y に等しくならないことがあります。具体的に見てみましょう。

x = 456789, y = 0.1, t = 1.0 のとき、それぞれ以下の値が返ります。

  1. 0.09999999997671694
  2. 0.1
  3. 0.09999999997671694
  4. 0.1
  5. 0.1

1 つめの式 x + (y - x) * t と 3 つめの式 fma(t, y - x, x) では問題が出てしまっています。
x, y を乱数にして試したところ、だいたい 16 % 程度にこの現象がみられるようです。

ほかにも結果が変わる例があったので紹介します。
最初の行が (x, y, t) 、続く行が n 番目の式、最後の行 (0. がない行) は整数化して BigInteger で計算した参考値です。

0.43840423350536384, 0.35411112619051244, 0.21474226926519646  
        0.42030294035715793  
        0.4203029403571579  
        0.42030294035715793  
        0.4203029403571579  
        0.4203029403571579  
          4203029403571579193038747032939560  
  
0.2530206785167861, 0.7429566475206353, 0.42010793232833676  
        0.45884666542827324  
        0.4588466654282733  
        0.4588466654282733  
        0.4588466654282733  
        0.4588466654282733  
          458846665428273276122038673856592  
  
0.42627189060522985, 0.28161146670562975, 0.5186179477132566  
        0.3512483984470895  
        0.35124839844708955  
        0.3512483984470895  
        0.3512483984470895  
        0.3512483984470895  
          351248398447089509911817791314340  
  
0.48581429649946484, 0.05085961859894306, 0.871859806353322  
        0.10659479525264437  
        0.10659479525264434  
        0.10659479525264436  
        0.10659479525264436  
        0.10659479525264436  
          10659479525264437688525716364684  

乱数で調べた限りでは、 4 番目と 5 番目の式は常に一致するようです。

BigInteger での計算値が正しいと仮定し、それとの差を比較してみたところ、誤差が小さい順に Lerp3 < Lerp1 < Lerp4 == Lerp5 < Lerp2 という結果が得られました。

2 番目の式 (1 - t) * x + t * y の精度が良いと聞いていたので、 3 番目の式 fma(t, y - x, x) が一番良いのが意外でした。
そういえば Herbie は 3 つめの式を返していましたね… 実際に精度が良いことが確かめられた形になります。

速度

そのまま測ったところどれも測定不能レベルで速かったので、 Random.Shared.NextDouble() を各引数に渡して若干遅らせています。

BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1555/22H2/2022Update/SunValley2)  
12th Gen Intel Core i7-12700F, 1 CPU, 20 logical and 12 physical cores  
.NET SDK=8.0.100-preview.3.23178.7  
  [Host]     : .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2  
  DefaultJob : .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2  
Method Mean Error StdDev Code Size
Lerp1 18.78 ns 0.406 ns 0.965 ns 148 B
Lerp2 19.08 ns 0.448 ns 1.285 ns 168 B
Lerp3 18.91 ns 0.383 ns 0.974 ns 153 B
Lerp4 19.06 ns 0.411 ns 0.968 ns 173 B
Lerp5 19.29 ns 0.417 ns 0.843 ns 170 B

ほとんど変わりはないのですが、しいて言えば Lerp1 < Lerp2 = Lerp3 = Lerp4 < Lerp5 、という感じです。
文字通り誤差範囲内ですし、そのままだと測定不能になるレベルなのであまり気にしなくてもよさそうです。

擬似乱数生成との関係

ところで突然 Lerp の話を始めた理由はというと、実は擬似乱数生成と関係があるからです。
乱数生成において、 [0, 1) の範囲の乱数を返す NextDouble() をもとに、 [min, max) の範囲に変換して NextDouble(min, max) とする処理はよくあります。
手書きで NextDouble() * (max - min) + min とした経験がある方も多いでしょう。

ここで役立つのが Lerp の式です。 [0, 1) の乱数を t として min, max 間を Lerp させることで、この範囲変換を行うことができます。

この範囲変換を行うにあたり、どの式にしたら一番いい感じになるのか(雑)、と思い立ったのが本記事のきっかけです。

おわりに

「全部同じじゃないですか~」「違いますよ~」といった幻聴がする気がしますが、実際どれも微妙に異なります。

精度を求めるなら fma(t, y - x, x)t == 1 の場合や極端な値を与えたときの挙動を考えるなら fma(t, y, (1.0 - t) * x) が良い、でしょうか…?

おすすめの式はこれとか、実はこっちの式のほうがいいとか、有識者の方がいらっしゃいましたらコメント等いただけると助かります。