数学をとらなかった人の為の3D数学 その1:概要

ベクトル(Vector)

ベクトルとは、向きと長さを持った数である。それは2Dであっても3Dであっても変わらない。
ベクトル同士や複数のベクトルは加減算でき、下の図のようになる。
 
面白いことに、始点が同じベクトルを加算した結果は、2Dでは四角形、3Dでは立方体の対角線と重なる。
また始点もしくは終点が同じ2つのベクトルとそれらを減算した結果を結ぶと、三角形になる。
(後ほど詳しく述べるが、例えば次項のベクトル座標ではベクトルの始点が決まっているので、頂点と頂点を減算することで辺を表現する。)


ベクトル座標(Vector Coordinate)

ベクトル座標とは、原点(0, 0)から始まる(=始点を持った)ベクトルである。
3Dプログラミングではベクトル座標を使用する。


一次、二次関数などで使った原点(0, 0)で始まり位置を点で表す座標系を、デカルト座標(直交座標系)という。
例えば、同じ座標をベクトル座標とデカルト座標で表現する。
デカルト座標では点なので、二点の関係は見ただけでは分からない。
しかし、ベクトル座標では矢印(線)で表すので、同じ長さで向きが変わっている、「回転している」ことが直感的に分かる。
 


またベクトル座標→デカルト座標への変換は一意であるが、その逆は一意ではない。
点は終点のみであるが、ベクトルは終点と始点と2つの情報を持つからである。
なので、ベクトル座標はデカルト座標の基底概念、デカルト座標はベクトル座標の発展(抽象)概念と言える。

そして座標変換が一意である(=関係性がある)ということは、何らかの計算式を定義できる。
それが、内積外積等のベクトル演算である。

行列(Matrix)

ひとつのベクトルは複数のベクトルに分解でき、複数のベクトルは加算できる。
加算は一意であるが、分解はもちろん一意ではない。
しかし、ベクトルを行列で表現する場合は分解方法が決まる。X軸成分、Y軸成分、Z軸成分の3つである。(左図)

例えば、ベクトル(x, y, z)はベクトル(x, 0, 0)とベクトル(0, y, 0)とベクトル(0, 0, z)を加算したものと考える。
 

その分解したベクトル(X,Y,Z軸の3成分ベクトル)を並べたものが、3x3行列である。(右図)
(なので2Dベクトルは2x1行列、3Dベクトルは3x1行列とも言える)


行列で何ができるか。3x3行列で表現できるのは、「回転」「スケーリング」、4x4行列ならばそれに「移動」を加えた3つである。
なぜ分解するのか。それはそれぞれの軸に対する座標変換を行う為である。


分解したということは、その成分ベクトルを加算すれば元のベクトル座標に戻るはずである。
想像してみてほしい。成分ベクトルのひとつ、例えばX軸成分を90度回転するとどうなるか。
その後それぞれの成分ベクトルを加算した結果もX軸で90度回転されているのではないだろうか。


同じようにX軸成分をn倍すると、加算した結果もX軸方向にn倍されていることが想像できると思う。
(後に詳しく述べるが、これはスケーリング、拡大縮小の表現である。)
つまり行列とは、ベクトルの座標変換を表現する為に使うのである。

ベクトルと行列(Vector, Matrix)

行列とはベクトルを分解した成分ベクトルを並べたものと書いた。
行列はベクトルをただ単純に行に置き換えただけのものではないが、その性質は行だけでも見ることができる。
まず適当な回転行列を作って、各行ベクトルの長さを計算してほしい。
どんな回転行列も、全ての行が全て単位ベクトル(長さが1のベクトル)であることがわかる。
(当然だが、ベクトルは回転しても長さは変わらない。よって作用する成分は1、つまり単位ベクトルであることと一致する。)


