DG-Caradチュートリアル(2)

ポリゴンモデルを表示しよう

ポリゴンです、ポリゴン

 2D画像を表示は、前項で説明したとおりですが、今度は3Dに挑戦してみましょう。

 とりあえずポリゴンで表現されたメカを回転させるプログラムを組んでみようと思います。

 Delphiを起動して、新しいプロジェクトを作ってください。作ったら適当な名前を付けて保存しましょう。

 

プロジェクトを作ったら

 前のチュートリアル同様、まっさらなフォームに、コンポーネントパレットの[QuadrupleD]タブからDGCaradコンポーネントと、[Additional]タブからApplicationEventsコンポーネントを貼り付けます。

 コンポーネント名も前回同様、DGCaradコンポーネントの名前はDG、ApplicationEventsコンホーネントの名前はデフォルトのまま、ApplicationEvents1としておきます。

 

 次に、uses節に使用するユニットを追加します。

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, AppEvnts, DGCarad9, SXLib9, d3d9, s_mathpack;

 3D描画ユニット「SXlib9」、DirectGraphicsユニット「d3d9」、演算ルーチン集「s_mathpack」をuses節に追加してください。チュートリアル(1)と同じです。

 

 チュートリアルで用いる変数やオブジェクトを追加します。

 水色になっている部分が追加するコードです。今回は少し多いですよ。

