gfortranからDirectXを使用する方法について

更新日付 2021年01月12日


目次


1.gfortran から DirectX を使用するには
2.DirectX を使用する目的
3.ポインタを操作する
4.インタフェースを用意する
5.四錐体を表示するプログラム
6.シェイダーを使って影を表示する
7.3Dモデルのアニメーション
8.美人モデルアニメーションの修正
9.Xファイルを編集する
10.オブジェクトを追加する

1.gfortran から DirectX を使用するには

gfortran をインストールすると,DirectX を使用するためのライブラリや include ファイルも同時にインストールされます。 include ファイル(ヘッダーファイル)や lib ファイルは, DirectDraw から DirectX11 まで含まれているようです。本稿では shader を用意しなくても利用できる DirectX9 を使用します。
 これらのライブラリは C/C++ から利用することを前提にしています。 fortran から DirectX を利用するには,Win32/64 API を用いる他,DirectX の COM 形式のライブラリにアクセスしなければなりません。 Visual Fortran から DirectX を使用する例は,前章に解説しています。
 しかしながら,gfortran には COM 形式のライブラリにアクセスする機能はありません。そこで筆者は裏ワザを使って COM 形式のライブラリにアクセスする方法を考案しました。 おそらく,gfortran を用いて DirectX を利用する記事は国内では類がないと思います。

注)Windows10 から DirectX9 を使用するには,Microsoft のサイトからランタイムライブラリ「 DirectX 9.0c」をインストールする必要があります。こちらの記事を参照してください。
また,実行時に d3dx9_43.dll が見つからないというエラーが出る場合は,こちらからダウンロードできます。DirectX9.0c より古いバージョンには含まれていないようです。

2.DirectX を使用する目的

DirectX は元々ゲームソフトを開発するために用意されたプログラム開発支援ツールですが,本稿ではゲーム開発を目的とした解説をするつもりは毛頭ありません。DirectX を用いると三次元の図形表示が容易だからです。基本的に二次元しか扱えない描画ルーチンでは,ソフト的に立体を二次元化して表示するには限界があります。特に陰線陰面消去が必要な立体視は,DirectX のZバッファを利用することで,いとも容易に実現できます。

3.ポインタを操作する

gfortran から DirectX を使用するにあたって,ポインタが重要な鍵となります。gfortran では,fortran 2003 の仕様で採用された pointer 属性を持つ変数を定義できるようになりました。
 しかし,fortran で扱うことのできる pointer は,allocate 属性又は,target 属性をもつ変数(配列)と一体で使用することになっています。

・ポインタの使用例


   integer(C_INTPTR_T), target  :: TBL(10)
   integer(C_INTPTR_T), pointer :: pTBL

   TBL = (/1,2,3,4,5,6,7,8,9,10/)
   pTBL => TBL(1)
   write(6,*)pTBL

  [write文の出力]
         1

TBL は integer 型の配列で target 属性を持っています。TBL の各要素には 1 から順に 10 まで値を代入しています。
pTBL は TBL と同じ integer 型の変数で pointer 属性を持つように宣言しています。
pTBL => TBL(1) はポインタ代入文で,pTBL を TBL(1) に設定しています。この段階で pTBL は TBL(1) と同じ値を指し示すことになります。

ここで,ポインタ代入文を用いずに pTBL を操作して TBL の2番目の要素を指し示すにはどうすればよいでしょうか?


   M = sizeof(TBL(1))
   pTBL = pTBL + M
   write(6,*)pTBL,TBL(1)

  [write文の出力]
         5       5

上記のように,ポインタ変数に TBL の1要素分のアドレスを加算するつもりが,実は TBL(1) そのものに値を加算したことになります。(32bit 版を用いた例です。)
 fortran 標準のポインタ変数はポインタを操作して別のアドレスの内容を指示させることはできないのです。(Visual Fortran の整数ポインタとは異なります。)
 しかし,これを解決しないと gfortran から DirectX を実行することは不可能になります。次に解決策を示します。


   use ISO_C_BINDING
   integer(C_INTPTR_T), target  :: TBL(10)
   integer(C_INTPTR_T), pointer :: pTBL
   type(C_PTR) :: cADR
   integer(C_INTPTR_T) :: fADR,M

   TBL = (/1,2,3,4,5,6,7,8,9,10/)
   pTBL => TBL(1)
   M = sizeof(TBL(1))
   fADR = LOC(pTBL)
   fADR = fADR + M * 1
   call MoveMemory(LOC(cADR),LOC(fADR),M)
   call C_F_POINTER(cADR,pTBL)
   write(6,*)pTBL

  [write文の出力]
         2

上記の例は,ポインタ変数の指示先を変更する裏ワザです。

1) fADR 変数に pTBL のアドレスをセットします。
2) fADR 変数に TBL の1要素分のアドレスを加算します。
3) Windows API の MoveMemory サブルーチンを用いて,type(C_PTR)属性を持つ cADR 変数に型の異なる fADR 変数の値をコピーします。cADR 変数は属性が異なるため演算には使えません。
4) gfortran の C_F_POINTER サブルーチンを引用して cADR の値を pTBL の指示先にセットします。
5) pTBL を出力すると,TBL(2) の値が出力されました。

4.インタフェースを用意する

第1項でも述べましたが,DirectX は COM 形式のライブラリとして提供されます。COM形式のライブラリは,図4.1 に示すような構造になっています。

図4.1 DirectX9 のライブラリ呼び出し手順

DirectX9 は,最初に Direct3DCreate9 関数を実行すると,Direct3D9 オブジェクトが作成され,IDirect3D9 インタフェースのオブジェクトポインタを取得することができます。取得したオブジェクトポインタは,IDirect3D9 メソッド(関数)の呼び出しアドレスを列挙した Vtable と呼ばれるアドレステーブルの位置を示した領域のアドレスを指しています。Vtable に記述されているメソッド(関数)の順序は,インストールされている include ファイルの d3d9.h というヘッダーファイルに記載されています。

 Direct3D9 オブジェクトの CreateDevice メソッドを実行するには,ポインタを辿って Vtable の 17 番目に記載されているアドレスにアクセスします。


   use ISO_C_BINDING
   procedure(CreateDevice), pointer :: ptrSub16
   integer(C_INTPTR_T), save :: pVPTR,M
   type(C_FUNPTR), save :: cADX

   M = sizeof(M)
   call MoveMemory(LOC(pVPTR),lpD3D,M)
   pVPTR = pVPTR + M * 16
   call MoveMemory(LOC(cADX),pVPTR,M)
   call C_F_PROCPOINTER(cADX,ptrSub16)
   i = ptrSub16(lpD3D,Adapter,DevType,hFWnd,BFlags,pPreParm,ppRetDev)

ptrSub16 は,CreateDevice 関数の関数ポインタとして定義しています。
 pVPTR,M は,ポインタ型の整変数として定義しています。32bit 版では 4byte,64bit 版では 8byte の変数になります。
 変数 M には,Vtable の 1 項目(アドレス)と同じサイズを設定しておきます。
 cADX は type(C_FUNPTR) 型の変数で,演算には直接使用できません。
 lpD3D は,Direct3DCreate9 関数で取得した Direct3D9 のオブジェクトポインタで,IDirect3D9 インタフェースの Vtable のアドレスを格納した領域のアドレスが格納されています。他のサブルーチンから引用するため別途共通領域で定義しています。
 Win32/64 API の MoveMemory サブルーチンを用いて,Vtable のアドレスを pVPTR 変数に代入しています。
 pVPTR 変数に CreateDevice メソッドの相対アドレスを加算します。相対アドレスは,ヘッダーファイルの d3d9.h を調べると,何番目に記載されているか分かります。
 再度,MoveMemory サブルーチンを用いて計算で求めた CreateDevice メソッドのアドレスを cADX 変数に代入します。
 gfortran に用意されている C_F_PROCPOINTER サブルーチンを用いて,関数ポインタの ptrSub16 に cADX 変数の値をセットします。
 ptrSub16 関数を実行すると,実質的に CreateDevice メソッド(関数)が実行されます。

