気まぐれな戯れ言 バックナンバー10


気まぐれな戯れ言バックナンバーです。全体の一覧はこちら

04/09/05 乱数+Rnd関数解析編
04/07/20 2度目の自作編
04/06/15 VBでOpenGL編−1

04/09/05 乱数+Rnd関数解析編[★★◎◎◎]
(いつもそうですが)どう考えても自分の趣味で突っ走った今回の戯言です。

日ごろゲームなり統計なりのプログラムを作ろうとすると、乱数が登場する場面は非常に多いです。
こんなとき、C言語ならrand、VBならRnd関数を使うことが多いと思います。
しかし、使ってて「Rndって何やってるの?」って思ったことはないでしょうか?
「いや、別にどうでもいいよ」と言われてしまえばそれまでですが、まぁ興味を持ったので調べて見ました。

乱数の基礎  擬似乱数  C言語では何を使ってる?  いよいよVBのRnd関数
ランタイムの解析  Randomizeの謎  (2007/08追記)VBでRand/Randomizeを実装  まとめ

乱数の基礎

一応教科書にあるような話ですが乱数の基礎。
ここで言う乱数とは一様乱数を表すものとします。
一様乱数は範囲内の値をすべて同じ確率で返すような乱数です。
もうひとつの条件として、これまでの値から次の値が予測できないことが必要です。
たとえばサイコロの目やコインの裏表はそれぞれ各値は同じ確率で出ますし、次の値が予測できないので、一様乱数と言えます。

コンピュータは与えられた計算を高速に正しく行う機器であるため、乱数とは相性が悪いです。
もし何らかの計算式で一見ランダムな数列を計算できるとしても、計算で求められる以上何かしら偏りが出るとか、次の値が予測できてしまいます。
1つの乱数の生成法としては、コンピュータの外部から値を持ってきて加工すると言うものがあります。
代表的な物は以下のような物があります。
  • ユーザーにでたらめにマウスやキーボードで入力させて乱数とする。

    もちろん真の意味での乱数ではありませんが、少なくとも入力前に生成される値を予測することは不可能です。
    暗号の鍵の生成などでは、この手法を利用しているソフトウェアもあります。
    キーボードやマウスはPC標準の入力なので、実装は楽です。

  • クロック数や実時間を利用。

    これらを利用した場合、(カウンタがいっぱいになってループとかすることを考えなければ)基本的に同じ値は2度と出てきません。
    実際、VBのRandomizeステートメントは引数を省略するとTimer関数の値を入力したこととみなされます。
    実時間を算出する機能を持っていない組み込み機器などでは使いにくいかも?

  • 物理現象を利用する。

    「物理乱数」で検索すると、実際の物理現象を用いた乱数生成器がすでに存在していることがわかります。
    電流を流して音や温度を計測し、乱数とするもの等があります。
    HDDやCDのアクセスの待ち時間を用いる方法などもあります。
いずれも有効な手段ですが、「真の意味で乱数」と言える物はありません。
物理現象も厳密にはそのときの空気やら配線の状況がわかれば(人間やコンピュータが計算しきれるかどうかは置いておいて)生成される音や温度は予測可能だからです。
それこそ完全に真の意味で乱数を取ろうとするのであれば、量子力学がうんぬんとか言い出す事になります。


擬似乱数

だいぶ脱線しましたが、まぁそこまでやらんでもいいだろう、と言うことで良く使われるのが擬似乱数です。
上記のとおり、完全な乱数は生成するのが難しいため、計算によって乱数っぽく見える値を生成します。
(暗号論的には、長さに対して多項式時間で法則性が見つからなければ擬似乱数)
非常にシンプルな手法として、以下の2つを紹介します。

  • 線形合同法

    乗算・加算・剰余だけで簡単に行える乱数生成法です。
    そのため、実際にソフトウェアで乱数を生成する場合に広く利用されています。

    前回の乱数値をxとすると、次の乱数値は次の式で計算されます。
    x = (x × A + B) mod C
    AとBとCはこの計算に置けるパラメータです。
    この計算では0〜(C-1)のC通りの値が計算されます。
    通常、AとBとCは互いに素な値を選びます。まぁ3つとも素数にしておけばいいでしょう。

    例えばA=2、B=3、C=11とすると、x=1からスタートした場合、次の順で乱数を生成します。
    1、5、2、7、6、4、0、3、9、10、1、5、2、……(以後ループ)
    A・B・Cをちゃんと選べば、上を見てわかるように周期はCになります。
    C回計算を行うと、必ず各値が1回ずつ出てくるので少なくとも各値が出てくる確率は1/Cで偏りはありません。

    問題点として、ある値が1度出てくると次の値は毎回同じ値になってしまうことがあります。
    VC++のrand関数では少し工夫して表向き同じ数が出ても、次の数は異なるようにしています(後述)。

  • 線形フィードバックシフトレジスタ(LFSR)

    やっていることは線形合同法に近いです。
    線形合同法は掛け算や足し算を行っていますが、ここではそこをシフトとXORで行います。
    そのため、ハードウェアで行うにはこちらの方が向いています。
    例えば、ファミコンのノイズ音はこのLFSRで生成しています。

    Lbitの乱数を生成するとします。
    まず、L個のビット位置に対して、それぞれ係数を0か1かを決めておきます(これがパラメータ)。
    現在の値に対して、係数が1の位置にあるビットを持ってきて(ANDすると考えて良い)、全部をXORして1bitの値にします。
    現在の値を1ビットずらし、なくなったところに先ほどXORで計算した値を入れます。

    言葉だとわかりにくいので、実際に計算して見ます。
    4bitの値について、パラメータを1001としておきます。
    現在の値を1100とすると・・・
    1101 And 1001 = 1000
    この1000をそれぞれXORして1 xor 0 xor 0 xor 0 = 1
    1101を1ビット左シフトして101*、この一番下に先ほどXORで求めた1を入れて1011
    

    同様の処理を繰り返していくと、
    1101、1010、0101、1011、0110、1100、1001、0010、0100、1000、0001、0011、0111、1111、1110、1101、1010、……(以後ループ)
    パラメータを正しく選ぶ場合、0000以外のすべての値を生成します。
    そのとき周期は2^L - 1で、今回の場合は15です。
    弱点としては、線形合同法と同じで次の値が予測できてしまうことがあります。

    (暗号・整数論的に言うと「正しいパラメータ」とは、パラメータの最後に1をつけた10011から生成される特性方程式x^4 + x + 1が拡大体GF(24)で原始多項式である場合のこと)
他には比較的単純な物ではWikipediaの擬似乱数の項に紹介されているように、線形合同法の最後のmodの部分を変形した混合合同法や、平方採中法などがあります。
ここまで挙げてきたものでは、線形合同法がわかりやすいし次の値が予測しにくく、結局広く使われているようです。
(乗算器や除算器を持たない簡単な回路ならLFSRの方が実装しやすいでしょうが。)

より質の高い(周期が長い、偏りが少ない)乱数生成アルゴリズムとしてはメルセンヌ・ツイスタ等が注目されています。
さすがに線形合同法ほどシンプルで高速なわけではありませんが、他の(もう少し質が高くて使われている乱数生成ルーチンに比べて)高速、プログラムもシンプルなようです。

いずれも共通しているのは、初期値を同じ値にすると同じ乱数列を出力するということです。
先ほども書いたように「正確に計算を行う」機械である以上当然ですが。
この性質はゲームのリプレイなどを保存するとき役に立ちます。
乱数の初期値だけ覚えておけば内部で乱数を使うゲームもリプレイを再現できるので。