そしてこの性質を利用すると、その行列のスケーリング成分を抽出できる。
行列の3x3部分に存在するのは回転とスケーリング成分である。
まず、x,y,z各成分に作用する行列の成分は次の通りであるので、その成分ベクトルの長さを求める。
前述の通り、回転成分ベクトルは必ず単位ベクトルであるので長さに関係しない。
つまり、長さがそのままスケーリング成分であるのだ。


x成分ベクトル (11 12 13)
y成分ベクトル (21 22 23)
z成分ベクトル (31 32 33)

ベクトルの長さは、各値の2乗を足したものの平方根をとることで計算できる。→sqrt( (x*x)+(y*y)+(z*z) )


四元数(Quaternion)

一般に四元数とは複素数の虚部をi,j,kの3つに拡張したものであるが、
3Dプログラミングにおいてはベクトル、行列と同じように座標に演算を加えたものである。(実部wと虚部i,j,kをそれぞれw,x,y,zに割り当てる)
何ができるか。任意軸回転である。では任意軸とは何か。
回転するには回転軸が必要である。例えば地球の自転などは、北極点と南極点を繋ぐ線分が回転軸である。
そして、回転軸は物体の中心になくても構わない。例えば太陽を軸にした地球の公転である。
 

任意軸とは原点からの任意の向きの回転軸である。3Dプログラミングでは単位ベクトルで表現される。
勿論、任意軸回転は行列でも表現できる。何故四元数を使うのか。
ひとつ目の理由は、演算量である。
4x4行列では16個の数値が必要だが、四元数は4個の数値で表現できる。
これは何千もの頂点変換が行われることを考えると、大きな利点になる。


ふたつ目の理由は、回転の補間である。
四元数ではH(1.0-t)と、始点から終点までの回転量を調整することができる。
軸から回転軌道を見ると球の裏面をなぞることから、球面線形補間と呼ばれる。
回転行列と相互変換が可能で、実際に使う分にはさほど難しくない。


ここまでの各項の文字数で分かる通り、一番複雑で深い理解が必要なのはベクトルである。
それと、ここでは数学的な数学をしないので、偏らない為にもいろいろな参考サイト、資料と合わせて読んでほしい。

C/C++プログラマー向けメモ帳

久々にバージョンアップしました。

変更点は、

  • 編集中かどうかの判定をちゃんとするよう変更(タイトルバーに*を表示)
  • ビルド、実行、実行中断 を追加(要VC++

ダウンロード

http://d.hatena.ne.jp/nepo_n/edit.zip


コンソールアプリケーション、Win32アプリケーション共にビルドと実行を確認しました。
現在はリンカを使ってないのでシングルソース用となってます。
リンカを動かさない分ビルド時間が短いです。


あとprintf等の標準出力関数でエディタログに出力(デバッグ用に)できます。(win32アプリの場合はfflush(NULL)とかする)。


ちゃんとしたログ管理はまた暇な時に(・ω・。)


##
Windows7コンパイルできないようです。
原因究明中です!
WindowsUACがCreateProcessの邪魔してるのかも…?
…PeekNamedPipeで名前無しパイプを渡してるのが原因かも(2000ではセーフコードだけど以降はあやしい)

ウィンドウメッセージその2

前回のコード部分になります。
ついでに現在使っている「プロシージャ内でのCtrl,Shift押しの検出」を追加してみました。

// フックプロシージャ
LRESULT HookProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    // このウィンドウに関連あるウィンドウオブジェクト
    CEdit* edit = (CEdit*)::GetWindowLong(hWnd, GWL_USERDATA);

    // プロシージャ内におけるCtrl,Shift押しを検出
    bool bShift = (::GetKeyState(VK_SHIFT) & 0x8000) != 0;
    bool bControl = (::GetKeyState(VK_CONTROL) & 0x8000) != 0;

    switch(msg) {
    case WM_KEYDOWN:
        // 全て選択
        if (bConrtol && wParam=='A') {
            edit->SetCaret(0, -1);
            edit->ScrollOnCaret();
        }
        break;

    case WM_LBUTTONDOWN:
        // マウスクリックを無効にする部分
        // 元々のプロシージャを呼ばずにreturn
        return 0;
    }

    // 元々のプロシージャを呼び出す
    return ::CallWindowProc(m_pProc, hWnd, msg, wParam, lParam);
}