5.四錐体を表示するプログラム

DirectX の使用例として,四錐体を表示するプログラムを作成してみます。DirectX は DirectX9 を使用します。

図5.1 四錐体の表示例

DirectX を使用するには,前章で解説した Win32/64 API を用いた Windows プログラミングを用います。ここでは Windows プログラミングの解説は省略し,DirectX に関わる部分のみを解説します。

プログラム全体の流れは次のようになります。

1) ウインドウを表示する。
2) DirectX9 用のインタフェースを定義する。
3) DirectX9 を初期化する。
4) 表示する 3D のデータを用意する。
5) ライティング,材質,視点などを設定する。
6) データを表示する手続きをサブルーチンにまとめる。
7) 視点を移動する機能を追加する。
8) 終了時の後始末をする。
9) プログラムの翻訳と実行。

5.1 ウインドウを表示する

前章で解説した Win32/64 API を用いたウインドウ表示のプログラムを流用し,DirectX 用に変更します。


   i = ShowWindow(hWnd, SW_SHOWNORMAL)
   if (i .ne. 0) then
     i = MessageBox(hWnd,'Dx02 failed.'//char(0),'Dx02'//char(0),0)
   end if
   call InitDirect3D(hWnd)
   call DrawFig(hWnd)
   i = UpdateWindow(hWnd)

WinMain 関数では,ShowWindow 関数を実行した後で DirectX の初期化サブルーチン InitDirect3D を実行します。


   case (WM_CLOSE)
     call ClsDirectX
     i = DestroyWindow(hWnd)
   case (WM_DESTROY)
     call PostQuitMessage(0)
   case (WM_PAINT)
     call DrawFig2(hWnd)
   case (WM_KEYDOWN)
     key = IAND(wParam,Z'0000FFFF')
     ish = 0
     if (key == VK_RIGHT) ish = 1
     if (key == VK_LEFT)  ish = 2
     if (key == VK_UP)    ish = 3
     if (key == VK_DOWN)  ish = 4
     call Disp1(ish)
     call DrawFig(hWnd)

MainWndProc 関数では,WM_PAINT で DrawFig2 を実行し,画面を再描画します。BeginPaint関数,EndPaint関数 は実行しません。
 WM_CLOSE で DirectX で取得したオブジェクトを開放します。
 WM_KEYDOWN で矢印キーを押したときの動作を定義します。Disp1 で視点の位置を変更し,DrawFig で画面の再描画を行います。

5.2 DirectX9 用のインタフェースを定義する。

DirectX9 用のインタフェースには,DirectX9 で使用する固有の定数名と構造体の定義があります。これらのインタフェースは DirectX をプログラムする過程で必要に応じて include ファイルの d3d9.h と d3d9types.h を参照しながら fortran の定義に変更し,windx という共通 module を用意して記述します。

!*********************************************************************
!  Define Interface for DirectX9
!*********************************************************************
module windx
   use win32_types
   use win32
   use ISO_C_BINDING

   integer, parameter :: D3D_SDK_VERSION        =  32
   integer, parameter :: D3DADAPTER_DEFAULT     =   0
   integer, parameter :: D3DDEVTYPE_HAL         =   1
   integer, parameter :: D3DCREATE_SOFTWARE_VERTEXPROCESSING = Z'0020'
   integer, parameter :: D3DCREATE_HARDWARE_VERTEXPROCESSING = Z'0040'
   integer, parameter :: D3DCREATE_MIXED_VERTEXPROCESSING    = Z'0080'

   integer, parameter :: D3DMULTISAMPLE_NONE    =   0

   integer, parameter :: D3DFMT_UNKNOWN         =   0
   integer, parameter :: D3DFMT_D16             =  80

   integer, parameter :: D3DSWAPEFFECT_DISCARD  =   1

   integer, parameter :: D3DCLEAR_TARGET        =   1
   integer, parameter :: D3DCLEAR_ZBUFFER       =   2
   integer, parameter :: D3DCLEAR_STENCIL       =   4
   integer, parameter :: D3DRS_ZENABLE          =   7
   integer, parameter :: D3DRS_CULLMODE         =  22
   integer, parameter :: D3DRS_ZFUNC            =  23
   integer, parameter :: D3DRS_ALPHABLENDENABLE =  27
   integer, parameter :: D3DRS_AMBIENT          = 139

   integer, parameter :: D3DZB_TRUE             =   1
   integer, parameter :: DDSCL_NORMAL   = Z'00000008'
   integer, parameter :: DDSD_CAPS              =   1
   integer, parameter :: DDSCAPS_PRIMARYSURFACE = 512
   integer, parameter :: D3DCULL_CCW            =   3

   integer, parameter :: D3DFVF_XYZ             = Z'0002'
   integer, parameter :: D3DFVF_XYZRHW          = Z'0004'
   integer, parameter :: D3DFVF_DIFFUSE         = Z'0040'
   integer, parameter :: D3DFVF_NORMAL          = Z'0010'
   integer, parameter :: D3DFVF_VERTEX          = Z'0112'
   integer, parameter :: D3DPT_LINELIST         = 2
   integer, parameter :: D3DPT_LINESTRIP        = 3
   integer, parameter :: D3DPT_TRIANGLELIST     = 4
   integer, parameter :: D3DPT_TRIANGLESTRIP    = 5

   integer, parameter :: D3DTS_VIEW             = 2
   integer, parameter :: D3DTS_PROJECTION       = 3
!
   type, bind(C) :: D3DPRESENT_PARAMETERS
    integer*4 ::  BackBufferWidth
    integer*4 ::  BackBufferHeight
    integer*4 ::  BackBufferFormat       ! D3DFORMAT
    integer*4 ::  BackBufferCount
    integer*4 ::  MultiSampleType        ! D3DMULTISAMPLE_TYPE
    integer*4 ::  MultiSampleQuality
    integer*4 ::  SwapEffect             ! D3DSWAPEFFECT
    integer(C_INTPTR_T) ::  hDeviceWindow
    logical ::    Windowed
    logical ::    EnableAutoDepthStencil
    integer*4 ::  AutoDepthStencilFormat ! D3DFORMAT
    integer*4 ::  Flags
    integer*4 ::  FullScreen_RefreshRateInHz
    integer*4 ::  PresentationInterval
   end type D3DPRESENT_PARAMETERS

   type, bind(C) :: D3DVIEWPORT9
     integer*4 :: X,Y,Width,Height
     real*4 :: MinZ,MaxZ
   end type D3DVIEWPORT9

   type, bind(C) :: VERTEX
     real*4 :: px,py,pz,rhw
     integer*4 :: icolor
   end type VERTEX

   type, bind(C) :: D3DVECTOR
     real*4 :: x,y,z
   end type D3DVECTOR

   type, bind(C) :: D3DVERTEX
     type (D3DVECTOR) :: p,n  ! p:vertex homogeneous n:vertex normal
     real*4 :: tu,tv          ! tu,tv:vertex texture
   end type D3DVERTEX

   type, bind(C) :: D3DTLVERTEX
     real*4 :: sx,sy,sz,rhw
     integer*4 :: color
     integer*4 :: spacular
     real*4 :: tu,tv
   end type D3DTLVERTEX

   type D3DCOLORVALUE
        real*4 :: r,g,b,a
   end type D3DCOLORVALUE

end module windx

一方,メソッドを実行するための関数呼び出し手順を XDirect3DCreate9 というサブルーチンを用意して,使用する全ての関数やメソッドのインタフェースを記述します。サブルーチン名の先頭に X を付けるのは, DirectX の関数やメソッドのインタフェースルーチンであることを明確にするためです。
 使用するメソッドは,前項で示したポインタ変数を操作してアクセスできるようにサブルーチンの entry の形で定義しています。以下にサブルーチンの一部を示します。

!*********************************************************************
!    Direct3D9 interface routine
!*********************************************************************
subroutine XDirect3DCreate9(lpD3D,SDKVersion,icon)
   use win32_types
   use win32
   use ISO_C_BINDING
   integer(C_INTPTR_T) :: lpD3D
   integer(C_INT) :: SDKVersion,icon
   integer(C_INTPTR_T) :: hFWnd,pPreParm,ppRetDev,pdRegion
   integer(C_INT) :: Adapter,DevType,BFlags

   interface

   function Direct3DCreate9(SDKVersion) bind(C,name='Direct3DCreate9')
   use ISO_C_BINDING
!GCC$ ATTRIBUTES STDCALL :: Direct3DCreate9
   integer(C_INTPTR_T) :: Direct3DCreate9
   integer(C_INT), value :: SDKVersion
   end function

   integer function CreateDevice(lpD3D,Adapter,DevType,hFWind, &
           BFlags,pPreParam,ppRetDev) bind(C,name='CreateDevice')
   use ISO_C_BINDING
!GCC$ ATTRIBUTES STDCALL :: CreateDevice
   integer(C_INTPTR_T), value :: lpD3D         ! Object Pointer
   integer(C_INT), value :: Adapter,DevType,BFlags
   integer(C_INTPTR_T), value :: hFWind,pPreParam,ppRetDev
   end function

   integer function Release(lpD3D) bind(C,name='Release')
   use ISO_C_BINDING
!GCC$ ATTRIBUTES STDCALL :: Release
   integer(C_INTPTR_T), value :: lpD3D
   end function

   end interface

   procedure(Release),pointer :: ptrSub2
   procedure(CreateDevice),pointer :: ptrSub16

   integer(C_INTPTR_T), save :: pVPTR
   type(C_FUNPTR), save :: cADX
   integer(C_INTPTR_T) :: fADR
   integer(C_INTPTR_T), save :: M

   M = sizeof(M)

   lpD3D = Direct3DCreate9(SDKVersion)
   icon = 1
   if (lpD3D /= 0)icon = 0
   return
!=====================================================================
!    Release interface routine
!=====================================================================
entry XRelease(lpD3D,icon)
   call MoveMemory(LOC(pVPTR),lpD3D,M)
   pVPTR = pVPTR + M * 2
   call MoveMemory(LOC(cADX),pVPTR,M)
   call C_F_PROCPOINTER(cADX,ptrSub2)
   icon = ptrSub2(lpD3D)
   return
!=====================================================================
!    CreateDevice interface routine
!=====================================================================
entry XCreateDevice(lpD3D,Adapter,DevType,hFWnd,BFlags,pPreParm, &
                    ppRetDev,icon)
   call MoveMemory(LOC(pVPTR),lpD3D,M)
   pVPTR = pVPTR + M * 16
   call MoveMemory(LOC(cADX),pVPTR,M)
   call C_F_PROCPOINTER(cADX,ptrSub16)
   icon = ptrSub16(lpD3D,Adapter,DevType,hFWnd,BFlags,pPreParm,ppRetDev)
   return
   end

5.3 DirectX9 を初期化する。

DirectX9 の初期化は,前バージョンの DirectX8 と比較すると大幅に簡略化されています。DirectX8 で行った BackBuffer の作成や Clipping などの設定が CreateDevice メソッドに集約されています。以下に DirectX9 の初期化を行うサブルーチンを示します。

!*********************************************************************
!    InitDirect3D
!*********************************************************************
Subroutine InitDirect3D(hWnd)
   use win32_types
   use win32
   use windx
   use WINCOM
   use ISO_C_BINDING
!
   integer(C_INTPTR_T) :: hWnd
!
   type(D3DPRESENT_PARAMETERS) :: d3dpp
   type (D3DVIEWPORT9) vp

!== Get Direct3D9 Component
    call XDirect3DCreate9(lpD3D,D3D_SDK_VERSION,iRes)
    if (iRes /= 0) then
     write(6,*)'XDirect3DCreate9',iRes
     return
    end if
!== Create Device
    d3dpp%BackBufferWidth    = 600
    d3dpp%BackBufferHeight   = 400
    d3dpp%BackBufferFormat   = D3DFMT_UNKNOWN
    d3dpp%BackBufferCount    = 1
    d3dpp%MultiSampleType    = D3DMULTISAMPLE_NONE
    d3dpp%MultiSampleQuality = 0
    d3dpp%SwapEffect         = D3DSWAPEFFECT_DISCARD
    d3dpp%hDeviceWindow      = 0_C_INTPTR_T
    d3dpp%Windowed           = .TRUE.
    d3dpp%EnableAutoDepthStencil = .TRUE.
    d3dpp%AutoDepthStencilFormat = D3DFMT_D16
    d3dpp%Flags              = 1
    d3dpp%FullScreen_RefreshRateInHz = 0
    d3dpp%PresentationInterval = 0

    call XCreateDevice &
        (lpD3D,0,1,hWnd,INT(Z'0040'),LOC(d3dpp),LOC(lpD3Ddev),iRes)
    if (iRes /= 0) then
      write(6,*)'XCreateDevice',iRes
      return
    end if
!== Define Pyramid data
    call CreateData
!== Set eye point
    gEyePt = D3DVECTOR(3.0,0.2,-6.0)
    call TransForm(gEyePt)
!== Set Direct light
    call SetLight(0, 0.2,1.5,-1.0,D3DCOLORVALUE(1.0,1.0,1.0,0.0))
    return
    end

DirectX9 を使用するには,作成したインタフェースサブルーチンXDirect3DCreate9 を用いて,Direct3DCreate9 関数を実行し,Direct3D9 オブジェクトを作成します。Direct3DCreate9 関数には,対応するヘッダーファイルの整合性を確認するために,ヘッダーファイル(d3d9.h)に定義されている D3D_SDK_VERSION をパラメータに設定します。関数が成功すれば,IDirect3D9 インタフェースのポインタ lpD3D が得られます。

Direct3DCreate9 関数で取得した IDirect3D9 インタフェースを用いて,CreateDevice メソッドを実行します。構造体 D3DPRESENT_PARAMETER の d3dpp には,バックバッファのサイズの他,ウインドウモードを使用するかフルスクリーンモードを使用するか等の設定を行います。Z バッファを使用するときは,EnableAutoDepthStencil を.TRUE.に設定し,AutoDepthStencilFormat にグラフィックボードが対応している Z バッファのフォーマットを設定します。ここでは多くのボードで利用できる D3DFMT_D16 を設定しています。
 CreateDevice メソッドが成功すると,IDirect3DDevice9 インタフェースのポインタのアドレスが lpD3Ddev に得られます。IDirect3DDevice9 インタフェースには,画面表示に関わる多くのメソッドが用意されています。
 CreateData サブルーチンでは,表示する四角錘のデータを定義しています。
 TransForm サブルーチンを用いて,視点の位置を設定しています。
 SetLight サブルーチンではライトの設定をしています。ライトを設定しないと表示されたデータは真っ黒になります。

5.4 表示する 3D のデータを用意する。

DirectX を用いると,3D 表示をしたときの陰線や陰面消去が容易にできます。分かり易いように,前後に2つのピラミッドを作成します。表示する順序に関係なく後ろのピラミッドが前のピラミッドに視線を遮られて隠されることが確認できます。

!*********************************************************************
!    CreateDta Subroutine
!*********************************************************************
Subroutine CreateData
   use windx
   use WINCOM
   type (D3DVECTOR) v1,v2,v3,v4,n0

! === Define Axis
    axis(1) = D3DVECTOR(2.5,0.0,0.0)
    axis(2) = D3DVECTOR(0.0,0.0,0.0)
    axis(3) = D3DVECTOR(0.0,2.5,0.0)
    axis(4) = D3DVECTOR(0.0,0.0,0.0)
    axis(5) = D3DVECTOR(0.0,0.0,2.5)

! === 未トランスフォーム&未ライティング頂点(法線ベクトルを含む)を定義する
!   square pyramid No.1
!   1st triangle
    v1 = D3DVECTOR( 0.0, 1.5, 0.0)
    v2 = D3DVECTOR( 1.0,-1.0,-1.0)
    v3 = D3DVECTOR(-1.0,-1.0,-1.0)
    call Norm(v1,v2,v3,n0)
    vTr(1,1) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTr(2,1) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTr(3,1) = D3DVERTEX(v3,n0, 0.0,0.0)
!   2nd triangle
    v1 = D3DVECTOR( 0.0, 1.5, 0.0)
    v2 = D3DVECTOR( 1.0,-1.0, 1.0)
    v3 = D3DVECTOR( 1.0,-1.0,-1.0)
    call Norm(v1,v2,v3,n0)
    vTr(1,2) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTr(2,2) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTr(3,2) = D3DVERTEX(v3,n0, 0.0,0.0)
!   3rd triangle
    v1 = D3DVECTOR( 0.0, 1.5, 0.0)
    v2 = D3DVECTOR(-1.0,-1.0, 1.0)
    v3 = D3DVECTOR( 1.0,-1.0, 1.0)
    call Norm(v1,v2,v3,n0)
    vTr(1,3) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTr(2,3) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTr(3,3) = D3DVERTEX(v3,n0, 0.0,0.0)
!   4th triangle
    v1 = D3DVECTOR( 0.0, 1.5, 0.0)
    v2 = D3DVECTOR(-1.0,-1.0,-1.0)
    v3 = D3DVECTOR(-1.0,-1.0, 1.0)
    call Norm(v1,v2,v3,n0)
    vTr(1,4) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTr(2,4) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTr(3,4) = D3DVERTEX(v3,n0, 0.0,0.0)

!   square pyramid No.2
!   1st triangle
    v1 = D3DVECTOR( 0.0, 1.3, 1.8)
    v2 = D3DVECTOR( 0.8,-1.0, 1.0)
    v3 = D3DVECTOR(-0.8,-1.0, 1.0)
    call Norm(v1,v2,v3,n0)
    vTs(1,1) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTs(2,1) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTs(3,1) = D3DVERTEX(v3,n0, 0.0,0.0)
!   2nd triangle
    v1 = D3DVECTOR( 0.0, 1.3, 1.8)
    v2 = D3DVECTOR( 0.8,-1.0, 2.6)
    v3 = D3DVECTOR( 0.8,-1.0, 1.0)
    call Norm(v1,v2,v3,n0)
    vTs(1,2) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTs(2,2) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTs(3,2) = D3DVERTEX(v3,n0, 0.0,0.0)
!   3rd triangle
    v1 = D3DVECTOR( 0.0, 1.3, 1.8)
    v2 = D3DVECTOR(-0.8,-1.0, 2.6)
    v3 = D3DVECTOR( 0.8,-1.0, 2.6)
    call Norm(v1,v2,v3,n0)
    vTs(1,3) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTs(2,3) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTs(3,3) = D3DVERTEX(v3,n0, 0.0,0.0)
!   4th triangle
    v1 = D3DVECTOR( 0.0, 1.3, 1.8)
    v2 = D3DVECTOR(-0.8,-1.0, 1.0)
    v3 = D3DVECTOR(-0.8,-1.0, 2.6)
    call Norm(v1,v2,v3,n0)
    vTs(1,4) = D3DVERTEX(v1,n0, 0.0,0.0)
    vTs(2,4) = D3DVERTEX(v2,n0, 0.0,0.0)
    vTs(3,4) = D3DVERTEX(v3,n0, 0.0,0.0)

!   Floor
    v1 = D3DVECTOR(-1.5,-1.0, 3.0)
    v2 = D3DVECTOR( 1.5,-1.0, 3.0)
    v3 = D3DVECTOR(-1.5,-1.0,-1.5)
    v4 = D3DVECTOR( 1.5,-1.0,-1.5)
    call Norm(v1,v2,v3,n0)
    flr(1) = D3DVERTEX(v1,n0, 0.0,0.0)
    flr(2) = D3DVERTEX(v2,n0, 0.0,0.0)
    flr(3) = D3DVERTEX(v3,n0, 0.0,0.0)
    flr(4) = D3DVERTEX(v4,n0, 0.0,0.0)
    return
   end

Direct3D では基本となる三角形の頂点のデータをローカルな座標で定義し,画面座標にトランスフォームして表示する方法と,画面座標に変換されたデータを直接表示する方法があります。ここでは,トランスフォームしていないローカルな座標で定義します。
 axis は,XYZの軸を表示する直線の定義です。
 v1,v2,v3 は四角錘を構成する三角形の頂点の座標です。Norm サブルーチンで三角形の頂点法線を計算しています。法線は反射光の強さ判定に使用されます。D3DVERTEX 構造体に頂点の座標と法線のベクトルを設定します。テクスチャ座標は使用しません。DirectX では,三角形の頂点が右回りになっているときに,面が表向きと判断します。四角錘の底の面は定義していません。
 Z軸方向に位置をずらしてもう一つ四角錘を定義します。
 flr は四角錘が乗っかっている床面の定義データです。四角形を定義するときに TRIANGLESTRIP 形式という一連の結合された三角形で定義しています。頂点の定義順序に注意してください。

図5.2 TRIANGLESTRIP 形式の頂点の定義順序

5.5 ライティング,材質,視点などを設定する。

Direct3D では,表示するプリミティブにライトを当てないと暗闇の中に置かれた状態で何も見えません。画面を黒以外でクリアすると表示されたプリミティブのシルエットを見ることができます。

図5.3 ライトを使用しない場合の表示

以下にライトの設定例を示します。

!*********************************************************************
!    SetLight Subroutine
!*********************************************************************
Subroutine SetLight(index,x,y,z,color)
   use win32
   use windx
   use WINCOM

   type (D3DCOLORVALUE) color

   type D3DLIGHT9
     integer*4 :: Type                ! Type of light source
     type (D3DCOLORVALUE) :: diffuse  ! Diffuse color of light
     type (D3DCOLORVALUE) :: specular ! Specular color of light
     type (D3DCOLORVALUE) :: ambient  ! Ambient color of light
     type (D3DVECTOR) :: position     ! Position in world space
     type (D3DVECTOR) :: direction    ! Direction in world space
     real*4 :: range                  ! Cutoff range
     real*4 :: falloff                ! Falloff
     real*4 :: attenuation0           ! Constant attenuation
     real*4 :: attenuation1           ! Linear attenuation
     real*4 :: attenuation2           ! Quadratic attenuation
     real*4 :: theta                  ! Inner angle of spotlight cone
     real*4 :: phi                    ! Outer angle of spotlight cone
   end type D3DLIGHT9

   type (D3DLIGHT9) light

   integer, parameter :: D3DLIGHT_POINT          = 1      ! 点光源
   integer, parameter :: D3DLIGHT_SPOT           = 2      ! スポットライト
   integer, parameter :: D3DLIGHT_DIRECTIONAL    = 3      ! 無限遠光源
   real*4, parameter  :: D3DLIGHT_RANGE_MAX      = 1.0e10

   call ZeroMemory(LOC(light),SIZEOF(light))
   light%Type         = D3DLIGHT_DIRECTIONAL ! 光源タイプ
   light%diffuse      = color                ! 光源が放出するディフューズ色
   light%specular     = color                ! 光源が放出するスペキュラ色
   light%ambient%r    = color%r*0.3          ! 自然光
   light%ambient%g    = color%g*0.3
   light%ambient%b    = color%b*0.3
   light%ambient%a    = color%a*0.3
   light%direction%x  = x                    ! ワールド空間で光が指す方向
   light%direction%y  = y
   light%direction%z  = z
   light%position%x   = x                    ! ワールド空間での光源の位置座標
   light%position%y   = y
   light%position%z   = z
   light%attenuation0 = 1.0                  ! 光の減衰定数
   light%range        = D3DLIGHT_RANGE_MAX   ! 光源の有効距離

   call XSetLight(lpD3Ddev,index,LOC(light),iRes)

   call XLightEnable(lpD3Ddev,index,.TRUE.,iRes)

   return
   end

ライトは,D3DLIGHT9 構造体を用いて定義します。構造体には,光源のタイプ,ディフューズ色(拡散光),スペキュラ色(光沢),環境色,光の方向ベクトル,光源の位置等を設定します。
 SetLight メソッドでライトの設定を行い,LightEnable メソッドでライトを点灯します。

以下にマテリアルの設定を行います。

!*********************************************************************
!    Material Subroutine
!*********************************************************************
   subroutine Material(red,green,blue)
   use win32
   use windx
   use WINCOM

   type D3DMATERIAL9
     type (D3DCOLORVALUE) :: diffuse    ! ディフューズ色
     type (D3DCOLORVALUE) :: ambient    ! アンビエント色
     type (D3DCOLORVALUE) :: specular   ! スペキュラ色
     type (D3DCOLORVALUE) :: emissive   ! エミッション色
     real*4 :: power                    ! スペキュラハイライトの鮮明度
   end type D3DMATERIAL9

   type (D3DMATERIAL9) mtrl

   call ZeroMemory(LOC(mtrl),SIZEOF(mtrl))
   mtrl%diffuse%r = red
   mtrl%diffuse%g = green
   mtrl%diffuse%b = blue
   mtrl%diffuse%a = 0.0
   mtrl%ambient%r = red * 0.3
   mtrl%ambient%g = green * 0.3
   mtrl%ambient%b = blue * 0.3
   mtrl%ambient%a = 0.0
   mtrl%power = 10.0

   call XSetMaterial(lpD3Ddev,LOC(mtrl),iRes)
   return
   end

マテリアルは,光の反射特性を表します。マテリアルの設定は D3DMATERIAL9 構造体を用いて SetMaterial メソッドを実行します。

次に視点の位置を設定します。

!*********************************************************************
!    TransForm Subroutine
!    トランスフォームマトリックスを作成しDirectXに設定
!*********************************************************************
Subroutine TransForm(vEyePt)
   use windx
   use WINCOM
   type (D3DVECTOR) vLookatPt,vUpVec,vEyePt
   real*4, parameter  :: PI = 3.1415926
   real*4 matView(4,4),matProj(4,4)

!  ビュー変換マトリックスの設定(ビューポイントの定義)
   vLookatPt = D3DVECTOR(0.0, 0.0, 0.0) ! 注視点
   vUpVec    = D3DVECTOR(0.0, 1.0, 0.0) ! 上方向単位ベクトル
   i = iSetViewMatrix(matView, vEyePt, vLookatPt, vUpVec)

!             |  Ux   Vx   Nx   0  |   U:右方向ベクトル
!   matView = |  Uy   Vy   Ny   0  |   V:上方向ベクトル
!             |  Uz   Vz   Nz   0  |   N:ビュー方向ベクトル
!             | -U*C -V*C -N*C  1  |   C:視点の位置座標

   call XSetTransform(lpD3Ddev,D3DTS_VIEW,matView,icon)

!   プロジェクション変換マトリックスの設定
   Fov    = PI*0.25   ! 視野角45°(ラジアン)
   Aspect = 0.75      ! アスペクト比(画面の縦横比4:3)
   Znear  = 1.0       ! 前方クリップ面までの距離
   Zfar   = 100.0     ! 後方クリップ面までの距離
   i = iSetProjectionMatrix(matProj,Fov,Aspect,Znear,Zfar)

!             | w  0   0  0|
!   matProj = | 0  h   0  0|
!             | 0  0   Q  1|
!             | 0  0 -QZn 0|

   call XSetTransform(lpD3Ddev,D3DTS_PROJECTION,matProj,icon)
   return
end

視点位置の設定を TransForm サブルーチンで行います。TransForm サブルーチンでは,変換マトリックスを用意してビュー変換とプロジェクション変換を行います。変換に関する説明はここでは省略します。前章の Direct3D の項目を参照してください。

5.6 データを表示する手続きをサブルーチンにまとめる。

データ表示を行う手順をサブルーチンにまとめておくと,画面の再表示が必要になったときや視点の位置を変更した場合に,画面表示が容易になります。

!*********************************************************************
!    DrawFig
!*********************************************************************
subroutine DrawFig(hWnd)
   use windx
   use WINCOM
   use ISO_C_BINDING
   integer(C_INTPTR_T) :: hWnd

   type (VERTEX)  fr(5)

   call XClear(lpD3Ddev,0,0_C_INTPTR_T,3,INT(Z'00205060'),1.0,0,iRes)
!  Start Draw
   call XBeginScene(lpD3Ddev,iRes)

!  frame
   fr(1)=VERTEX( 20.0, 30.0,1.0,1.0,INT(Z'00ff0000'))
   fr(2)=VERTEX( 20.0,370.0,1.0,1.0,INT(Z'00ff0000'))
   fr(3)=VERTEX(570.0,370.0,1.0,1.0,INT(Z'00ff0000'))
   fr(4)=VERTEX(570.0, 30.0,1.0,1.0,INT(Z'00ff0000'))
   fr(5)=VERTEX( 20.0, 30.0,1.0,1.0,INT(Z'00ff0000'))
   call XSetFVF(lpD3Ddev,68,iRes)     ! D3DFV_XYZRHW|DIFFUSE
   call XDrawPrimitiveUP(lpD3Ddev,3,4,LOC(fr),INT(sizeof(fr(1))),iRes)

!  Squre pyramid No.1
   L = sizeof(vTr(1,1))
   call XSetFVF(lpD3Ddev,18,iRes)    ! D3DFVF_XYZ|NORM
   call Material(0.8,0.0,0.0)        ! R,G,B
   do i = 1,4
    call XDrawPrimitiveUP(lpD3Ddev,5,1,LOC(vTr(1,i)),L,iRes)
   end do
!  Squre pyramid No.2
   call Material(0.8,0.9,0.0)        ! R,G,B
   do i = 1,4
    call XDrawPrimitiveUP(lpD3Ddev,5,1,LOC(vTs(1,i)),L,iRes)
   end do
!  Floor
   call Material(1.0,1.0,1.0)
   call XDrawPrimitiveUP(lpD3Ddev,5,2,LOC(flr),L,iRes)
!  Axis
   call XSetFVF(lpD3Ddev,2,iRes)     ! D3DFVF_XYZ
   call Material(0.0,0.0,0.0)        ! R,G,B
   L = sizeof(axis(1))
   call XDrawPrimitiveUP(lpD3Ddev,3,4,LOC(axis(1)),L,iRes)

!  End Draw
   call XEndScene(lpD3Ddev,iRes)
!=====================================================================
!  Show BackBuffer to Display
!=====================================================================
entry DrawFig2(hWnd)
   call XPresent(lpD3Ddev,0_C_INTPTR_T,0_C_INTPTR_T,hWnd,0_C_INTPTR_T, &
        iRes)
   return
   end

DrawFig サブルーチンは,バックバッファに表示データを描画するときに呼び出します。描画は常にバックバッファに描き出します。
 Clear メソッドを実行し,バッファ画面を塗りつぶし,Zバッファを 1.0 に初期化します。
 BeginScene メソッドを実行し,描画の開始を指示します。
 VERTEX 構造体配列 fr に,赤枠を描画するための座標をセットしています。VERTEX 構造体は,トランスフォーム済みの同次座標を定義するときに用います。
 SetFVF メソッドは,DrawPrimitiveUp メソッドと対で使用し,描画データのフォーマットを指定します。第2パラメータの数字は D3DFVF_XYZRHW と D3DFVF_DIFFUSE の合成でXYZ 同次座標と Diffuse 色を持つデータフォーマットであることを指定しています。
 DrawPrimitiveUp メソッドを実行して赤枠を描画します。2番目の引数 3 は,LINESTRIP 形式のフォーマットを用いることを示し,3番目の引数 4 は,4組のデータを描画することを指定しています。赤枠を描画する意味は特にありませんが,トランスフォーム済みのデータを描画する例を示すために表示しています。
 2つの四角錘を描画します。データフォーマットは,XYZ 座標と頂点法線を持つ未トランスフォームデータであることを SetFVF メソッドで指定します。
 DrawPrimitiveUp メソッドの2番目の引数の 5 は,TRIANGLESTRIP 形式のフォーマットを用いることを指定します。
 Material サブルーチンを実行し,描画するデータに色を設定します。
 黒線でXYZ方向の座標軸を描きます。
 EndScene メソッドを実行し,描画の終了を指示します。
 最後に Present メソッドを実行し,バックバッファのデータを画面に表示します。Present メソッドは,画面の再表示が必要になったときにも呼び出されます。

5.7 視点を移動する機能を追加する。

3D のデータ表示ができたら,折角だから視点の位置を移動させ,表示がどのように変化するのか確認できるようにします。キーボードの矢印キー →←↑↓ を押すと左右上下に視点の位置が回転します。MainWndProc 関数の中で WM_KEYDOWN メッセージを捕らえて Disp1 サブルーチンで視点位置の回転処理を行います。プログラムは前章の Direct3D を応用していますので解説はそちらを参照してください。

5.8 終了時の後始末をする。

表示ウインドウの閉じるボタンを押したときの処理を記述します。

!*********************************************************************
!    ClsDirectDraw
!*********************************************************************
subroutine ClsDirectX
   use WINCOM
!== Release all objects
   call XRelease(lpD3Ddev,iRes)
   call XRelease(lpD3D,iRes)
   return
   end

ClsDirectX サブルーチンは,MainWndProc 関数の中で WM_CLOSE メッセージを捕らえたときに実行され,取得した DirectX の IDirect3DDevice9 オブジェクトと Direct3D9 オブジェクトを取得した逆順に Release メソッドを実行して開放します。

 最後に Dx02.f90 のソースプログラム全編を,こちらに示します。

5.9 プログラムの翻訳と実行。

プログラムの翻訳は,Win32/64 API を使用する API のライブラリに加え,DirectX9 のライブラリを指定します。dx02.bat を実行したときのコマンドの実行例を以下に示します。


E:\Program Files\gFortran>dx02     翻訳とリンクのバッチファイル

E:\Program Files\gFortran\gfortran Dx02.f90 -fno-range-check -lgdi32 -ld3d9

E:\Program Files\gFortran>a

図5.4 表示した四角錘



6.シェイダーを使って影を表示する

図6.1 影を表示したモデル

いきなりですが,Compaq Visual Fortran で作成した影の実装を gfortran 用に書き換え,64bit で動作するようにしました。今回は,DirectX9 を使用しています。DirectX9 は公開されてから10年以上経っていますので古いといわれるかもしれません。しかし,3DモデルはXファイルを使用しています。Xファイルを扱えるのは DirectX9 が最後です。

6.1 Cray Pointerの利用

gfortran から DirectX を使用するには,Win32 API と DirectX のインタフェースを自作する他に,バーチャル領域とポインタを駆使する必要があります。Visual Fortran などに実装されている機能で,標準の Fortran の文法にはない機能です。

例えば,
  character msg*100
  pointer (lpmsg,msg)

のように記述します。msg は文字型の変数ですが,pointer 宣言で上記のように記述すると,msg は実体のない変数になります。別に確保された領域のアドレスを pointer 変数 lpmsg に代入すると,msg はその領域を文字型変数としてアクセスすることができます。gfortran では,このような pointer を Cray Pointer と称しています。使用するには,コンパイルオプションで -fcray-pointer を指定する必要があります。この機能を使用すると,DirectX の関数から渡された Pointer 変数の示す領域を実領域にコピーしなくても直接アクセスできるようになります。

6.2 バーテックスシェーダとピクセルシェーダ

DirectX9 では通常の画面表示ではシェーダは必要としません。しかし,影の実装ではシェーダを使う必要があります。アセンブラではなく C 言語風の言語で記述することもできます。実は,筆者はアセンブラが大好きなのです。汎用コンピュータでは殆どのプログラムをアセンブラで記述していました。PC ではMSDOSの時代に通信プログラムをアセンブラで作成しました。なので,このプログラムでは敢えて懐かしいアセンブラで記述しています。

6.3 プログラムと翻訳コマンド

ソースプログラム dx3d03.f90 の全編(インタフェースを除く)は,こちらに示します。

ファイル一式のダウンロードは,こちらです。
 (2020.06.10 Xファイルとテクスチャーの一部を修正しました。)

翻訳は次のように行いますが,別途メニューリソースの用意が必要です。実行には表示するXファイルとテクスチャーを準備する必要があります。


E:\Program Files\gFortran>windres dx3d03.rc dx3d03.o  リソースの翻訳

E:\Program Files\gFortran>dx3d03    翻訳とリンクのバッチファイル

E:\Program Files\gFortran\gfortran dx3d03.f90 dx3d03.o -fno-range-check ^
   -fcray-pointer -lgdi32 -lcomdlg32 -lcomctl32 -lwinmm -ld3d9 -ld3dx9 ^
   -ld3dxof -ldxerr9 -static

E:\Program Files\gFortran>a

Windows 10 で実行時に d3dx9_43.dll が見つからないというエラーが出る場合は,こちらからダウンロードできます。

6.4 操作方法

1) メインメニュー

 File :
