結果だけでなく過程も見てください

たい焼きさんの日々の奮闘を綴る日記です。

小数の計算にはfloat型よりもdouble型を使おう(C/C++)

ゲームプログラミングでのお話です。

ゲームプログラミングでは、通常キャラクターの速度や座標を小数で保持します。

DirectXでは、これらの値を保持するためのベクトルクラスが定義されています。
D3DXVECTOR3などのクラスです。このクラスはx,y,zのメンバ変数を持ち、
速度や座標を表すことができます。

struct D3DXVECTOR3
{
  :
  FLOAT x;
  FLOAT y;
  FLOAT z;
  :
}

ここで見てほしいのが、メンバ変数x,y,zの型です。型が「FLOAT」なのです。
(※FLOAT型はここではfloat型と同義と考えてください)
なぜdouble型ではなく、FLOAT型が使われているのか?理由は2つあります。(もっとあるかも)

  • CPUではdouble型の方が計算速度が速いが、GPUではFLOAT型の方が速い
  • double型が8byteでFLOAT型が4byteなのでメモリが少なくてすむ

というわけで、普段プログラミングする際には、変数定義・式の計算はdouble型で行い、
GPUに渡すときには上記構造体に合わせてFLOAT型にキャストして使えばよいでしょう。

FLOAT型で計算を行うと結果がずれる例

もともとこの記事を書こうと思ったのは、自作ゲームのジャンプ処理で、
動きにところどころにおかしなところがあったからです。
うまく口で説明できないのですが、たまに座標が飛ぶというか、原因を探るのに苦労しました。

FLOAT型とdouble型では表現できる幅に違いがあります。

指数部 仮数
FLOAT 8bit 23bit
double 11bit 52bit

FLOAT型は有効桁数が10進数で6.92桁なく、同じくらいの大きさの値の引き算が
行われると有効桁が少なくなるため、誤差が大きく出ることになります。
ゲームのように連続して加算を繰り返す場合は、誤差の積み重ねで座標が大きく
ずれてしまうことがあるため、計算は精度の高いdoubleで行った方がよいでしょう。

ジャンプの計算式

ジャンプの座標は二次関数的に変化させます。
高校の頃に習った「y = -(x-a)^2 + b」という式を使えばいいですね。
ここではx軸を時間として、ジャンプ開始の時刻を原点にしているので、
aおよびbの値を調整して、原点を通る放物線になるようにします。

図にするとこんな感じになります。

↓にソースコードを載せていますが、
要はソースコード内の変数xがx軸、変数g_lfCurJumpHeightがy軸の値になります。
これで放物線を描くジャンプが行えるというわけです。

// 400msで1回ジャンプするときのソースコードサンプル

double g_lfCurJumpHeight;   // 今回フレームのジャンプの高さ
double g_lfPrevJumpHeight;  // 前回フレームのジャンプの高さ// 400msで1回のジャンプが完了するため40.0で割ると、[0,10)の実数値が返る
double a = 5.0;
double x = (dwCurrentTime - dwStartJumpTime) / 40.0;

// 前回ジャンプ時の高さは保存しておく
g_lfPrevJumpHeight = g_lfCurJumpHeight;

// 原点を通り、x=2aのときy=0となる放物線を描く二次関数
g_lfCurJumpHeight = -(x-a)*(x-a) + a*a;

実際のゲームの処理では、衝突判定の関係で直接座標を操作せず、
(g_lfCurJumpHeight - g_lfPrevJumpHeight)で算出される値を速度として持たせます。
毎フレーム速度を設定して、衝突判定で壁にめりこまないことなどが確定したら、
速度を座標に加算して、座標をアップデートします。(ゲームでは一般的な手法です)

これらの浮動小数の計算過程で結果にずれが発生します。
FLOAT型とdouble型で、それぞれジャンプ時のy座標をプロットした図が以下となります。

FLOAT型の方はところどころ値が大きくずれています。
double型の方はずれが少なくなっています。
なんだこの程度のずれか、と思う方もいるかもしれませんが、
実際にキャラクターの動きを見るとかなり違和感があるのです。

というわけで、CPUで小数計算を行うときはdoubleを使いましょう。