ホーム < ゲームつくろー! < Programming TIPs編

その16 なるほど面白い浮動小数点乱数


 ゲームでは色々な所で乱数が必要になります。乱数発生器にも様々なものがありますが、整数乱数であればメルセンヌ・ツイスター法が有名だったりします。ところで、浮動小数点の乱数もまた良く使うのですが、なるほど面白い実装方法を教えてもらいましたので備忘録として残します。ちなみに、元ネタはこの辺りにあります(http://iquilezles.org/www/articles/sfrand/sfrand.htm)。



@ 結果はこんなです

 0.0f〜1.0fの間の浮動小数点乱数を範囲での乱数を作る方法としてべたなのは以下のようなものです:

float frand() {
    return (float)rand() / RAND_MAX;
}

精度の話は抜きにして、この乱数発生には割り算が1回入っています。Windowsマシンではほぼ問題無いのですが、スペックの低いマシンだと割り算1回もなかなかにして高く付きます。

 今回の最終的な浮動小数点の乱数発生器は以下のようなものです:

float frand(unsigned v) {
    unsigned res = (v>>9) | 0x3f800000;
    return (*(float*)&res) - 1.0f;
}

これで引数に入ってくる32bit整数乱数が0.0f以上1.0f未満の浮動小数点一様乱数に変換されます。内部ではシフト演算と論理和と引き算がありますが、積や割り算はありません。シフト演算と論理和は非常に速い計算です。浮動小数点の引き算も掛け算や割り算に比べれば遥かに速い計算です。結果的に軽い計算で乱数が出力されるわけです。



A 浮動小数点を自前で作るのですよ

 上の関数の内部では単精度浮動小数点を自前で作っています。その仕組は次のとおりです。

 まず、IEEE方式の単精度浮動小数点は次のような式で表されます:

指数部、仮数部というのは、単精度浮動小数点のビット列の以下の部分から作られる数値です:

指数部は8bitの整数、そして仮数部は23bitの固定小数点値として扱います。

 さて、上記の基本を踏まえてここから0〜1の一様乱数を作ります。ここでポイントなのが「一様乱数」の意味。これは「指定の範囲内の値が等しい確率で出現する乱数」です。上の式で私達が調節できるのは指数部と仮数部のみです。このうち指数部の値を変更しても一様にはなりません(べき乗乱数にはなりそうですが)。一方の仮数部は単なる掛け算なので、「仮数部が一様乱数なら小数点値も一様乱数」になります。

 そこで、まず指数部の値を127に固定します。これにより2の0乗、つまり括弧の左側が1.0に固定されます。仮数部23bitは固定小数点なので、各ビットを立てるか下ろすかによって0.0〜1.0の値が作られます。あ、ちなみに1.0は出来ません!

 以上から、仮数部に23bitの一様な整数のビット列を与えると1×(1 + 0.0〜1.0)という1.0〜2.0の浮動小数点乱数が出力されます。つまり以下のようなビット列を作るんです:

 後はここから1.0を引いてあげれば0.0〜1.0の範囲になるわけです。

以上を踏まえて改めて関数を見てみます:

float frand(unsigned v) {
    unsigned res = (v>>9) | 0x3f800000;
    return (*(float*)&res) - 1.0f;
}

入力されてきた32bitの整数乱数を9bit右シフトしています。これにより23bitの整数が作られます。上の青い「F」の部分です。指数部は127、つまり31ビット目のみ0にするので0x3f800000(2進数だと0011 1111 1000...)を論理和しています。return部分は作成された32bit整数のアドレスを浮動小数点に強引にキャストして浮動小数点値にし、先に説明した理由から1.0を引いて0.0〜1.0の範囲に変換して返しています。


 同じような考え方は単精度だけでなく倍精度浮動小数点(double)でもできます。倍精度の場合は仮数部が52bitありますので少なくとも52bit以上の整数を関数に渡す必要がありますが、そういう値を出せる高速な整数乱数発生器はもちろんあります。気をつけるのはbitの足りない値を入れてしまうと一様にならない点です。関数内部で整数乱数を取れるのであれば、引数を作らないのも手です。


 浮動小数点の性質をうまく利用した乱数で、割り算や掛け算が無いので、プラットフォームによっては高速化もはかれるかもしれません。面白いですね。