Open X-file : 表示するXファイルを選択する。
Save X-file : 最適化したXファイルを出力する。
UV Save : 頂点に対応するUV座標(テクスチャー)を出力する。
Exit : プログラムを閉じる。
 Panel :
Object : オブジェクトの選択ダイアログを表示し,オブジェクトの表示 ON/OFF や材質等を設定する。
Shadow : シャドーを表示する。
Option : ワイヤーフレームで表示する。

2) マウス操作

左クリック : 表示を上下左右に移動させる。
右クリック : 上下左右に回転させる。
左クリック+右クリック : 視野角を変化させ拡大縮小表示する。

3) Pfキー

Pf2キー : 正面を向く。
Pf3キー : 側面を向く。
Pf12キー : 初期画面に戻す。


7.3Dモデルのアニメーション

図7.1 3Dモデルのアニメーション

7.1 gfortran 用に移行

Compaq Visual Fortran では,DirectX8 を使って3Dモデルのアニメーションを行いましたが,gfortran では DirectX9 を使用してプログラムすることにしました。当初の目論見では,Cray Pointer さえ使用できれば,DirectX8 と DirectX9 では大きな違いがないだろうと予想して変換作業を実施していきました。ところが,途中までは何とか代替の関数があって移行作業は進んだのですが,アニメーションオブジェクトに対応するフレーム名の取得関数 Resolve に相当する関数が見つからず挫折しました。止むを得ず従来の方針を諦め,DirectX9 のサンプルを参考にして,アニメーションコントローラを使用することにしました。ところがところが,さにあらんやアニメーションコントローラは gfortran とは相性が悪く散々手古摺って完成まで4ヶ月を要してしまいました。ネットで捜しても fortran で DirectX9 を使用してアニメーションを行った例は見つけられませんでした。本当に動作するか全く予想が付かない作業のため,先ずは gfortran と同時にインストールされた g++ を用いてXファイルを読み込んで動作確認をすることから始めました。筆者は元々 g++(C++)はクラスだの派生だのと分けの分からない用語が沢山あって大嫌いなのです。幸いなことに,入手した C++ のサンプルプログラムは多少の手直しで動作することが確認できました。でも気持ちの悪い Tiny ではなく以前に作成した美人モデルを動かしたいと思いました。本稿ではアニメーションモデルの作成や骨入れなどは以前の解説に委ね,DirectX9 で大きく変わった部分について解説します。