type
TForm1 = class(TForm)
  DG: TDGCarad;
  ApplicationEvents1: TApplicationEvents;
  procedure FormCreate(Sender: TObject);
  procedure ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
  procedure FormResize(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    Root:TSXFrame;      //全てのフレームの親になるフレーム
    MechFrame:TSXFrame; //メカを格納するフレーム
    Camera:TSXFrame;    //カメラの位置・姿勢を示すフレーム

    Mech:TSXMesh;       //メカの形状データ

    Lights:TSXLightGroup; //ライト管理オブジェクト

    Tick:Integer;       //メカの回転につかうカウンタ
end;

 

初期化

 次に、解像度の設定と、ポリゴンメカの形状データ(以降、形状データの事を「メッシュと呼びます)読み込みなどを行います。

 TForm.OnCreateイベントハンドラを書き換えましょう。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //解像度とデプスステンシルサーフェスの設定
  DG.WindowMode(ClientWidth, ClientHeight, DGFMT_ZCheap);

 まず、解像度の設定とデプスステンシルサーフェスの設定です。

 解像度の指定は良いとして、2Dだけの場合と違い、3Dグラフィックを扱うにはデプスステンシルサーフェスが必要になってきます。このデプスステンシルサーフェスのフォーマットも環境によってサポートされるものとされないものがあるので、本来は配列パラメータに使いたいデプスステンシルサーフェスのフォーマットを第一希望から順に書いて行くのですが、DG-Caradで定義済みの配列定数を渡すことにします。

 DGFMT_ZCheap :Array[0..2] of D3DFORMAT = (D3DFMT_D16, D3DFMT_D16_LOCKABLE, D3DFMT_D32);

 となっており、16ビットのデプスバッファ、ロック可能(プログラムで読み書きが行える)16ビットのデプスバッファ、32ビットのデプスバッファの順にサポートしているかどうかをテストし、最初にサポートされていると判断されたものを採用します。

 このチュートリアルではステンシルバッファは使わないので、デプスバッファだけ作ってステンシルバッファは作りません。

 

procedure TForm1.FormCreate(Sender: TObject);
begin
  //解像度とデプスステンシルサーフェスの設定
  DG.WindowMode(ClientWidth, ClientHeight, DGFMT_ZCheap);

  //シーンオブジェクトの生成
  Scene:=TSXScene.Create(DG);

  //ビューポートの設定
  Scene.SetProjection(Pi/4, ClientHeight/ClientWidth, 0.01, 10000.0);
  Scene.SetViewport(0,0,ClientWidth,ClientHeight);

 次に、シーンオブジェクトを生成し、ビューポートの設定をします。

 SetProjectionメソッドによって画角アスペクト比近方/遠方クリッピング面と視点との距離を設定します。

 画角というのは、視野の広がりを示す角度の事です。SXLibでは画角の設定には「画面の左端から右端までに何度の角度が見えるのか」をラジアン単位で渡す事になっています。

 アスペクト比は、画面の縦横比です。垂直方向の画角の決定に用いられます。

 余談ですが、写真用語での画角と、ここで設定するパラメータは違う値になりますので、カメラに詳しい方は特にご注意ください。

 クリッピング面は、「これ以上近づいたら画面に描画されない」という面と「これ以上遠ざかったら画面に描画されない」という面の事です。

 今回は、画角45度、近方クリッピング面まで0.01、遠方クリッピング面まで10000という事にしています。

 次に、SetViewportメソッドで、画面上のどの範囲にこれから作る3Dのシーンを描画させるかを指定します。今回は、ウィンドウのクライアント領域全体を指定します。

 

procedure TForm1.FormCreate(Sender: TObject);
begin
  //解像度とデプスステンシルサーフェスの設定
  DG.WindowMode(ClientWidth, ClientHeight, DGFMT_ZCheap);
  
 //シーンオブジェクトの生成
  Scene:=TSXScene.Create(DG);

  //ビューポートの設定
  Scene.SetProjection(Pi/4, ClientHeight/ClientWidth, 0.01, 10000.0);
  Scene.SetViewport(0,0,ClientWidth,ClientHeight);

  //フレームの作成
  Root:=TSXFrame.Create(Nil);       
  MechFrame:=TSXFrame.Create(Root);
  Camera:=TSXFrame.Create(Root);
  Scene.CameraFrame:=Camera;

  //カメラ、メカの位置・姿勢を決める
  Camera.SetTranslation(Root, Vector(0,0,0));
  Camera.SetOrientation(Root, Vector(0,0,1), Vector(0,1,0));

  MechFrame.SetTranslation(Root, Vector(0,-0.5,4));
  MechFrame.SetOrientation(Root, Vector(0,0,1), Vector(0,1,0));

 次はいよいよ、シーンの構築です(大げさ)

 シーン構築の基礎になるのは、TSXFrameオブジェクトで表される「フレーム」の生成です。フレームとは、いわばメッシュの入れ物です。

 フレームとは何か、なぜフレームが必要なのかを理解するために、どうやってメッシュを表示するかについて、少し考えて見ましょう。メッシュを表示するにしても、「どこに」「どんな向き(姿勢)で」表示するかという問題が出てきます。また、ヒト型ロボットのような多関節物体を表現する際には、例えば腕を動かすにしても、腕の先には手が付いているので、手も一緒に動かさないと手がすっぽ抜けて表示されることになります。

 ここで、フレームの登場です。フレームには親子関係があり、各々がその親となるフレームに対しての位置と姿勢を保持しているため、フレームに対してメッシュを格納する、という事で人体モデルのような階層構造を持った物体でも、複数のメッシュの集合から表示が可能になるのです。

 DG-CaradとSXLibでフレームを利用するのは簡単です。

 TSXFrame.Createメソッドでフレームオブジェクトを生成します。引数には親フレームを指定します。今回の例では、

 Root ... 全てのフレームの親になるフレーム。
 MechFrame ... メカを表示するためのフレーム。
 Camera ... 視点の位置、向きを示すフレーム。

 としています。

 Scene.CameraFrameプロパティに指定することで、このフレームの視点でシーンが描画される事をSceneオブジェクトに伝えます。

 フレームの生成が出来たら、次は各フレームの位置を決めましょう。

 TFrame.SetTranslationメソッドでフレームの位置を指定します。「第一引数で示されるフレームの中で、第二引数で示される座標」と同じ位置が、自分の原点になります。

 姿勢の設定には、TFrameSetOrientationを用います。「第一引数で示されるフレームの中で、第二引数で示されるベクトル」が自分のZ軸の向きとなり、「第一引数で示されるフレームの中で、第三引数で示されるベクトル」が自分のY軸の向きとなります。 第二,第三引数で示される各々のベクトルは軸を表現しているからには互いに直交していて、さらに長さを1にする必要があります。

 ちなみに、X軸の向きは、Z軸とY軸から計算されます。Direct3Dでは左手座標系を採用しているため、Z軸が正面、Y軸が高さ方向とすると、X軸は右手の方向となります。左手を開いて、親指と人差し指でL字を作ってくみましょう。X軸は親指の方向、Y軸は人差し指の方向、Z軸は手のひらの方向です。

 なに、分からない?わかんなかったらウダウダ考えてないで自分で座標とか変えて動かしてみやがれ(ぉ

 

procedure TForm1.FormCreate(Sender: TObject);
begin
  //解像度とデプスステンシルサーフェスの設定
  DG.WindowMode(ClientWidth, ClientHeight, DGFMT_ZCheap);
  
 //シーンオブジェクトの生成
  Scene:=TSXScene.Create(DG);

  //ビューポートの設定
  Scene.SetProjection(Pi/4, ClientHeight/ClientWidth, 0.01, 10000.0);
  Scene.SetViewport(0,0,ClientWidth,ClientHeight);

  //フレームの作成
  Root:=TSXFrame.Create(Nil);       
  MechFrame:=TSXFrame.Create(Root);
  Camera:=TSXFrame.Create(Root);
  Scene.CameraFrame:=Camera;

  //カメラ、メカの位置・姿勢を決める
  Camera.SetTranslation(Root, Vector(0,0,0));
  Camera.SetOrientation(Root, Vector(0,0,1), Vector(0,1,0));

  MechFrame.SetTranslation(Root, Vector(0,-0.5,4));
  MechFrame.SetOrientation(Root, Vector(0,0,1), Vector(0,1,0));

  //メカ読み込み
  Mech:=TSXMesh.Create(DG);
  Mech.LoadFromFile('mech.sx');
  MechFrame.Mesh:=Mech;

  //ライトの設定
  Lights:=TSXLightGroup.Create(DG, 1);
  Lights[0].SetupDiffuse(1.0,1.0,1.0);
  Lights[0].SetupSpecular(1.0,1.0,1.0);
  Lights[0].SetupAmbient(0.3,0.3,0.3);
  Lights[0].SetupDirectional(Vector(0,0,1));
  Lights[0].Enabled:=True;

  Tick:=0;
end;

 最後に、メカを表現するためのメッシュのデータを読み込みと、ライトの設定を行います。

 メッシュオブジェクトを作成し、LoadFromFileメソッドでメッシュを読み込みます。そうしたらメカ表示用フレーム(MechFrame)にくっつけるだけ。

 扱えるファイルは、*.sx形式というSXLibオリジナルの形式となっていますが、tool\Xsimplify.exeというツールで .x 形式からコンバートできますし、TSXMesh.SaveToFileメソッドで作成することも出来ます。

 ライトの設定は、ライト管理用オブジェクトを生成する事で行います。

 ライト管理用オブジェクトはTSXLightGroup.Createで生成します。このとき第二引数で同時に使用するライトの最大数を指定します。

 ライト管理用オブジェクトのLight配列プロパティでライトの性質を決定します。Lightプロハティはデフォルト配列プロパティなので、プロパティ名を省いて記述できます。略さないと、

Lights.Light[0].SetupDiffuse(1.0, 1.0, 1.0);

 という風になります。

 Setupなんとか、というメソッドがライトの性質を示しているのですが、それぞれについて説明すると

SetupDiffuse 光源が物体に当たっている部分に付く色です。拡散反射光の色
SetupSpecular 光源が物体の表面で反射して目に飛び込んでくる場合に付く色です。鏡面反射光の色
SetupAmbient 光が当たってない部分にも満遍なく付く色です。環境光の色
SetupDirectional これは色についての指定ではなく、光源の種類についての指定。太陽光のような向きのある、平行光源である事をしめします

 以上でシーンの構築は終り。

 

 オブジェクトの生成を書いたら、対になる解放ルーチンを、アプリケーションの終了時に書いてやる必要があります。

//解放
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  Root.Free;  //これで、Rootの子フレーム(Camera,MechFrame)も含めて解放される
  Lights.Free;
  Mech.Free;
  Scene.Free;
end;

 前の章で解説したように、Sceneオブジェクトは自動的に解放されます。また、Lighs(TSXLightGroupオブジェクト), Mech(TSXMeshオブジェクト)も自動的に解放されます。

 しかし、TSXFrameオブジェクトは自動的には解放されませんので注意してください。

 

 では、ホントにシーンが構築できたか、描画させましょう。

 

描画ループの作成

 なんだか設定にやたら手間取った感じがしますが、描画ループは実にシンプルだったりします。

 ApplicationEvents.OnIdleイベントハンドラを書き換えて、メカを回転させまくりましょう。

 デバイスのテストとかはやらないといけないので、まずは描画していいかどうかの場合分けを先に作りましょう。

procedure TForm1.ApplicationEvents1Idle(Sender: TObject;
  var Done: Boolean);
begin
  //Direct3Dデバイスが生きてるかテストしてから描画
  Case DG.D3DDevice.TestCooperativeLevel of
    //OK、生きてます
    D3D_OK: begin
    end;

    //デバイスは生きてるけど、ロスト後でリセットされてない
    D3DERR_DEVICENOTRESET: begin
      DG.Reset;
    end;
  end;

  //これを設定すると、他のイベントが実行されなくてもまたこのイベントが呼ばれる
  Done:=False;
end;

 これに実際に描画を行うコードを書きましょう。TestCooperativeLevelがD3D_OKで分岐する先に書くんでしたね。

 

procedure TForm1.ApplicationEvents1Idle(Sender: TObject;
  var Done: Boolean);
begin
  //Direct3Dデバイスが生きてるかテストしてから描画
  Case DG.D3DDevice.TestCooperativeLevel of
    //OK、生きてます
    D3D_OK: begin

      //描画開始
      DG.D3DDevice.BeginScene;

      //画面のクリア、色は黒で
      Scene.Clear(D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, $FF000000, 1.0, 0);

      //シーンの内容を全部描く
      Scene.Render(Root);

      //描画終り
      DG.D3DDevice.EndScene;

      //表示
      DG.D3DDevice.Present(Nil, Nil, 0, Nil);
    end;

    //デバイスは生きてるけど、ロスト後でリセットされてない
    D3DERR_DEVICENOTRESET: begin
      DG.Reset;
    end;
  end;

  //メカをまわせー
  Inc(Tick,20);
  MechFrame.SetOrientation(Root, NowRotY(Vector(0,0,1),Tick), Vector(0,1,0));

  //これを設定すると、他のイベントが実行されなくてもまたこのイベントが呼ばれる
  Done:=False;
end;
  1. 描画開始をDirectGraphicsに伝える

  2. 今まで描かれていた内容をクリア

  3.  シーンを画面に描かせる

  4. 描画終了をDirectGraphics伝える

  5. 描画した内容を画面に表示させる

 というわけですが、画面のクリアについて補足しますと、第一引数のフラグにD3DCLEAR_TARGETとD3DCLEAR_ZBUFFERというのが付いています。これはデプスバッファもクリアしろ、という指定です。 また、第3引数がなぜ1.0なのかというと、デプスバッファにおいて「一番奥」を示す数が1.0であり、「一番手前」を示す数は0.0だからです。

 次に、シーンの内容の描画についてですが、Scene.Render(Root) とすることでRootを一番祖先のフレームとしてその子孫の内容を画面に描け、という事になっています。Rootの子フレームは、CameraとMechFrameであり、そのうちMechFrameがメッシュを持っているので、それが画面に描かれるわけです。

 描画させる部分が出来たら、メカを回転させるためのコードをつけましょう。

 カウンタを増やして、その値に従ってSetOrientationメソッドで向きを変えます。今回は、Y軸周りに回転させるだけとします。

 

できたかな?

 

 以上でポリゴンモデルの表示は完了です。

 ポリゴンで出来た物体をぐるぐる回転させるのにも飽きたら、次に進みましょう。