C言語では何を使ってる?

C言語と言うか、C言語のライブラリではどんな手法を使っているのかをちょっと見て行きたいと思います。
最終的にはVBのRnd関数が目的なわけですが、ここでC言語に寄り道する理由として、「VB以上に広く使われている物だけにそれなりな乱数生成ルーチンが使われているのでは?」と言うのもありますが、最も大きいのが「ソースファイルが公開されている物もある」と言うものです。

ここで重要なのは、C言語の規格(ANSI-C)では、乱数については以下のことしか決まっていないと言うことです。
  • srand関数で初期値を指定し、rand関数で値を取得する。取得する値は0〜(RAND_MAX-1)の整数
これを見てわかるように、乱数の生成の手法が定められていないばかりか、乱数の範囲を示す定数RAND_MAXすら決まっていません。

実際にVC++、Cygwin、Fedore Core(Linux)の3つについて比較して見ます。
とりあえず先頭から10個の乱数を生成して見ました。
2行目はRAND_MAXの値です。
VisualC++6CygwinFedore Core
0x7FFF(32767)0x7FFFFFFF(2147483647)0x7FFFFFFF(2147483647)
41 01804289383
184671481765933 846930886
633410853777431681692777
2650012702162621714636915
1916911913915291957747793
15724 812669700 424238335
11478 553475508 719885386
29358 4453497521649760492
269621344887256 596516649
24464 7304172561189641421

みごとにバラバラです。(なお、MingwはVC++と一致した。)

Cygwinの1回目はなぜか0になってしまいました。
後述の計算式だと、最初の値が1であればの次に1481765933が出るので、1が出るならまだわかるのですが…

では、具体的にソースを見てみた結果です。

(Fedore Coreってglibcを使っていると言うことでいいんでしょうか?今回glibcのソースを見ました。
もし違っていたらすいません。ブート時にrundom number generatorが云々っていうメッセージもあった気がする・・・)

結果ですが、VisualC++とCygwinは線形合同法、glibcはパラメータ指定で線形合同法と他の手法を入れ替えられる、という状態でした。
なお、glibcはデフォルトでは線形合同法でないパラメータになっていました。
その線形合同法でないパラメータはなんだかよくわからず。