7.2 Xファイルの読み込みルーチンについて

DirectX8 ではXファイルを読み込むときに D3DXLoadMeshFromX 関数を使用しました。DirectX9 でもXファイルを読み込むだけならば同じ関数が使用できます。しかし,アニメーションコントローラを使用する場合は,D3DXLoadMeshHierarchyFromX という関数を使用します。この関数は Hierachy という文字列が入っているとおり,Xファイルの中身を解読して必要な情報を渡してくれるという一見便利そうな関数なのです。読み込んだデータを分類するためのテンプレートも用意する必要がありません。しかし,実はこの関数は使い熟すのに1ヶ月以上かかってしまう程大変な関数なのです。

この関数の引数は次のようになっています。

   D3DXLoadMeshHierarchyFromX(
      _In  FileName   : 読み込むXファイルの名前のPtr
      _In  MeshOption : メッシュの作成オプション
      _In  pdevice    : IDirect3DDevice9インタフェースのPtr
      _In  pAlloc     : ID3DXAllocateHierachyインタフェースのPtr
      _In  pUserDataLoader : XFileにユーザ作成定義がある場合に用いるPtr
     _Out  ppFrameHierachy : ルートフレームを格納するLPD3DXFRAMEのPtrのPtr
     _Out  ppAnimationController : ID3DXAnimationControllerインタフェースPtrのPtr
     )