::CallWindowProc()の活用方法

標準のエディットコントロールにオートインデントを実装する場合、ひとつの方法としてWM_KEYDOWN::VK_RETURNをフックします。
単に改行といっても、「改行挿入」「選択されていれば改行で上書き」「画面スクロール」等の処理がされています。
それらがあるのにわざわざ自分で作成するのは勿体無いので、デフォルトのものを利用してみます。

  • ::CallWindowProc を呼ぶ
  • インデント幅を計算
  • タブを挿入

このように先に元々のプロシージャに仕事をさせてその後に自分の処理を追加していく、ということができます。
(でも真面目にオートインデントを実装するなら、コントロールごと自分で作る方がかなり楽です(´▽`;))

ウィンドウメッセージその1

今回は検索(ウィンドウメッセージとかのワード)で来た方への記事になります。


Windowsプログラミングにおいて必須なのがウィンドウメッセージの処理です。
エディットコントロールにしても、ツリービュー、リストビュー等コモンコントロール等にしても、ウィンドウメッセージの処理方法がプリセットされているだけですので、挙動を変更するには、コントロールにウィンドウメッセージが到達する直前に横取りして処理すれば良いです。
(コントロールを自作する場合は、RegisterClassでそのまま独自のプロシージャを登録します)


// 1.ウィンドウプロシージャを独自のものに置き換える(サブクラス化)
WNDPROC m_pProc = (WNDPROC)::GetWindowLong(m_hEdit, GWL_WNDPROC);

SetWindowLong(m_hEdit, GWL_WNDPROC, (LONG)HookProc);

// 2.プロシージャ内からメンバを呼ぶ為にウィンドウハンドルにthisを隠しておく。

SetWindowLong(m_hEdit, GWL_USERDATA, (LONG)this);

2.ではGWL_USERDATAにポインタを隠してますが、プロシージャ自体にポインタを送り込むトリッキーな技もあります。


次に実際に挙動を変更してみます。
例えばエディットコントロール上でキャレット位置を変更する方法のひとつに、「左クリックする」ことがあります。
基本的にコントロールは全てウィンドウメッセージで処理するので、この場合はWM_LBUTTONDOWN(もしくはUP) が処理されていると推測できます。
なのでこれを無効にしたければ、このメッセージが飛んで来た時に元々のプロシージャを呼ばなければ良いわけです。

(その2につづく)

することメモ

STGエンジンの作成

  • AI.txt→敵、弾の動作パターン、弾生成パターンの定義
  • Object.txt→敵、弾の定義(AI.txtで定義したパターンの組み合わせ)
  • Scenario.txt→背景設定、進行管理(Object.txtで定義したオブジェクトの配置)

PHP5開発環境の作成

  • プロジェクト管理(プロジェクトファイルとファイル一覧、MDI)
  • 変更履歴をファイルごと保存(LZSSで圧縮)
  • 色分けキーワードの実装
  • PHPコンパイラとの連動(デバッグウィンドウの行番号からのファイル表示、メッセージボックスに変換して文字列表示etc.)
  • 通常実行(ブラウザ)とデバッグ実行(コンソール)

 たしか警告エラーとechoが別々のパイプで出力されてたような気がする。
 パイプ処理→http://d.hatena.ne.jp/nepo_n/20051111


 追伸
 標準のツリービューが貧弱すぎて、実装がとても面倒くさいです
 ある機能を実装しようとすると基本的機能を自分で実装し直さなきゃいけないことに
 いっそこれも自作した方が早いかもしれない。・゚・(ノД`)・゚・。