それぞれ以下のような計算をしているようです。
いずれも初期値は1と言うのは共通。(Cygwinで最初0が出たのは謎)

  • Visual C++

    Visual C++をインストールするとき、Cランタイムライブラリのソースコードを同時にインストールしておくと、VisualCの中のcrt\rand.cにソースファイルがインストールされているのがわかります。 出てくる値は0〜0x7FFFと16bitの値の範囲なのですが、内部的には32bitの値を持っています。
    そこで以下の計算を行います。
    modによる剰余が省略されていますが、自動的に2^32でmodされるものと考えてください。
    (C言語がわからない人は、「>>は右シフト、&はAnd演算子、returnは関数の返り値」と考えてください。
    
    x = x * 214013 + 2531011
    return (x >> 16) & 0x7FFF
    
    内部の値は32bitなのですが、それをそのまま出さず、上位15bitのみをrandの値として返します。
    このため、返り値だけから次の値がわからないという利点があります。
    例えば、rand関数の返り値が3だとしても、内部的には下16bitが隠れていて実際は30000〜3FFFFのいずれかの値を持っていることになります。
    当然次の値はこの下16bitの影響を受けるので、次のrand関数の返り値は異なる、というわけです。
    結果として、返る値は0x7FFF通りですが、周期は2^32となります。
    なお、この214013や2531011の根拠はわかりません……

  • Cygwin

    ソースコードは、Cygwinを提供しているサーバのgnu-win32/release/cygwin/cygwin-***-src.tar.bz2(***はバージョン番号)を展開して出てくるソース群のうち、cygwin-**/newlib/libc/stdlib/rand.cが該当します。
    Visual C++と同様に、rand関数の返り値と内部で保持する値の精度が異なっています。
    内部的には64bitの値を持っておき、出力は31bit分で0〜0x7FFFFFFFの値を返します。
    
    x = x * 6364136223846793005 + 1
    return (x >> 32) & 0x7FFFFFFF;
    
    この6364136223846793005と言うパラメータはTeXの考案者でもあるKnuth氏が書籍中で勧めてた値のようです。
    (Cygwinのソース中に書いてあるが、The Art of Computer Programmingの2巻。)

  • glibc

    stdlib/rand.c及びrandom.cが該当。
    上記の様に、デフォルトでは単純な線形合同法は利用していません。
    一応線形合同法を使う設定にすると以下の計算をするようです。
    
    x = ((x * 1103515245) + 12345) & 0x7fffffff;
    
    VisualC++やCygwinでは、返す値の倍程度の精度の値を内部で保持していましたが、こちらは内部で利用している値をそのまま外に返しています。
    外に出ている値から次が予測できるのはいまいちかも?

    と言うことで、デフォルトでは別の方法を利用しているようです。
    ソースだけから完全には理解できなかったのですが、過去の数回の履歴から、LFSR風にいくつか取り出して、加算などを行うと言うものの様です。
    (1回ごとに履歴を1個押し出すわけで、そこら辺もLFSR風っぽい。ソース中にもそう書いてあります。)

いよいよVBのRnd関数

C言語では線形合同法を用いたライブラリがまだ現役で動いている環境もあることがわかりました。
んで、ようやくVBのRnd関数を見て見ることにする。
まず、VBのマニュアルからRnd関数の仕様を確認すると・・・

Rnd([number])

Single型の数値(省略可能)を引数に渡すと、返り値として0≦Rnd<1のSingle値を返す。
numberの値によって以下のように処理が分かれる。
numberの値戻り値
numberの値から計算される値
正・省略時乱数系列の次の乱数を返す
0直前の乱数を返す

まぁ引数が0の時は直前の乱数ということで置いておいて・・・
まず、負の場合を考えて見ます。
負の場合は引数だけで返り値が決まり、前の値とかを考慮する必要がないからです。

とりあえず、ためしに-0.01〜-1、-1〜-100までを入力した時の乱数一覧を求めて見ます。
もし線形合同法であれば、簡単な線形の関係が出てくるはずです。
下の表では一部省略をしています。

引数Rndの値1つ上との差
-10.224007
-20.713326-0.510681
-30.963326-0.750000
-40.213326-0.750000
-50.838326-0.375000
-60.463326-0.375000
-70.088326-0.375000
-80.702644-0.385681
-90.515144-0.187500
-100.327644-0.187500
-110.140144-0.187500
-120.952644-0.187500
-130.765144-0.187500
-140.577644-0.187500
-150.390144-0.187500
-160.202644-0.187500
-170.108894-0.093750
-180.015144-0.093750
-190.921394-0.093750
-20〜-27省略-0.093750
-280.077644-0.093750
-290.983894-0.093750
-300.890144-0.093750
-310.796394-0.093750
-320.691963-0.104431
-330.645088-0.046875
-340.598213-0.046875
-350.551338-0.046875
-36〜-61省略-0.046875
-620.285713-0.046875
-630.238838-0.046875
-640.191963-0.046875
-650.168526-0.023438
-660.145088-0.023438
-670.121651-0.023438
-680.098213-0.023438
-69〜-100省略-0.023438
-980.395088-0.023438
-990.371651-0.023438
-1000.348213-0.023438
引数Rndの値1つ上との差
-0.010.338615
-0.020.838615-0.500000
-0.030.879896-0.958718
-0.040.327933-0.551963
-0.050.853915-0.474018
-0.060.379896-0.474018
-0.070.075624-0.304273
-0.080.838615-0.237009
-0.090.601605-0.237009
-0.100.364596-0.237009
-0.110.127587-0.237009
-0.120.890578-0.237009
-0.130.688788-0.201790
-0.140.564942-0.123845
-0.150.441097-0.123845
-0.160.317252-0.123845
-0.170.193407-0.123845
-0.180.069561-0.123845
-0.190.945716-0.123845
-0.200.821871-0.123845
-0.210.698025-0.123845
-0.220.574180-0.123845
-0.230.450335-0.123845
-0.240.326490-0.123845
-0.250.224007-0.102483
-0.260.167425-0.056582
-0.270.110843-0.056582
-0.280.054261-0.056582
-0.29〜-0.47省略-0.056582
-0.480.922622-0.056582
-0.490.866040-0.056582
-0.500.809458-0.056582
-0.510.727760-0.081698
-0.520.699469-0.028291
-0.530.671178-0.028291
-0.540.642887-0.028291
-0.55〜-0.97省略-0.028291
-0.980.398084-0.028291
-0.990.369793-0.028291
-1.000.341502-0.028291

これを見て、はっきりと癖がわかるのが左側の-1〜-100での変化の場合です。
差の値に注目して見ると・・・
「入力する値が変化するにつれ、差が段々半分になっていってる」
「差が変化する間隔が倍々になっていってる」
境目はノイズっぽい値が混ざってはいますが、両方の性質が見て取れないでしょうか。
右側の-0.01〜-1.00での場合も、差の変化する位置は違うものの、両方の性質が見られます。
-1〜-100の例では、どうも-4、-8、-16、-32、-64あたりで差が変化してるような気がします。

さて、ここからRnd関数の法則を少しでも探って見ることにします。
問題は、「差が段々半分」「その間隔は倍々」と法則性があるんだかないんだかわからないこの差です。
もしかしたら検討のついた人がいるかも知れません。
それは、「Single値とか言っておきながら、実際のビットパターンに比例するんじゃないの?」と言うことです。
以後の話を理解する上で、小数がコンピュータ内でどう表現されているかを知っておく必要があります。
以下に簡単に書いておきます。
知ってる人は飛ばしても大丈夫です。
(これに関してはGoogleで検索すると色々出てくるのでそちらを確認してもいいかも。。

32bit浮動小数の表現

小数の表現法には色々ありますが、通常はIEEEの規格に則った方式が一般的です。
プログラムなどでは小数は3.512E+2のように、1〜10未満の数×10の累乗という形式で表記することがありますが、小数も2進数で似たように表記します。
例えば、1.0は1.0×20、3.0は1.1×21と表現できます。(2進数の1.1=1+1/2は10進数の1.5に相当)
×の前の部分は1.0〜2.0未満になるようにします。
×の前の部分(上の1.0や1.1)を仮数、2の累乗のところにある0や1を指数と言います。

32bitの浮動小数では、1bitを符号(0なら正、1なら負)、8bitを指数、23bitを仮数に割り当てることになっています。

指数 仮数

指数は8bitで0〜255まであるわけですが、0と255は予約されていて、実際は1〜254が利用できます。
1.0より小さい数を表現することもあるので、実際の値に127ゲタを履かせた値を格納します。
例えば、1.0なら1.0×20なので0+127=127、3.0は1.1×21なら1+127=128となります。
仮数は1.0〜2.0未満と言うことで、整数部は必ず1になります。
そのため、整数部の1を省略して、後の小数部を23桁分記録します。
この省略された整数部と仮数23bitで計24bitの精度があるため、32bit浮動小数の有効数字は7桁(log10(224)=7.22)となります。

先ほどの1.0・3.0の例ではビットパターンは以下のようになります。
1.0 = 1.0×20なら符号は正で0、指数部は0+127=127、仮数は小数部は全部0なので、
0 01111111 00000000000000000000000
→ 3F800000(16進数)

3.0 = 1.1×21なら符号は正で0、指数部は1+127=128、仮数は小数部は最初1、あとは0なので、
0 10000000 10000000000000000000000
→ 40400000(16進数)

0は指数部も仮数部も全部0にする事で表現します。
他に余っている指数部の255を使ってNaN等を表現します。
Doubleのような64bit浮動小数では符号1bit、指数部11bit、仮数部52bitとなります。


上のことを考えて見ると、指数部は確かに-2、-4、-8、-16、-32、-64で変化するので、そこで差が変化するのは納得できるし、指数部が大きくなってくると、値が-1ずつ変化した時の仮数部の変化が段々小さくなります。
このことから、「ビットパターンそのものに比例するんじゃないの?」と予測。
小数の値を一定間隔で変化させるのではなく、ビットパターンを一定間隔で変化させて見ました。

左側はビットパターンを16進数で80000001から1ずつ増加、右側は80100000から100000ずつ増加させて見た場合です。
最初が8になっているのは、浮動小数で先頭にある符号ビットを1にするためです。

引数(括弧内は実際の値)Rndの値1つ上との差
80000001(-1.40E-45)0.386250
80000002(-2.80E-45)0.375568-0.010681
80000003(-4.20E-45)0.364887-0.010681
80000004(-5.61E-45)0.354206-0.010681
80000005(-7.01E-45)0.343524-0.010681
80000006(-8.41E-45)0.332843-0.010681
80000007(-9.81E-45)0.322162-0.010681
80000008(-1.12E-44)0.311480-0.010681
80000009(-1.26E-44)0.300799-0.010681
8000000A(-1.40E-44)0.290118-0.010681
8000000B(-1.54E-44)0.279436-0.010681
8000000C(-1.68E-44)0.268755-0.010681
8000000D(-1.82E-44)0.258074-0.010681
8000000E(-1.96E-44)0.247392-0.010681
8000000F(-2.10E-44)0.236711-0.010681
80000010(-2.24E-44)0.226030-0.010681
80000011(-2.38E-44)0.215348-0.010681
80000012(-2.52E-44)0.204667-0.010681
引数(括弧内は実際の値)Rndの値1つ上との差
80200000(-2.94E-39)0.021931
80400000(-5.88E-39)0.646931-0.375000
80600000(-8.82E-39)0.271931-0.375000
80800000(-1.18E-38)0.896931-0.375000
80A00000(-1.47E-38)0.521931-0.375000
80C00000(-1.76E-38)0.146931-0.375000
80E00000(-2.06E-38)0.771931-0.375000
81000000(-2.35E-38)0.386250-0.385681
81200000(-2.94E-38)0.011250-0.375000
81400000(-3.53E-38)0.636250-0.375000
81600000(-4.11E-38)0.261250-0.375000
81800000(-4.70E-38)0.886250-0.375000
81A00000(-5.88E-38)0.511250-0.375000
81C00000(-7.05E-38)0.136250-0.375000
81E00000(-8.23E-38)0.761250-0.375000
82000000(-9.40E-38)0.375568-0.385681
82200000(-1.18E-37)0.000568-0.375000
82400000(-1.41E-37)0.625568-0.375000
左側を見る限り、見事にビットパターンと線形の関係にあるっぽいことがわかります。
ただ、右側を見ると0x1000000ごとにちょっと値がずれていることがわかります。
しかもその幅(-0.385681と-0.375000の差)は左側の差の-0.010681と一致します。
一体これは何なんでしょうか・・・


ランタイムの解析

上の様子を見る限り、ほぼ線形合同法っぽいですが、一部よくわからないところもあります。
そこで、ランタイムを実際に解析して見ました。
VB5の場合は、msvbvm50.dll内のrfcRandomNextと言う関数がそれに該当します。

この関数ですが、最初の方は何をやっているかわかりませんでした。
(一応、一番最初は引数が省略されている場合はビットパターン3F800000 = Singleで1を引数の来るべきだった場所に入れる、という処理を行っています。引数省略時と、引数が正の場合は同じ処理を行うのでこれはマニュアルどおりと言えるでしょう。) もしかしたらRandomizeステートメントの影響がここに出てくるのかも知れません。
その後の処理は以下のようになっているようです。
「現在の値」と言うのは内部で保持している32bitの整数です。

(注意する事として、入力した値を生のビットパターンで解釈したり、小数の値として解釈したりしているのを理解する必要があります。
例えば3F800000は16進数の整数だと見ると1065353216になりますが、小数を表してるととると1.0になります。
これは内部でCPUがどの命令で該当するデータを処理しようとしているかに依存します。)

  1. もし入力値が0だったら(具体的には、7FFFFFFFとのANDをとって0だったら)5までとぶ
  2. 入力値を浮動小数として解釈し、0と比較することで符号を見る。(上の0かどうか生のビットパターンを見るのに比べ、こちらは実際の小数の値で比較する)
  3. もし0未満、符号が負であれば以下の処理を行う。(小数でなく、ビットパターンのままで行う)
    『 現在の値 = ((入力値 >> 24) + 入力値 ) And &H00FFFFFF) 』
  4. 次の式で「現在の値」を更新する。
    『 現在の値 = (現在の値 * &HFFFD43FD - &H003C613D) And &H00FFFFFF 』
  5. 現在の値を小数に変換、ビットパターン33800000から生成される小数2-24を掛けて、そのまま関数の返り値として返す。
入力値が正・0・負の時それぞれ流れを見て行きます。

  • 入力値が0なら、上の1の比較によって5の処理までとぶ。
    「現在の値」は前回Rndを呼んだ時と変わらないので、前回と同じように2-24を掛けて返す。
    そのため、前回と同じ値が返る。

  • 入力値が正なら、「現在の値」に対して4の計算を行った後、2-24を掛けて返す。

  • 入力値が負なら、「現在の値」を3の式で生成した後、4の計算を行い、2-24を掛けて返す。

3の式でも4の式でも最後に00FFFFFFとのANDをとっているため、「現在の値」は実質24bitの整数となります。
上記の通り、Single型の精度は24bitとなっているので、これにあわせたんでしょう。
24bitなら0〜224-1の値をとれるので、最後に2-24を掛けることで0〜1未満の値を返すことができます。

これで計算の式がわかったことになります。
4を見てわかる通り、線形合同法まんまですね。(FFFFFFとのANDは1000000でMODとるのと同じだし)
3を見ると、入力値が1000000変わる毎に出力がちょっとずれるのもわかります。
ちょっと気になるのが、VC++なんかは内部的に32bit持って置いて、16bit分だけ返り値を返しているのですが、こちらは内部的にも24bit、返り値も24bitの精度で値を保持することになります。
せっかく8bit余ってるんだから、内部的に32bitで持って置けば前のRndの返り値から次の値を予測できない(256通りになる)と思うんだけどなぁ・・・


Randomizeの謎

(2007/08にRandomizeの式について、後述のVBでRand/Randomizeを実装に付け足しました。)
ここまで避けていた話題に、Randomizeステートメントがあります。
C言語の場合、乱数のシード値を指定するsrandとrandの関係は単純です。
srandはrandで利用する「現在の値」をそのまま更新するだけの物です。
VBでは、Rnd関数を負の値を引数として呼ぶと「現在の値」を更新できるので、これはsrandと近い性質を持っています。

Randomizeは引数をとるのですが、同じ引数を入れても後の乱数が必ずしも同じにならないという点があります。
同じ乱数列を再現するにはRandomizeは向きません。(最初に負の値でRndを呼んだ後、あとは引数を省略するのが良い)

とりあえずマニュアルを見ると以下のようにかかれています。
乱数ジェネレータを初期化 (乱数系列を再設定) する数値演算ステートメントです。

引数 number には、Rnd 関数の乱数ジェネレータに与える新しいシード値を指定します。引数 number を省略した場合、システム タイマーから取得した値が新しいシード値として使われます。
Randomize ステートメントを使用しない場合、引数を指定しないで Rnd 関数を呼び出すと、最初に Rnd 関数を呼び出したときのシード値と同じ値が使用されます。それ以降は、直前に生成された数がシード値として使用されます。
原因はDLLを解析しても良くわかりませんでした。
とりあえず実際にRandomizeを呼んで見てわかったこと。
同じ値をRandomizeに入れても、続くRnd関数の数列は異なる物になる。 これはC言語のsrandとは明らかに違います。
ただし、異なるとは言え、そのRandomizeまでに呼んだRnd関数の履歴が同じならば同じ数列を生成するというのがあります。
言い換えると、「これまでのRandomize関数の呼び出し方には依存しない。(最後に呼んだRandomizeの引数のみが影響する」「過去のRnd関数お呼び出し回数やその引数が異なると、Randomize関数の引数が同じでもRandomize後のRnd関数は異なる値を返す。」と言うことがあります。

結局「よくわからん」というオチです。
別にRandomizeで線形合同法のパラメータが変わるわけでもないですし・・・
もしRandomizeについて詳しい方がいたら教えていただけないでしょうか。


VBでRnd/Randomizeを実装

(2007/08加筆)
よくわからなかったRandomizeですが、いっちゃんさんより解析結果をいただきました。
まず、Cで書くと以下のようになるとのことです。

/* ------------------------------------------- */
/* RndTest.c */
#include <windows.h>
static long x=327680;

__declspec(dllexport) float CALLBACK Rnd2(float f)
{
    unsigned u=*(unsigned*)&f;
    if (u&2146828287) {
        if (f<0) x=(u>>24)+u;
        x=x*16598013+12820163&16777215;
    }
    return x*(1.0/16777216.0);
}

__declspec(dllexport) void CALLBACK SRnd1(double d)
{
    unsigned short*s=(unsigned short*)&d;
    x=((s[2]^s[3])<<8)|(x&255);
}

__declspec(dllexport) void CALLBACK SRnd2(float f)
{
    unsigned char*c=(unsigned char*)&f;
    x=((c[0]^c[2])<<8)|((c[1]^c[3])<<16)|(x&255);
}

int WINAPI DllMain(HINSTANCE a,DWORD b,PVOID c) { return TRUE; }
/* ------------------------------------------- */

' ---------------------------------------------
' 標準モジュール
Declare Function Rnd2! Lib "RndTest" (Optional ByVal a! = 1)
Declare Sub SRnd1 Lib "RndTest" (ByVal a#)
Declare Sub SRnd2 Lib "RndTest" (ByVal a!)
Sub Randomize2(Optional ByVal a)
    If IsMissing(a) Then SRnd2 (Timer()) Else SRnd1 (a)
End Sub
' ---------------------------------------------


これをRnd/Randomizeともに自分なりにVBにしたところ、以下のようになりました。
VBではUnsigned型がない上、そのままではオーバーフローしてしまうのでだいぶいじりました…

Public Declare Sub CopyValtoVal Lib "kernel32" Alias "RtlMoveMemory" _
  (Destination As Any, Source As Any, ByVal Length As Long)
Dim num As Variant
Dim first As Boolean

Private Sub CheckFirst() '初期値の設定 If Not first Then num = CDec(327680) first = True End If End Sub
Function MyRnd(ByVal v As Single) As Single Dim n As Long, d As Variant Call CheckFirst If v <> 0 Then If v < 0 Then '入力が負なら、その値で現在の値を更新。 'singleをlongとして読む Call CopyValtoVal(n, v, 4) 'VBにはUnsigned longがないので、Dec型に変換 dec = CDec(n And &H7FFFFFFF) + CDec(&H10000000) * 8 '最上位ビットは別扱い num = dec + Int(dec / &H1000000) 'num = dec + (dec>>24) End If 'num = (num * &HFD43FD + &HC39EC3) mod &h1000000 をmodナシで行う num = num * CDec(&HFD43FD) + &HC39EC3 num = num - CDec(Int(num / &H1000000)) * &H1000000 End If MyRnd = num / &H1000000 End Function
Sub MyRandomize(Optional ByVal v As Double) Dim s As Single, i As Integer, l As Long Dim b(3) As Byte, inte(3) As Integer Call CheckFirst If IsMissing(v) Then '引数なしの場合 'バイト配列に変換 s = Timer Call CopyValtoVal(b(0), s, 4) num = ((b(0) Xor b(2)) * &H100&) Or ((b(1) Xor b(2)) * &H10000) Or (CLng(num) And &HFF) Else '引数ありの場合、doubleをintege配列に変換 Call CopyValtoVal(inte(0), v, 8) i = inte(2) Xor inte(3) ' unsigned int→unsigned long化 l = (i And &H7FFF&) + IIf(i And &H8000, &H8000&, 0) num = (l * &H100) Or (CLng(num) And &HFF) End If End Sub

まとめ

コンピュータに置ける乱数生成法は色々ありますが、中でも線形合同法は実装も簡単で一様な乱数を高速に作る事ができます。
そのため、特にソフトウェアで乱数生成ルーチンを実装する際に良く使われるようです。
実際、いくつかのC言語の環境でも使われていましたし、今回のVBも線形合同法と言っていいでしょう。

(2007/08修正)
Randomizeがすっきりしないのが不満…だったのですが、いっちゃんさんの解析により中身が判明しました。
Rnd関数に比べるとRandomizeはだいぶ複雑ですね。

04/07/20 2度目の自作編
マシンを買い換えました。
まぁ一応自分の買った製品名とかの備忘録ということでだらだら書いてきます。
もしかしたら近いうちに安めの値段でPC買う人の参考には・・・ならないかな(^^;

前回は雑誌を読んでほぼそのとおりに構成したものの、今回はほぼネットの情報だけでチャレンジ。
とりあえずチップセットの名前とかぜんぜんわかんないのでそこら辺を1週間程度調べた。

パーツ購入

予算は7万円ということで土日秋葉原を回ってきました。
買ったもの・金額・店は以下のとおり。

パーツ商品名店名値段
(ケース)TUKUMO ST-5688TUKUMO ex7,980円
(VGA)玄人志向 GFX5700-A128CETUKUMO ex13,650円
(M/B)AOpen AK77-600NTUKUMO ex7,547円
(CPU)Athlon 2500+ BOXPC-Success 2号店9,030円
(OS)Windows XP Home OEM版PC-Success 2号店11,580円
(MEM)SanMax=Hynix PC3200 CL3 512MB(相性保証付)OVERTOP10,390円
(HDD)Maxtor M6Y160P0 160GBクレバリー9,628円
69,805円

無事に予算内。
TUKUMO exでは開店前から並ぶと3000ポイント還元のに並んだので、3000円分それも残った。
まぁ3000ポイント還元は3000円引きとは違うけど。

ケースが重いので2往復必要ということになり、まず土曜にケース目当てでTUKUMO exに朝から並んだ。
ケースはいいとして、買うべきものはどのパーツも約1万円。
TUKUMOの今回のポイント還元は2万円以上購入で3000ポイント還元だったので、あと12020円分何か買わないといけない。
他のものは他の場所の方が安く買える場所も多く、できるだけ他店と価格差が少なく、かつ2万円を大きく超えないということで、結局グラフィックボードにした。
GeForceFX5700LEぐらいかなと思ってたけど、GeForceFX5700(ただし、玄人志向のものはメモリクロックが通常55MHzに対し400MHzと少な目)のものを買った。
午後からは用事があったのでこの日はケースとVGAだけで終了。

で、2日目。
今度はいろいろ回るので午後からゆっくり。
人が多いし暑いしで疲れました。
まずCPU、これは大体の店で9000円台後半でした。
Athlonリテールのファンはうるさいと言うことだったんですが、2500+あたりからそうでもないということでBOXでいくことに。
ちなみに、PC-Success2号店では最後の1個でした・・・
WinXPもここがかなり安かったのでここで購入。

次にメモリ。
どうもOVERTOPがかなり安いという情報があったものの、偶然通りかかった道に同じく安いという情報のあったテクノハウス東映があったので見てみた。
バルク品としては評判のいいらいいInfineon純正のものが10400円だった。
ただ相性保障とかついてるかどうか忘れたんで、結局OVERTOPでSanMax=Hynixを購入。
全体的にどの店もバルクでないものはSamsung製のものが多かった。
最近のSamsung製はいまいちという話があったので避けましたが。

意外に迷ったのがHDD。
ATA133なのかATA100なのか書いてない店が多い。
書いてある店で型番を確認しつつ、ATA133、7200rpm、キャッシュ8MBのHDDを買いました。
後でわかったことだけどPC-Successの方が8円安かった。
HDDはSofmapやTUKUMOみたいなところでも100円とかしか変わらないから、ポイントカードあるならそっちでもよかったかも。
10000円超えてる店はほとんどなくてみんな9000円後半。

最後にマザーボード。
値段と安定性ということでKT600のものを探してたけど、Athlonは半分が64向け、残りがnForce2とKT880チップセットのものが多かった。
小さめの店だとKT600モノは扱ってないね。
とりあえず目をつけてたAK77-600Nを売ってたのがSofmapが7770円、PC-Successが7800円で、TUKUMOは7780円だったんだけどタイムサービス3%引きだったので7547円で購入。
この3%引きのおかげで最終的に予算内に収まりました。

2日ともTUKUMOに並んでた方がよかったかな・・・
ただ、HDDはともかく、CPU・OSは3000円ポイントもらってもWinXP Homeが14000円越えじゃPC-Successの方が安いからなぁ。

組み立て・OSのインストール

とりあえず組み立て。
組み立てていてしまったと思ったのはケースのST-5688。
小さいのがいいかなと思って6688でなく5688にしたが、CPUのファンのすぐ上を半分電源が覆う。
結局CPU・SYSTEMの温度は50度程度なんでまぁ平気といえば平気かもしれないが、クロックアップとかクーラーを変えるとかは厳しそう。
中の配線もその分かなりやりにくい。
これは調査不足だったな・・・店頭で中身が見えなかったのもまずかったかも知れない。
6688もほとんど値段変わらないしな・・・

まぁ電源周りに苦労しつつも順調に組み立て。
電源を入れる・・・がBIOSが表示されない。
一応DVDドライブとかはしばらくなんかやってるので、画面に出ないだけで起動はしている模様。
マザーボードが悪いのかVGAが悪いのかわからなかったが、今までのGF2MXでも表示されない。
30分ぐらいいろいろやって、結局CMOSクリアをちゃんとやったら表示された。

最初FSBが100の設定になってたので、AthlonXP 1100MHzとか表示されてびっくり。
まぁすぐ166にしたけど。

しかし、上にあったようにAthlonのリテールファンは(最近マシとはいえ)うるさめと聞いていたが、今までよりよっぽど静かだ・・・
まぁ前は5400rpm、今回は3200rpmだから当たり前といえば当たり前だけど。


で、次はOS。
時間はかかるけど特に難しいことはない。
ここで、アカウント名を日本語でつけてしまって後で後悔した。


環境の移行
今までのメインHDDを付けて片っ端からファイルを移す。
設定もいろいろいじって結局今までのWin98とほぼ同じ環境に。

で、まず判明したのがグラフィックボード周りのバグ。
まずDirect3Dを使ってるソフトを動かすと三角形の点が飛んだり、テクスチャにごみが入ったりして画面が非常に乱れる。
これはドライバをごちゃごちゃ入れ替えてたら直った。
さらに、またドライバを元のにしても直ってた(^^;

次に開発環境の移行。
VB5はXP非対応だがとりあえずインストールは順調に終了。
ただし、ボタンなどのカスタマイズがなぜか毎回リセットされてしまうな・・・

次にVC++6、これが参った。
「oledb32.dllがレジストリに登録できませんでした」とかいってインストールが成功しない。
ほかにもデータベース関連の機能をインストールしようとするといくつかのファイルで同じ現象が発生した。
とりあえず開発環境の起動とかはできるんだが、インストールが完了してくれないとSP6を入れることができない。
結局CDの中身をいったんHDDにすべて移し、インストールの手順らしきものが記述されているテキストファイルをいじってoledb32.dllを処理しないようにしてインストール完了。
インストール手順がテキストファイルでよかったな。

で、そこらへんやってたらVS.NET the Spokeのプロダクトキーが来たのでインストール。
MSDN Libraryと合わせてCD5枚あるだけあって時間かかるな・・・
C#やVB.NETはしばらくおいといて、VC++.NETに移行するか検討中。
ソースを出さないものは移行してもいいかな?(今VC6とVC.NETのどっちが使われてるんだろ)

ほかごちゃごちゃソフトをインストール。
以前のWin98のレジストリの内容をテキスト出力してきたので、ソフトの設定はそれをそのままコピーして利用できるものもある。

で、上で日本語のアカウント名を付けて失敗したなと思ったのが、一部の英語のソフトだとGetTempFileNameで一時ファイルを作成するとき、Win98と違い、Documents and setting\アカウント名の下にtempディレクトリがあるため、そこでこける。
しかもあとでアカウント名を変更してもこのディレクトリ名は変更できないのね・・・
Cygwinとかでもなんか挙動がおかしかった気がする。
結局今のアカウントは消して新しくアルファベットでのアカウントを作り、そっちにいろいろと移行。

一部のゲーム(まぁMicro Warもそうなんだが)タイマーの精度がWin98と違い、ウェイトが正しくかからないのね・・・
timeGetTimeの精度がWinXPだと10msらしく、ウェイトを17msにして約60FPSにしようとしたらタイマーの精度のせいで実質20ms→50FPSとなってしまった。
近いうちに直す予定。
とかくWin98とWinXPの違いに戸惑ってます。
(大学とかではWinXP使ってるけど、やはり無理にWin98の環境を持ち込もうとしているのが無理があるんだろう・・・)

ベンチマーク
とりあえずHDBENCHをやってみた。
上は今回作ったPC、下はこれまで使ってたAthlon950MHz+GeForce2MX。
BitBlt・DirectDrawはあまり参考にならないとして、CPU・メモリは倍近くなっている。

   ALL  Integer   Float  MemoryR MemoryW MemoryRW  DirectDraw
 45539    77806   94882    26359   37569    43310          60

Rectangle   Text Ellipse  BitBlt    Read   Write    Copy  Drive
    56760  44991    7554      56   51638   55231   41967  C:\100MB

   ALL  Integer   Float  MemoryR MemoryW MemoryRW  DirectDraw
 21198    40726   49479    15606   19447    24663          59

Rectangle   Text Ellipse  BitBlt    Read   Write    Copy  Drive
    37792  19980    5413     997   18927   17885   22346  C:\20MB

ほかにも3DMark2001を動かしたりしてみたけど、木や草のある画面がかなりスムーズに動いていい感じ。
とはいえ、(メモリクロックが低いこともあるんだろうけど)3DMark03だと遅くなる部分もある。
N-bench3もGeForce2GTSの倍程度のスコアしかでないしな・・・

久々に最近のデモでもいろいろダウンロードしてみようかなとか考えてます。

04/06/15 VBでOpenGL編−1(初期化と簡単な描画)[★★◎◎]
(VBでOpenGLとありますが、VCでもほとんど同じ方法で利用出来ます。定数名とかが多少違う程度。)
過去様々な3Dレンダリングエンジンがありましたが、なんだかんだでWindows用ではDirectXが広がってます。
ただ、DirectXはcomで構成されていることもあり、利用できる言語も多少選ぶし、バージョンアップが激しいし初期化が面倒(かなり改善されて来てますが)ということがあります。
そこで、ちょっとした3DのプログラムならDirectXよりずっととっかかりが簡単ということでVBでOpenGLをいじる方法を調べてみました。

Direct3Dとの比較 関連ライブラリ 必要なファイルの前準備 OpenGLの初期化・終了 ちょっと描画してみる

Direct3Dとの比較

まずOpenGLをWindowsで使おうとすると、Direct3Dが気になります。
一応簡単な比較をしておきます。(結構主観が入ってますが)

 OpenGLDirect3D
描画の速さ速いがD3Dよりは遅い速い
プログラムからの呼び出し方普通のDLL呼び出しCOM(Component Object Model)
バージョンアップ遅い早い
互換性高い古いコードの利用はできるが
新機能を使うとコード大幅入れ替え
精度一般に高いといわれる普通
主な用途CAD・研究用ゲーム
機能シンプル豪華
ハードの相性あまりないかなりある
座標系右手座標系左手座標系

  • まず速さですが、一般向けのグラフィックボードの場合はドライバを作るメーカーがDirect3Dに力を入れていることが大きいようです。
    一方、CAD等ゲーム以外のプロ向けのボードだとOpenGLに力が入ってるようで。

  • プログラムからの呼び出し方ですが、OpenGLは単なるDLL呼出なのでVBからでもDeclare文で定義し、呼び出せます。

  • バージョンアップでは、OpenGLは複数の会社で管理してる事もあり少しずつしかバージョンアップされません。
    ただし、各社が勝手に拡張機能を追加できるようになっており、特定のグラフィックボードでのみ動く機能などは色々あります。
    DirectXは基本的にMicrosoft1社で管理してるので、バージョンアップも早いですね。

  • 互換性ですが、OpenGLは下位互換性がかなり強いです。
    バージョンアップ時は拡張機能の追加みたいな感じで行われます。
    DirectXの場合、新しい機能を使おうと思ったら関連オブジェクトを全て新しい方に合わせないといけません。

  • 精度は用途を考えればOpenGLの方がいいのは当たり前かと。

  • 機能面ではOpenGLはシンプルな機能しか持っていません。
    例えば、3Dの物体を描画するためには頂点や面の一覧を管理しなければ行けませんが、そこらへんは自前で行う必要があります。
    DirectXではある程度オブジェクトが用意されています。
    ただ、その分DirectXでは憶えることも多くなってしまうのでどちらがいいかは人によると思います。

  • ハードの相性ですが、OpenGLはかなり少ないと思います。
    Direct3Dだと「どこそこのグラフィックボードだと動きません」なんていうのも多いですし。

  • 座標系も結構重要。
    OpenGL / DirectXにおける3D基礎概念の対比にもありますが、+X(親指)が右向き、+Y(人差し指)が上向きの場合、OpenGLは+Z(中指)は手前(右手)、DirectXは奥(左手)になります。
    まぁ相互変換可能ですが。
これらを色々考えると、「本格的にゲームを作るにはちょっと足りないが、3Dの勉強にはOpenGLの方がいいかも」という感じになるかと思います。


OpenGLでのプログラミングでは、C++/GLUTという環境用のものですがOpenGLプログラミングコースというわかりやすいテキストがPDFで公開されています。
多少C言語もわかると言う方は、このPDFを見ると3Dに必要な概念なども細かく解説されていて分かりやすいのでオススメです。
(と言うか、今回はGLUTで隠されてる初期化回りをやるのでともかく、次回のマテリアルとかはこのマニュアルで十分にわかる)


関連ライブラリ

先にOpenGL関連ライブラリの話について書いておきます。

OpenGLのコアであるGLは最低限の描画機能のみを提供しています。(関数名の最初がglで始まる)
通常の動作環境では描画回りを少し拡張したGLUライブラリが利用できます。(gluで始まる)
OpenGLの下にはプラットフォーム依存の処理を行うライブラリがプラットフォームごとに準備されています。
(X Window SystemならGLX、WindowsならWGL、OS/2ならPGL・・・今回はWindowsなのでWGLのみ関連)

いずれも描画回りの処理までしか持っていないと言うことで、ウインドウ処理などを行えるライブラリということでAUXが開発され、さらに実用的なものということでGLUTが開発されました。
これらはOSの差異を吸収してくれますが、逆に各OSの最大公約数の処理しか行えないことになります。
C言語でのOpenGLの学習には最適ですが、Windowsならではの機能を用いたアプリケーションを作ろうとしていて、かつ自分で(直APIにしろMFCにしろ)ウインドウ処理なども行える人はGLUTなしでもOKでしょう。
GLUTはウインドウの処理などを行うものなので、VBではそれほど利点はありません。

また、今回は関係ありませんが、GLUT上でテキストボックスや視点の変更のようなコントロールを実現するためのライブラリとして、GLUIがあります。
GLUTを使いつつある程度ユーザーインターフェースも備えたい方はこちらを使うといいでしょう。

大雑把に図にすると以下の感じです。

OpenGLを利用する場合、最低限必要なのはWGLとGLのみです。
WGLはそもそもWindowsからOpenGLを利用するための関数を提供しています。
GLUはGLの上でより高機能な描画機能を提供します。描画しか行わないので、GLの上にのっかっています。
一方、GLUTはウインドウ作成やキーボードなどのイベント処理など、描画に関係ないことも行うので、Windowsに直接関連する処理を行います。
GLU・GLUTはなくてもOpenGLのプログラムは作成できます。

Windows95ではOpenGLをサポートしていないので、OpenGLランタイムを導入する必要がありますが、Windows98以上では最初から導入されています。
ここで注意するのは、Windows98以上でデフォルトで使えるのはOpenGL+GLU+WGLまでということです。
GLUTは別に入手する必要があります。

Windows用のGLUTはGLUT for Win32でダウンロードできます。
GLUTはDLLで提供されているため、GLUTを利用する場合はDLLごと配布する必要があります。
幸い、「freely distributable without licensing fees.」とあるように再配布は自由です。

VBではGLUTはあまり意味がありませんが、一部は利用可能な命令もあります。
GLUTの命令一覧はThe OpenGL Utility Toolkit (GLUT) Programming Interface API Version 3で見ることが出来ますが、大半がウインドウ・メッセージ制御関連の機能です。
一部glutSolidSphere等のような描画回りの機能がありますが、このためにDLLを追加するかどうかは迷うところ。
後述のタイプライブラリにはGLUTの宣言も含まれていますが、実際にGLUTの命令を呼んでいなければ開発時・実行時にはDLLは必要ありません。


必要なファイルの前準備

OpenGLはDLL呼びだしで利用するのでDeclare文の定義が必要です。
また、Declare文の定義の替わりにタイプライブラリを使う方法もあります。
一応両方の入手先を書いておきます。

VB + OpenGL - VBGLApi.bas

こちらはDeclare文で宣言している.basファイル。
利点 − .basファイルなので編集も簡単、開発環境も簡単に移せる。定数名がC言語に合わせてある。
欠点 − WGL・GLUは一部の関数のみ宣言。GLUTはそもそも扱っていない。

Programming OpenGL

こちらはタイプライブラリ。
OpenGLだけ含むバージョンと、他のWindowsAPIも含むバージョンがある。
他のWindowsAPI宣言のタイプライブラリを使っていないなら後者がオススメ。

利点 − OpenGL・GLU・GLUT・WGLの関数をサポート。WindowsAPIも含む。
欠点 − タイプライブラリなので中身をテキストで編集したりできない。一部定数がC言語版と名前が異なる。

両方見ると色々ありますが、後者の方がオススメです。
APIビューアとかで他のAPIの宣言を追加する手間も省けるし。
ただ、他の環境で開発する際にはタイプライブラリも持って行く必要がある点では面倒。


OpenGLの初期化・終了

今回は後者のタイプライブラリを利用します。
上記のサイトから「VBOpenGL 1.2 for Microsoft」をダウンロードし解凍して出てくる「vbogl.tlb」をc:\windowsでもVBのプロジェクトと同じ所でもいいので置いておきます。
VBでプロジェクトを作る際、「プロジェクト」→「参照設定」し、「参照」で先ほどのvbogl.tlbを読みこみ、「VB OpenGL API 1.2」を使えるようにしておきます。
この状態だと、オブジェクトブラウザで関数も見ることが出来ますし、プログラム時に関数の引数リスト表示などの支援機能も働いていい感じです。


(補足)
タイプライブラリは開発時のみ必要であり、最終的なEXEファイルを実行する時には必要ありません。
ただ、複数の環境でソースをいじる場合はちゃんとタイプライブラリが使える状態でないといけません。

で、OpenGLは描画先を「コンテキスト」と呼びます。(GDIにおけるHDCとほぼ同じ)
この「コンテキスト」は通常HDCに対して1つ作ります。
なので、描画先はHDCを持つことのできる、ピクチャーボックスやウインドウになります。
初期化及び終了の処理は以下の通りです。(各構造体やAPIに関しては、MSDNライブラリに書いてあります)

◆初期化

PIXELFORMATDESCRIPTOR構造体を適切に埋め、ピクセルフォーマットを設定

ChoosePixelFormatで上記のピクセルフォーマットが利用可能か問い合わせ

SetPixelFormatで指定するHDCのフォーマットを指定

wglCreateContextでHDCに対応するコンテキストを生成

wglMakeCurrentで作成したコンテキストをカレントにする

あとはOpenGLの機能を使う

◆終了

wglMakeCurrentで別のコンテキスト(もしくはNULL)をカレントにする

wglDeleteContextでコンテキストを削除

PIXELFORMATDESCRIPTORは色々設定項目がありますが、後は引数もわずかです。
DirectDrawやDirect3Dよりははるかに初期化は簡単だと思います。
具体的には、以下のような感じ。

ここでは、ピクセルフォーマットはカラー32bit(RGBAそれぞれ8bit)、Zバッファ16bitを指定しています。
PIXELFORMATDESCRIPTORでは、他にアキュムレーションバッファやステンシルバッファも指定できますが、ここでは利用しません。
(PIXELFORMATDESCRIPTORの詳細は、MSDN Library等で見てください。)

'OpenGLのコンテキストハンドル
Dim hRC As Long

'OpenGLの初期化を行う Function InitOpenGL(ByVal hdc As Long) As Boolean Dim pfd As PIXELFORMATDESCRIPTOR Dim res& 'ピクセルフォーマットの指定 With pfd .nSize = Len(pfd) .nVersion = 1 'OpenGLでダブルバッファを使用 .dwFlags = PFD_DRAW_TO_WINDOW Or PFD_SUPPORT_OPENGL Or PFD_DOUBLEBUFFER 'パレットでなくRGBAで。 .iPixelType = PFD_TYPE_RGBA .cColorBits = 32 'RGBの各ビットは指定しなくてもよい 'アキュムレーションバッファやステンシルバッファも今回は使用しない 'Zバッファだけ使用する .cDepthBits = 16 .iLayerType = PFD_MAIN_PLANE End With 'ピクセルフォーマットが利用可能か? res = ChoosePixelFormat(hdc, pfd) '0なら失敗 If res = 0 Then Exit Function 'ピクセルフォーマットの指定 res = SetPixelFormat(hdc, res, pfd) If res = 0 Then Exit Function 'OpenGLのコンテキストを生成する hRC = wglCreateContext(hdc) If hRC = 0 Then Exit Function '今作成したコンテキストをカレントにする Call wglMakeCurrent(hdc, hRC) '成功 InitOpenGL = True End Function
Sub TerminateOpenGL() 'OpenGLの終了 'コンテキストを選択していない状態にする Call wglMakeCurrent(0, 0) If hRC > 0 Then Call wglDeleteContext(hRC) End Sub

上記のプログラムは描画先が1つの場合です。
複数描画先を持ちたい場合は、描画先のHDCに対して同じだけHRCを作り、描画の際に適宜wglMakeCurrentで対象となるHDC・HRCを切り替えつつ描画することも出来ます。


ちょっと描画してみる

せっかくなんでちょっと描画してみます。
3Dについては今回は置いておいて、2Dで簡単な図形を描いて見ます。
OpenGL初期化後、何もいじくらなければXY座標の-1〜1がそれぞれ見えている状態になります。
この座標はあくまで数学の座標系と同じで、上に行くほどY座標は大きくなるため、一般のコンピュータの座標とは上下逆です。
それがどの領域に描画されるかを指定するにはglViewportを使用します。
ここではPicture1全面を使うとして、以下の様に指定します。(もちろん上のInitOpenGLでPicture1.hDCを引数に送っていないと行けませんが)

(Picture1.ScaleModeは3-ピクセルにしておきます)
Call glViewport(0, 0, Picture1.ScaleWidth, Picture1.ScaleHeight)

例えば画面の1部分はステータスなどを2Dで描画して残りを3Dの画面を表示するとか、2人用のゲームを作る場合などにglViewportで領域の半分ずつ指定してそれぞれ描画したりすることが出来ますが、今回はその必要もないので全面使います。

描画は、glBegin命令からglEndの間で頂点をどんどんglVertex**で指定していきます。
glBeginの引数でbmLineとかbmPolygonとかを指定することで描画するものを直線・連続した直線・三角形など指定することが出来ます。
(C言語ではGL_LINEやGL_POLYGON等と定数名が違うので注意)

(補足)
glVertex**は指定する頂点の次元と型で色々名前が異なります。
glVertexの次の文字は次元(2,3,4)、その後は型(i-Long、f-Single等)がつきます(glVertex2iとかglVertex3fとかになります)。
色を指定するglColor**も同じです。(型ごとに色の最大値・最小値が異なるのでマニュアルで確認が必要)
glVertexの前にglColorを呼び出すと、glVertexで指定する頂点の色を指定することが出来ます。

(色んな型で頂点や型の情報を持っておけるので便利だけど、DirectXに比べ微妙に型変換の分損してるかも?)

描画の前には、glClearでバッファをクリアしておきます。
引数にクリアするバッファを指定できるので、ここではカラーとZバッファ(使わないけど)を指定します。
クリアする色はglClearColorで指定できます。
デフォルトだと黒で初期化されるのでそれでいいでしょう。
最後にSwapBuffersで画面に反映されます(DirectDrawのFlipと同じで、バッファを入れ替えている)。

プログラムは下みたいな感じ。
タイマーなどで下の関数が定期的に呼ばれる様にします。

下のプログラムではX軸・Y軸が白い幅2ピクセルの線で引かれた後、回転する3角形が描画されます。
図形の回転はOpenGLの機能で行うことも出来ますが、今回はとりあえず自分で計算。
3角形の各頂点は赤・緑・青が指定されており、その間は自動的に線形補間された色が指定されます。
各頂点は-1〜1の間なので、整数でなく小数で指定したいため、glVertex2fを使います。
色指定はglColor3fだとRGBの3色を0〜1で指定できます。(glColor3bだと0〜255で指定できる)

Sub DrawFrame()
  Static cnt&
  Dim deg As Single
  
  'メインの描画処理
  cnt = cnt + 1

  'バッファのクリア
  Call glClear(clrColorBufferBit Or clrDepthBufferBit)

  'ビューの指定
  Call glViewport(0, 0, Picture1.ScaleWidth, Picture1.ScaleHeight)

  '描画

  'X軸・Y軸を示す線を描く
  Call glColor3f(1, 1, 1) '白
  Call glLineWidth(2) '線の太さ
  
  Call glBegin(bmLines) '直線群
    Call glVertex2f(-1, 0)
    Call glVertex2f(1, 0)
    Call glVertex2f(0, 1)
    Call glVertex2f(0, -1)
  Call glEnd
  
  '適当に三角形を描画
  Call glBegin(bmTriangles) '三角形
    '最初の頂点
    Call glColor3f(1, 0, 0) '赤
    deg = cnt * 0.1
    Call glVertex2f(0.8 * Cos(deg), 0.8 * Sin(deg))
    
    Call glColor3f(0, 1, 0) '緑
    deg = cnt * 0.1 + 2 / 3 * 3.1415926535 '前の点より120度=2/3π余分に回転
    Call glVertex2f(0.8 * Cos(deg), 0.8 * Sin(deg))
    
    Call glColor3f(0, 0, 1) '青
    deg = cnt * 0.1 + 4 / 3 * 3.1415926535 '前の点より120度=2/3π余分に回転
    Call glVertex2f(0.8 * Cos(deg), 0.8 * Sin(deg))
  Call glEnd
  
  '最後にバッファを入れ替えて画面に反映
  Call SwapBuffers(Picture1.hdc)

End Sub

これを実行すると、以下のようになります。



DirectXに比べるとはるかに簡単にとっかかれるような気がしませんか?
とにかく画面に表示するまでに憶えなければ行けないことが少ないのはいいですね。

今回はこんなところで。
実際に動くコードはまた今度載せようと思います。

次回は簡単な3Dの物体の描画をして見ます。
マテリアル・ライティングあたり。
テクスチャも行けるかな・・・?


一覧へ戻る