この関数の4番目のパラメータに pAlloc があります。ID3DXAllocateHierachy インタフェースのポインタを渡すパラメータです。実は,ID3DXAllocateHierachy インタフェースはユーザー自ら作成する必要があります。このインタフェースは次に示す4つのメソッド(関数)があり,COM の形式で作成しなければなりません。COM 形式については第4項に解説してあります。

1) CreateFrame関数

D3DXFRAME構造体に合成変換行列とフレーム名を追加した領域を作成します。

2) CreateMeshContainer関数

D3DXMESHCONTAINER構造体にテクスチャーや骨の行列などを追加した領域を作成し,関数が受け取ったパラメータから必要な情報を設定します。設定した情報は描画処理の際に使用します。

3) DestroyFrame関数

CreateFrame関数で作成したフレーム領域を削除する関数です。この関数は一連の処理を終了するときに,D3DXFrameDestroy 関数を実行すると呼び出されます。

4) DestroyMeshContainer関数

CreateMeshContainer 関数で確保した領域やインタフェースを解放する関数です。この関数も D3DXFrameDestroy 関数を実行した時に呼び出されます。


これらの関数を実装するわけですが,関数の処理内容は特に gfortran に限ったことではないので別のサイトの詳しい記事を参考にしてください。

gfortran では,4つの関数について,呼び出しインタフェースの定義と,VTBL を作成し,ID3DXAllocateHierachy のオブジェクトインタフェースのポインタ pAlloc を作成する必要があります。

!   ID3DXAllocateHierachyインタフェースの定義
    integer(C_INTPTR_T), save :: lpAlloc
    type(C_FUNPTR), save :: VTBL(4)
    integer(C_INTPTR_T), save :: lpVTBL

!   COM形式VTBLの構築
    VTBL(1) = C_FUNLOC(CreateFrame)
    VTBL(2) = C_FUNLOC(CreateMeshContainer)
    VTBL(3) = C_FUNLOC(DestroyFrame)
    VTBL(4) = C_FUNLOC(DestroyMeshContainer)
    lpVTBL  = LOC(VTBL)
    lpAlloc = LOC(lpVTBL)

注意すべきことは,1) の CreateFrame関数と 2) のCreateMeshContainer関数では確保した領域のポインタを返す必要があります。筆者は当初何度プログラムを見直しても D3DXLoadMeshHierarchyFromX が原因不明の異常終了をしていました。しかし,この原稿を書いているときに気が付きました。ポインタの返し方が両者で異なっていたからでした。CreateMeshContainer の方ではパラメータを C 言語に合わせて value 渡しに指定していたからでした。詳細はプログラムのコメントを参照してください。

7.3 アニメーションコントローラについて

アニメーションコントローラは,アニメーションセットの切り替えや時間の管理を行ってくれるヘルパー関数を提供するインタフェースです。D3DXLoadMeshHierarchyFromX 関数を実行するとアニメーションコントローラのオブジェクトポインタを取得することができます。

アニメーションコントローラではアニメーションセットの切り替えや進み時間の設定を行います。


図7.2 アニメーションセットの切り替えダイアログ

アニメーションセットの切り替えは,図7.2 のようなダイアログを表示して行うようにしています。//記号はステップアニメーションを行う場合に用います。

weight を設定して2つのアニメーションセットをスムーズに移行させる機能もありますが,本プログラムでは採用していません。

7.4 シェーダは不要

DirectX8 では頂点と複数の骨を関連付けてトランスフォームする頂点ブレンド処理のために,最大4組のシェーダを用意する必要がありました。頂点が影響を受ける骨が最大4組だからです。実際に4組の骨から影響を受けることは,複雑な骨を用意しない限りはまずないでしょう。本プログラムでも2組しか設定していません。
 DirectX9 では,SetRenderState関数の D3DRS_VERTEXBLEND オプションでブレンド数を設定し,D3DRS_INDEXEDVERTEXBLENDENABLE オプションで頂点ブレンドを有効に設定すれば,SetTransForm関数で指定した数の頂点ブレンド処理を実行してくれます。シェーダを用意しなくても済みます。

7.5 サンプルプログラム

サンプルプログラムでは,前章の影の表示プログラムを継承し,前章で使用した静止モデルを開くと影の表示が可能になっています。

プログラムファイル一式のダウンロードは,こちらです。

(2020年8月27日 美人モデルのXファイルを修正しました。)



8.美人モデルアニメーションの修正

8.1 美人モデルの修正

前章では以前に作成した3Dモデルのアニメーションを gfortran で動作させることでした。しかし,動作はしましたが肩の動きがぎくしゃくしていたり,お尻の部分が出っ張っていたりするなど幾つか気になる個所がありました。そこで今回は,それらの修正を施すことにしました。そのためには,Xファイルを解読しデータの中身を修正する必要があります。

8.2 Xファイルを読み解く

Xファイルの概略については,こちらに記載していますが,スキンメッシュアニメーションなどの詳しい内容までは触れていません。

(1) UV座標

Xファイルには,テクスチャーを貼るために頂点に対応したUV座標データを有しています。UV座標は,縦横[0.0〜1.0]に正規化されています。自動で作成したUV座標データには,不具合が何箇所か見つかりました。そこで修正ツールを作成し,UV座標データを修正しました。図8.1,8.2はその一部を示します。


図8.1 修正前のUV座標


図8.2 修正後のUV座標

(2) ボーンの表示

スキンメッシュアニメーションを行うXファイルは,フレーム毎に階層構造になっています。フレームはアニメーションを制御するボーン(骨)と対応しています。

ボーンを可視化するには,フレームを定義している Frame 項目のデータから親子関係を解析し,FrameTransformMatrix(4×4の行列) の 4 行目からボーンの基準点を取得します。ボーンを可視化(水色で表示)した図を図8.3 に示します。尖った先がボーンの基準点です。

美人モデルデータの Frame の記述と階層構造のレベルを番号で表示すると以下のようになります。

0    121        232      343      4543
  hip{{}-abdomen{{}-chest{{}- neck{{}}
           |                   |   454       565         676      7876543
           |                 lColar{{}-lShldr{{}-lForeArm{{}-lHand{{}}}}}
           |                   |   454       565         676      787654321
           |                 rColar{{}-rShldr{{}-rForeArm{{}-rHand{{}}}}}}}
           |    232       343      454       5654321
          lThigh{{}- lShin{{}-lFoot{{}-  lToe{{}}}}}
           |    232       343      454       5654321
          rThigh{{}- rShin{{}-rFoot{{}-  rToe{{}}}}}


図8.3 ボーンの可視化

(3) ボーンとの関連付けの修正

スキンメッシュアニメーションでは,ボーンの動きに対応して各頂点がトランスフォームされます。各頂点は対応するボーンと依存率(SkinWeight)がデータとして存在しています。すべての頂点は何れかのボーンに対応付けがされています。

スキンメッシュアニメーションを行うと,動きにうまく追随できない箇所や不自然な変異箇所が見つかります。頂点とボーンとの対応付けを修正すると改善できる場合があります。

図8.4 に頂点とボーンとの対応付けを修正するために作成したツールの一部を示します。緑色の点は黄色で表示したボーンに対応付けされた頂点を示します。赤色の点は他のボーンと重複して対応付けされている点を示します。紫色の点は現在選択中の頂点を示します。


図8.4 ボーンとの関連付けの修正(ウェイトの再設定)

美人モデルでは肩の変形が改善しました。合わせてブラのストラップの変形も改善しました。お尻と脚の繋がりと変形もやや改善しました。

(4) ボーンの動きの修正

スキンメッシュアニメーションでは,AnimationSet の中にフレーム(ボーン)毎に時間の進行に応じたアニメーションデータが記述されています。美人モデルの場合は回転や移動が合成されたトランスフォーム行列が記述されています。 DirectX では,頂点の元の座標にトランスフォーム行列を掛け算することで新しい座標点を求めています。

  

今回は,美人モデルの腰の振り方が過大に思えるため,振り方を抑えるような修正を考えます。

ボーンの動きを修正するためには,このトランスフォーム行列を修正する必要があります。 しかし,この行列のどこがどういう動きに関係しているのでしょうか?

そもそも,これらの行列は回転や移動を行う行列を組み合わせてできており,上位のボーンの影響も受けてできています。目的の動作をさせる行列を作成する過程をフォワードキネマティックスといいます。

一方,今回のように動きを修正するような操作をインバースキネマティックスといいますが,トランスフォーム行列がどのような回転や移動が組み合わされてできているのかは分かりません。そこで以下のような回転行列を掛け合わせることでトランスフォーム行列を変更してやります。美人モデルの腰の振り方を抑制するためには,腰の動きを司るボーンのトランスフォーム行列に対してZ軸回りに逆回転の変換行列を掛け合わせてやります。

X軸回りに回転Y軸回りに回転Z軸回りに回転

図8.5 は,腰の動きを司るトランスフォーム行列を取り出し,動きを抑制するような最適な回転行列(θ)を試行錯誤で求めてシミュレーションした動画を示します。黒いボックスはオリジナルのトランスフォーム行列で変換し,赤いボックスは修正したトランスフォーム行列で変換した図を示します。表示には図形ルーチン WinPlotG の3D表示とアニメーション機能を用いています。シミュレーションでは腰の動きの上下の振りが抑制されています。


図8.5 腰の動きの修正

このようにして修正したトランスフォーム行列を美人モデルのXファイルに戻して動作確認をしてみました。結果,失敗でした。

腰の動きは改善されましたが,脚の動きがもつれたような動きになってしまいました。原因は腰の動きは身体全体の基本になっていたことを考慮していませんでした。したがって,腰の動きに追随する他のすべてのボーンの動作が影響を受けてしまいました。

今回は腰の振り動作の修正は諦め,右腕の回転を抑える修正に留めることとしました。

図8.6 は改良した美人モデルのアニメーションです。30 コマに減らしてあるので動きは少しぎくしゃくしています。

図8.6 改良した美人モデルのアニメーション



9.Xファイルを編集する

折角Xファイルを表示するプログラムを作成したので,新しいモデルを作成してみたくなりました。しかし,新しいモデルを一から作成するのは相当に手間暇がかかります。既存のモデルやネットで探してきたものを好みに合わせて修正するほうが賢明です。

Xファイルを編集できる PMXEditor というフリーソフトがあったので,インストールしてみました。しかし,使い方をマスターするのに相当時間と労力が必要なようでした。そこで,前章で使用したXファイルを読み解くプログラムに手を加えてXファイルを編集できるようにしてみました。編集プログラムを自作することのメリットは操作がし易いように自由にプログラムを変更できることです。

Xファイルの不便なところは,面を定義する部分とマテリアルや法線を定義する部分が別々に存在していることです。また,頂点の定義と対応するテクスチャーのUV座標やボーンの情報もそれぞれ別々になっています。なので,Xファイルを直接エディタで編集するのは実質的に不可能です。

とりわけ,頂点の座標位置の変更や面の削除は比較的容易にプログラムすることができます。これらの作業を実施し好みのモデルに仕上げるのは結構楽しいものです。ただし,頂点の位置を大幅に変更すると法線も変更する必要があります。

法線は光の反射に大きく影響を与えます。一般に法線には面の向きを示す面法線とグーロシェーディングに使用する頂点法線があります。Xファイルで使用しているのは頂点法線です。

図9.1 に頂点法線の概要を示します

図9.1 頂点法線の概要

頂点法線は当該頂点を共有する各面の法線を加算し,正規化して求められます。

頂点番号 3 の頂点法線は次のように計算できます。

図9.2 は入手したモデルを編集して作成した美少女モデルを示します。

図9.2 美少女モデル



10.オブジェクトを追加する

(1) オブジェクトの追加

作成した美少女モデルに服を着せてあげることにします。別に用意したスカートとブラウスのオブジェクトを追加します。Xファイルにオブジェクトを追加するには,頂点データ,インデックスデータ,マテリアルデータ,法線データ,法線インデックスデータ,及び,テクスチャーデータを追加する必要があります。

(2) 配列の拡張

gfortran に限らず確保した配列を拡張する機能を有するコンパイラは私の知る限りではあまりないようです。

Xファイルを処理するプログラムでは,アニメーションを含まない場合には,最低以下のデータを格納する配列を使用します。

1) Mesh (頂点のデータ)
2) Mesh_Index (フェイス(三角形or四角形)を表す頂点のインデックス)
3) MeshMaterialList (フェイスに対するマテリアルのデータ)
4) MeshNormals (頂点法線のデータ)
5) MeshNormals_Index (フェイスに対応する頂点法線のインデックス)
6) MeshTextureCoords (頂点に対応するテクスチャの座標データ)

gfortran には,配列の拡張をサポートする関数(move_alloc)が用意されています。

使用例

Mesh という配列に Mesh1 という配列の内容を追加する場合は,以下のように記述します。

real, allocatable :: Mesh(:,:),Mesh1(:,:),MeshT(:,:)
allocate (Mesh(3,1000))
Mesh = 1.0
allocate (Mesh1(3,500)
Mesh1 = 2.0
allocate (MeshT(3,1500))
MeshT(:,1:1000) = Mesh
MeshT(:,1001:1500) = Mesh1
call move_alloc(MeshT,Mesh)

配列 Mesh は 1500 に拡張(再確保)され,MeshT は自動的に解放されます。

この機能を用いて元のXファイルのデータに新しいオブジェクトのデータを追加します。但し,インデックスの番号やオブジェクトの番号は変更する必要があります。

(3) 三角メッシュを四角メッシュに変換する

用意したオブジェクトが三角メッシュの場合は,四角メッシュに変換した方が都合がよい場合があります。

処理速度が速いということでゲームソフトでは三角メッシュが多用されているようです。

一方,四角メッシュはデータ量が少なくて済み,ワイヤーフレームの表示がシンプルで見やすく編集操作がし易いといったメリットがあります。

三角メッシュを四角メッシュに変換するには,blender というフリーソフトで可能なようですが,新しいバージョンはXファイルに対応していないので,自前で変換することにしました。

変換はプログラミングの容易さを考慮して,法線の向きが同じ三角メッシュの長辺同士を結合します。図10.1 を参照。

図10.1 三角メッシュを四角メッシュに変換

結合したい辺が長辺以外等でメッシュが不規則なモデルの場合は結合がうまくいかない部分が生じます。図10.2 参照。

図10.2 変換できない部分があるモデルの例

(4) 完成した美少女モデル

スカート(裏面も用意)やブラウスをモデルにフィットするように調整して完成です。ここまで,他のソフトに頼らないですべて自前のプログラムで実行しました。

図10.3 服を着た美少女モデル




TOPページに戻る