DirectX8.1時代のプログラム置き場



■X-File Plugin for 3ds max R4/R5 (FML ver1.2) [2003.12.07]

Xファイルを出力してくれる3Dモデリングツールは、どれくらいあるのか分かりませんが、
なかなか素直にデータを出してくれない事があります。
今回は3DSMAXのSDKを使用して、DX8SDKのextrasにあるXSkinExpを改造してみたいと思います。

プラグイン本体のみ(dle)のダウンロード
xskinexp.zip (43Kbyte)

ソースファイルのダウンロード
xskinexp12src.zip (36Kbyte)


■MAXのPluginを作るには?
VisualC++とMax(R4 or R5)とMaxSDKが必要です。
MaxSDKをインストールし、MaxSDKのincludeとlib、Cstudio\sdkのパスを通せば準備完了です。

■Pluginのデバッグ例
Pluginのソースファイルでプロジェクトを作り、
プロジェクトの設定 > デバッグ > デバッグセッションの実行可能ファイル
ここに、3DSMAX本体の実行ファイル(3dsmax.exe)を指定します。

プロジェクトの設定 > リンク > 出力ファイル名
ここには、3DSMAXのプラグインフォルダ+XSkinExp.dleを指定します。
例えば c:\3dsmax\plugins\XSkinExp.dle 等
(もしくはコンパイル後にdleをコピーするような設定)

正常にコンパイル出来たら、実行してみましょう。
MAXが起動し、プラグインのデバッグが可能になります。
■Export X-File plugin(改良版)ver1.2の改良点

●XSkinExp既存の不具合の修正(DX8SDK版)
こちらのEric氏の情報と、独自の解析によります。

●[Animation Only]を追加
 このスイッチをONにすると、Frame と Anim だけを出力します。
 (meshデータを出力しなくなります)

●プラグインのオプション設定を自動で保存読み込みするようにしました。
 ファイルはシステムフォルダに "expxfile.opt" で出力しています。

●テクスチャファイル名のパス部分をカット
 元のプラグインは、フルパスのテクスチャファイル名がX-Fileに出力されますが、
 パス部分をカットし、パスなしのファイル名だけ出力するようにしました。

■他のMAXでX-Fileを出力するプラグイン
Panda SoftComputer SystemsでPandaDXExportというフリープラグインを公開しています。
細かな出力設定が可能なプラグインです。(頻繁にバージョンアップしているようです)

〜PandaDXExport(ver4.3.0.43)を使ってみて〜
詳しく調べていないので、なんとも言えませんが、スキンモデルのUVがうまく出なかったり、
テキストとバイナリでデータ内容が異なる、、、と言った不具合を確認しています。
これは、当方だけで起きている現象なのかもしれません。念のため。
また、新しいバージョンでは修正されているかもしれません。
EffectWareでDirectX Exporterというフリープラグインを公開しています。
残念ながらこちらはMAX3までしか対応していないようです。




■DirectPlayによるネットワークゲームの開発#2 [2003.12.06]

●今回は報告のみ
1年以上前にちょっとやってたネタですが、、、
クライアント・サーバー形態で、キャラクターを動かして、チャットが出来る所まで作ってみました。
今はただ歩くだけなので、もう少しなにか出来るようになったら公開したいと思います。




■3Dメガネで立体視 [2002.12.01]

★実行ファイルのダウンロード
dx8anaglyph.zip (418Kbyte)
スペースキーで、視線変更。
マウスドラッグで視点移動、ホイールで視点距離調整。
ESCキーで終了。


今回は、アナグリフ(anaglyph)と言われる簡単な立体視の画像を作って見たいと思います。
左右に赤と青のセロファンを貼った、あの3Dメガネですね。
1850年代からある技術との事ですから、今から約150年前ですね。

ん〜はっきり言って、立体に見える・・・・か?という感じです。
Web上でこういった画像を公開している所も見てみましたが、同じような感じだったので、
カラーフィルタ式3Dメガネでの立体視は、これくらいが限界なのかな、と思いました。


●原理
メガネの右目は青いセロファン、左目は赤いセロファンとします。
右目から見た画像を赤っぽく、左目から見た画像をシアンっぽくしてから、合成します。
メガネを通して、合成した画像を見ると、右目には「右目から見た画像」、
左目には「左目から見た画像」が強く映るので左右の視差により立体っぽく見える事があります(笑)。
(モニターの大きさや解像度、モニターからの距離、セロファンの色具合により、かなり左右されると思います)


左目から見た画像 右目から見た画像


●操作
スペースキーを押すと、平行視線、クロス視線、通常描画、と変わります。
平行視線は、左右の視線ベクトルが平行になります。
クロス視線は、アヒルの頭の位置を見つめるようなベクトルになります。


●3Dメガネをお持ちでない方へ
下記の画像を、いい感じのサイズでプリントアウトすると、3Dメガネを作る事が出来ると思います。




■シャドウテクスチャー [2002.11.20]

★実行ファイルのダウンロード
dx8texshadow.zip (450Kbyte)
カーソルキーでアヒル操作。
マウスドラッグで視点移動、ホイールで視点距離調整。
ESCキーで終了。


今回は、投影したテクスチャーで影を表現する方法です。
(スプリングボックスの時にも少しやってましたが、投影面は平面限定でした)


●影のテクスチャーを作る
光源から見たモデルを投影し、テクスチャーにレンダリングします。

レンダリングターゲットを変更し、テクスチャーを白でクリアして、
モデルが単色で描画されるように設定を変更します。
ShadowTextureColorをRGBA(0,0,0,255)にすると、真っ黒い影になり、
RGBA(255,255,255,255)にすると完全に透明となります。

//モデルが単色で描画されるように設定する
lpD3DDEV->SetRenderState(D3DRS_TEXTUREFACTOR, ShadowTextureColor);
lpD3DDEV->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
lpD3DDEV->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);

//ViewPortの設定  テクスチャーのサイズを設定する
D3DVIEWPORT8 viewport = { 0, 0, SHADOW_WIDTH, SHADOW_HEIGHT, 0.0f, 1.0f };
lpD3DDEV->SetViewport( &viewport );

//View行列
D3DXVECTOR3 ViewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtRH( &LightViewRh, &LightPos, &LightTarget, &ViewUp );
D3DXMatrixLookAtLH( &LightViewLh, &LightPos, &LightTarget, &ViewUp );

//Proj行列
float Radius = ModelRadius;  //モデルのバウンディング球の半径
float Dist   = D3DXVec3Length( &(LightPos - LightTarget) );
float Near   = Dist - Radius;
float Far    = Near + (Radius * 2.f);
float Angle  = 2.f * asinf(Radius / Dist);
D3DXMatrixPerspectiveFovRH( &LightProj, Angle, 1.0f, Near, Far );

lpD3DDEV->SetTransform( D3DTS_VIEW, &LightViewRh );
lpD3DDEV->SetTransform( D3DTS_PROJECTION, &LightProj );

〜モデルを描画する〜

上記の流れでモデルを描画すると、下のような画像が得られます。
(描画が終わったらViewPort等、元の値に戻しておきましょう)


●影のテクスチャーをはる
そのテクスチャー使いマルチテクスチャーでモデルをレンダリングします。
(すべてテクスチャーが貼られたモデルを前提としています)

テクスチャーステージ1に影のテクスチャーをセットします。
この時、影のテクスチャーの外側をどうするかを指定します。
ここでは、D3DTADDRESS_CLAMPを指定しています。
CLAMPを指定すると、テクスチャー座標[0〜1.0]以外の部分は、
それぞれ[0,1.0]のテクスチャーを使用するようになります。
(テクスチャーのエッジラインを引き伸ばしたような感じ)

本来であれば、D3DTADDRESS_BORDERを使いたい所ですが、悲しいかなGeForce2MX200(古!)では、
BORDERが使えないのでCLAMPを使いました。
ただ、基本的に「モデルのバウンディング球」からプロジェクションを決めているので、
その値が最適値であれば、モデルは常にテクスチャー内に納まるので、問題は起きないでしょう。


lpD3DDEV->SetTextureStageState( 1, D3DTSS_ADDRESSU,  D3DTADDRESS_CLAMP );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_ADDRESSV,  D3DTADDRESS_CLAMP );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_COLOROP,   D3DTOP_MODULATE );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_ALPHAOP,   D3DTOP_SELECTARG1 );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE );

あとはシェーダーの定数に、投影テクスチャーの変換行列を設定し、モデルを描画します。
(matWorldはモデルのワールド行列)

D3DXMATRIX m = matWorld * LightViewLh * LightProj;
D3DXMatrixTranspose(&m, &m);
lpD3DDEV->SetVertexShaderConstant(18, &m, 4);

VertexShaderは下記になります。

-----------------------------------------
定数
c0-c3	透視変換行列
c12	定数(0.0f, 0.5f, 1.0f, 2.0f)
c13	光源ベクトル
c14	平行光源カラー
c15	環境光カラー
c18-c21	投影変換行列

v0	頂点
v3	法線
v7	UV
-----------------------------------------

vs.1.0
m4x4 oPos,     v0,    c0              // 透視変換
m4x4 r1,       v0,    c18             // 投影変換
mov  oT0.xy,   v7.xy                  // オリジナルのUV

mul  r1.xy,    r1.xy, c12.y           // xy = xy * 0.5
mad  r1.xy,    r1.w,  c12.y, r1.xy    // xy = xy + (w * 0.5)
mov  oT1.xyzw, r1.xyzw                // 投影テクスチャーのUV

dp3  r1.x,     v3,    c13             // 法線と光源ベクトルとの内積
lit  r1,       r1                     // ライティング計算
mul  r0,       r1.y,  c14             // 平行光源
add  r0,       r0,    c15             // 環境光
min  oD0,      r0,    c12.z           // クランプ

投影テクスチャーのUVは、同次テクスチャー座標で設定しないと、
大きな面に投影した時や、視点近くに投影した時に、影が歪んでしまいます。
(xy * 1/w とするのではなく、投影変換後のxyzwを、そのまま使用)

また、固定パイプライン時の為にD3DTSS_TEXTURETRANSFORMFLAGSを設定します。

lpD3DDEV->SetTextureStageState( 1, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT3 | D3DTTFF_PROJECTED );


●操作
カーソルキーの「上」で前進、「左・右」で方向を変えます。
ほっとくと、くるくる回ります。
分かりやすいように、光源位置からの角錐台形視野(frustum)をワイヤーで表示しています。


●Frustumのライン
表示されるFrustumラインは、pOutに書き込まれる8つの頂点を結ぶと完成します。

D3DXVECTOR4 vFrustum[8] = {
    D3DXVECTOR4( 1.0f, 1.0f, 0.0f, 1.0f ),
    D3DXVECTOR4( 1.0f, 1.0f, 1.0f, 1.0f ),
    D3DXVECTOR4(-1.0f, 1.0f, 0.0f, 1.0f ),
    D3DXVECTOR4(-1.0f, 1.0f, 1.0f, 1.0f ),
    D3DXVECTOR4(-1.0f,-1.0f, 0.0f, 1.0f ),
    D3DXVECTOR4(-1.0f,-1.0f, 1.0f, 1.0f ),
    D3DXVECTOR4( 1.0f,-1.0f, 0.0f, 1.0f ),
    D3DXVECTOR4( 1.0f,-1.0f, 1.0f, 1.0f ),
};

/*------------------------------------------------------
  Frustum Lineの頂点を返す
------------------------------------------------------*/
void CalcFrustum( D3DXMATRIX *pView, D3DXMATRIX *pProj, D3DXVECTOR3 *pOut ){
    D3DXMATRIX InvView;
    D3DXMATRIX InvProj;
    D3DXMatrixInverse( &InvView, NULL, pView );
    D3DXMatrixInverse( &InvProj, NULL, pProj );
    for( int i = 0; i < 8; i++ ){
	D3DXVECTOR4 v = vFrustum[i];
	D3DXVec4Transform( &v, &v, &InvProj );
	D3DXVec4Transform( &v, &v, &InvView );
	pOut->x = v.x / v.w;
	pOut->y = v.y / v.w;
	pOut->z = v.z / v.w;
	pOut++;
    }
}

頂点の連結順番
0-1, 2-3, 4-5, 6-7
0-2, 4-6, 0-6, 2-4
1-3, 5-7, 1-7, 3-5


追記 2003/04/14 アヒルくんウォーズで発覚したバグを修正。




■DirectPlayによるネットワークゲームの開発#1 [2002.09.16]

●DirectPlayとは

DirectPlayとは、DirectXに用意されている通信ライブラリです。
ロビー機能や、音声チャットなども簡単に実装出来るようです。
今回はDirectPlayを使用して、簡単なネットワークゲームを作ってみたいと思います。

〜 サービスプロバイダ 〜
DirectPlayの通信方法には、4つのサービスプロバイダと呼ばれるものが用意されており
開発者は、これらの通信方法を意識せずに送受信を行う事が出来ます。
サービスプロバイダの種類は、下記の通りです。
「TCP/IP」・・・TCP/IPプロトコル。
「IPX」・・・・・・・IPXプロトコル。
「TAPI」・・・・・・モデム
「SerialPort」・・シリアルポート

ここでは、インターネット上でのネットワークゲームを目指しているので、
TCP/IPプロトコルを使用します。

〜 通信形態 〜
DirectPlayには、2つの通信モデルが用意されています。
1つは、「ピアツーピア」モデル、もう1つは「クライアント・サーバ」モデルです。

〜 「ピアツーピア」Peer to Peer 〜
接続されているノード同士が直接通信を行う形態です。
例えば10のノードが接続されていた場合、1つのノードがデータ送信を行うと、
そのノードが他のノード全てにデータを送信します。
クライアント側とサーバ側のプログラムを別々に組まなくていいぶん、お手軽かもしれませんが、
ノードが増えると、比例して送受信量が増え、ノード個々の負荷増加という弱点があります。
親子関係で例えると全ノードが親のような、通信形態ですが、実はどこかのノードがホストに
なっています。
このホスト役のノードが切断した場合、DirectPlayが自動的にホストを変更してくれます。
(自動的にホスト移行させないようにも設定出来ます)

〜 「クライアント・サーバ」 Client/Server 〜
各クライアントは、サーバとだけ通信を行う形態です。
サーバが親、クライアントが子、というシステム的に分かり易い形態。
「ピアツーピア」と比べると、クライアントが増えても、送信するのはサーバに対してだけなので、
クライアント側の負荷が増加しません(一般的には)。
しかしサーバが一手に全クライアントの送受信を行うので、サーバ側の環境(回線速度やマシン性能等)が
よいないといけません。

ここでは、「クライアント・サーバ」モデルで作っていきます。

●通信するまでの流れ

DirectPlayの「クライアント・サーバ」モデルの簡単な流れを説明します。
基本的にクライアントもサーバも、初期化や送受信などの手順はほとんど同じなので、
ここではサーバ側の説明をします。

〜 インクルードファイルとライブラリの追加 〜
当然ですが、インクルードファイルとライブラリを追加しなければなりません。
#include <dplay8.h> ・・・インクルードファイル
dplay.lib ・・・・・・・・・・・・ライブラリ


〜 COMライブラリの初期化 〜

CoInitializeEx( NULL, COINIT_MULTITHREADED );


〜 IDirectPlay8Serverのインターフェイスの取得 〜

IDirectPlay8Server  *g_pDPServer = NULL;

hr = CoCreateInstance( CLSID_DirectPlay8Server, NULL,
                       CLSCTX_INPROC_SERVER,
                       IID_IDirectPlay8Server,
                       (LPVOID*)&g_pDPServer );


〜 IDirectPlay8Serverインターフェイスの初期化 〜
ここで、メッセージハンドラを指定します。
ここで指定する関数はコールバック関数で、メッセージを受信した時や、新しいクライアントが
接続しようとしている時などに呼び出されます。

hr = g_pDPServer->Initialize( NULL, DirectPlayServerMessageHandler, 0 );


〜 IDirectPlay8Serverへ渡すメッセージハンドラ 〜

HRESULT WINAPI DirectPlayServerMessageHandler( PVOID pvUserContext,
                                         DWORD dwMessageId, PVOID pMsgBuffer )
{
  HRESULT hReturn = S_OK;

  switch( dwMessageId )
  {
	//-------------------------------------------------
	//	サーバ必須の処理
	//-------------------------------------------------
	case DPN_MSGID_CREATE_PLAYER:
		//プレーヤーが追加
		break;
	case DPN_MSGID_DESTROY_PLAYER:
		//プレーヤーが離れた
		break;
	case DPN_MSGID_RECEIVE:
		//メッセージを受信した
		break;
	case DPN_MSGID_INDICATE_CONNECT:
		//セッションに接続しようとしている
		break;
	case DPN_MSGID_INDICATED_CONNECT_ABORTED:
		//セッションに追加される前に削除された
		break;
	case DPN_MSGID_RETURN_BUFFER:
		//ユーザーバッファの処理が終了した
		break;
	//-------------------------------------------------
	//	サーバでは必須ではない
	//-------------------------------------------------
	case DPN_MSGID_TERMINATE_SESSION:
		break;
	case DPN_MSGID_ADD_PLAYER_TO_GROUP:
		break;
	case DPN_MSGID_APPLICATION_DESC:
		break;
	case DPN_MSGID_ASYNC_OP_COMPLETE:
		break;
	case DPN_MSGID_CLIENT_INFO:
		break;
	case DPN_MSGID_CONNECT_COMPLETE:
		break;
	case DPN_MSGID_CREATE_GROUP:
		break;
	case DPN_MSGID_DESTROY_GROUP:
		break;
	case DPN_MSGID_ENUM_HOSTS_QUERY:
		break;
	case DPN_MSGID_ENUM_HOSTS_RESPONSE:
		break;
	case DPN_MSGID_GROUP_INFO:
		break;
	case DPN_MSGID_HOST_MIGRATE:
		break;
	case DPN_MSGID_PEER_INFO:
		break;
	case DPN_MSGID_REMOVE_PLAYER_FROM_GROUP:
		break;
	case DPN_MSGID_SEND_COMPLETE:
		break;
	case DPN_MSGID_SERVER_INFO:
		break;
  }
  return hReturn;
}

クライアントが接続しようとするとと、最初にDPN_MSGID_INDICATE_CONNECT(接続させてくれ)が
サーバへ届きます。サーバでは、ここで接続を許可したり拒否したり出来ます。
(S_OKを返すと許可、それ以外を返すと拒否)
上の例では、無条件でS_OKをクライアントへ返しているので、また直ぐにクライアントから
DPN_MSGID_CREATE_PLAYER(接続したよ)が届きます。


〜 TCP/IPの検索 〜
現在使用可能なサービスプロバイダを列挙してくれるEnumServiceProvidersという
便利なメソッドが用意されているので、これを使ってTCP/IPを検索します。
下の例ではm_guidTcpipProviderにTCP/IPサービスプロバイダのGUIDが入るようになっています。

GUID                       m_guidTcpipProvider;

HRESULT                    hr = E_FAIL;
DWORD                      dwItems      = 0;
DPN_SERVICE_PROVIDER_INFO  *pdnSPInfo   = NULL;
DWORD                      dwSize       = 0;
TCHAR                      szName[MAX_PATH];

dwSize = 1024;
pdnSPInfo = (DPN_SERVICE_PROVIDER_INFO*) new BYTE[dwSize];
hr = m_pDPServer->EnumServiceProviders( NULL, NULL, pdnSPInfo, &dwSize, &dwItems, 0 );
if( FAILED(hr) && hr != DPNERR_BUFFERTOOSMALL )
{
  return hr;
}
if( hr == DPNERR_BUFFERTOOSMALL )
{
  // 一度エラーを出してdwSizeを得る
  SAFE_DELETE( pdnSPInfo );
  pdnSPInfo = (DPN_SERVICE_PROVIDER_INFO*) new BYTE[dwSize];
  if( FAILED( hr = m_pDPServer->EnumServiceProviders( NULL, NULL, pdnSPInfo,
                                                      &dwSize, &dwItems, 0 ) ) )
  {
    return hr;
  }
}

DPN_SERVICE_PROVIDER_INFO* pdnSPInfoEnum = pdnSPInfo;
for ( DWORD i = 0; i < dwItems; i++ )
{
  DXUtil_ConvertWideStringToGeneric( szName, pdnSPInfoEnum->pwszName );
  if( strstr( szName, "TCP/IP" ) != NULL ){
    memcpy( &m_guidTcpipProvider, &pdnSPInfoEnum->guid, sizeof(GUID) );
    hr = S_OK;
    break;
  }
  pdnSPInfoEnum++;
}

SAFE_DELETE( pdnSPInfo );

return hr;


〜 サービスプロバイダの指定 〜
TCP/IPのサービスプロバイダを得る事が出来たら、次は実際にサービスプロバイダの指定を行います。

IDirectPlay8Address  *m_pDeviceAddress = NULL;

#define SVR_PORT 2333  //使用するポート番号

HRESULT  hr;
GUID     *pGuid = &m_guidTcpipProvider;  //事前に得たTCP/IPサービスプロバイダのGUID

DPN_SP_CAPS dpspCaps;
ZeroMemory( &dpspCaps, sizeof(DPN_SP_CAPS) );
dpspCaps.dwSize = sizeof(DPN_SP_CAPS);

//指定されたサービス プロバイダの DPN_SP_CAPS 構造体を取得する。
if( FAILED( hr = m_pDPServer->GetSPCaps( pGuid, &dpspCaps, 0 ) ) )
{
  return hr;
}

SAFE_RELEASE( m_pDeviceAddress );

hr = CoCreateInstance( CLSID_DirectPlay8Address, NULL,CLSCTX_INPROC_SERVER,
                       IID_IDirectPlay8Address, (LPVOID*) &m_pDeviceAddress );
if( FAILED(hr) )
{
  return hr;
}

//アドレスオブジェクトのサービスプロバイダ GUID を設定する。
if( FAILED( hr = m_pDeviceAddress->SetSP( pGuid ) ) )
{
  return hr;
}

//ポートを指定
DWORD port = SVR_PORT;
hr = m_pDeviceAddress->AddComponent(L"port",(void*)&port,sizeof(DWORD),DPNA_DATATYPE_DWORD);

return S_OK;


〜 起動 〜
IDirectPlay8Addressの設定が終わったら、Hostメソッドを呼んで、サーバを立ち上げます。

TCHAR  m_strLocalPlayerName[MAX_PATH];  //プレイヤー名
TCHAR  m_strSessionName[MAX_PATH];      //セッション名
DWORD  m_dwMaxPlayers;                  //接続可能なプレイヤー最大数 (0で無制限)
GUID   m_guidApp;                       //アプリケーションのGUID

HRESULT hr;

//プレイヤー情報設定
WCHAR wszName[MAX_PATH];
DPN_PLAYER_INFO dpPlayerInfo;
DXUtil_ConvertGenericStringToWide( wszName, m_strLocalPlayerName );
ZeroMemory( &dpPlayerInfo, sizeof(DPN_PLAYER_INFO) );
dpPlayerInfo.dwSize = sizeof(DPN_PLAYER_INFO);
dpPlayerInfo.dwInfoFlags = DPNINFO_NAME;
dpPlayerInfo.pwszName = wszName;
if( FAILED( hr = m_pDPServer->SetServerInfo( &dpPlayerInfo, NULL, NULL, DPNOP_SYNC ) ) )
{
  return hr;
}

//セッション情報指定
WCHAR wszSessionName[MAX_PATH];
DXUtil_ConvertGenericStringToWide( wszSessionName, m_strSessionName );
DPN_APPLICATION_DESC dnAppDesc;
ZeroMemory( &dnAppDesc, sizeof(DPN_APPLICATION_DESC) );
dnAppDesc.dwSize          = sizeof(DPN_APPLICATION_DESC);
dnAppDesc.guidApplication = m_guidApp;
dnAppDesc.pwszSessionName = wszSessionName;
dnAppDesc.dwMaxPlayers    = m_dwMaxPlayers;
dnAppDesc.dwFlags         = DPNSESSION_CLIENT_SERVER;  //クライアント・サーバ指定

if( FAILED( hr = m_pDPServer->Host( &dnAppDesc,
                                    &m_pDeviceAddress,
                                    1,
                                    NULL, NULL,
                                    NULL,
                                    DPNHOST_OKTOQUERYFORADDRESSING ) ) )
{
  return hr;
}

return S_OK;


これでサーバが起動しました。
後は、クライアントから接続要求が来たり、データを受信したりすると
メッセージハンドラへ通達されるので、それらを処理します。


〜 クライアントへの送信 〜
クライアントへデータを送信するには、SendToメソッドを使用します。
各クライアントにはDPNIDという個別のIDが割り当てられています。
DirectPlayは、このDPNIDでメッセージ(データ)の宛先を管理しています。
サーバからクライアントへ送信する時には、そのクライアントのDPNIDと
送信するデータを指定します。
※クライアント側はサーバ宛にしか送信しないので、このDPNID指定はありません。

void ServerSendData( DPNID dpnid, BYTE* pBufferData, DWORD dwBufferSize ){
  DPN_BUFFER_DESC  bufferDesc;
  DPNHANDLE        hAsync;

  bufferDesc.dwBufferSize = dwBufferSize;
  bufferDesc.pBufferData  = (BYTE*)pBufferData;

  g_pDPServer->SendTo( dpnid,        //データを受信するクライアントまたはグループの識別子
                       &bufferDesc,  //DPN_BUFFER_DESC 構造体
                       1,            //構造体の数
                       0,            //メッセージの送信を待つ時間 (ミリ秒単位)
                       NULL,         //ユーザー指定のコンテキストへのポインタ
                       &hAsync,      //CancelAsyncOperationに渡して処理を取り消すことができるハンドル
                       DPNSEND_GUARANTEED );  //保証付き配信方式
}


〜 終了時の処理 〜
DirectPlayのインターフェイスをリリースします。

SAFE_RELEASE( m_pDeviceAddress );
g_pDPServer->Close(0);
SAFE_RELEASE( g_pDPServer );

COMライブラリをクローズします。

CoUninitialize();


〜 クライアント側の処理 〜
クライアントがサーバへ接続するには、IDirectPlay8Clientインターフェイスの
Connectメソッドを使用します。
待ち受けているサーバの「IPアドレス」、「ポート番号」、「セッション名」この3つの
情報があれば接続出来ます。

クライアントのメッセージハンドラで処理しなければならない必須のメッセージは3つです。

DPN_MSGID_RECEIVE            //メッセージを受信した
DPN_MSGID_TERMINATE_SESSION  //セッションがホストによって終了された
DPN_MSGID_RETURN_BUFFER      //ユーザーバッファの処理が終了した


●Tips?

DirectPlayの「クライアント・サーバ」モデルのサーバ部分だけを大雑把に説明しました。
もっと詳しく!という方は、SDKのサンプルや他の詳しく解説しているサイトを見ていただくとして、
ここでは自分が引っかかった部分だけを書き残しておきます。

〜 サービスプロバイダからTCP/IPを検索する 〜
EnumServiceProvidersで得た利用可能なサービスプロバイダからTCP/IPを選ぶとき、
DPN_SERVICE_PROVIDER_INFOのpwszNameが"DirectPlay8 TCP/IP Service Provider"か
どうかで判断していたのですが、環境によっては"DirectPlay8 TCP/IP Service プロバイダ"
だったり、色々あるようなので"TCP/IP"だけで検索すようにしました。(これでいいのか?)

〜 ポート番号を指定する 〜
DirectPlayは、ポートの指定がないと、2300番〜2400番の空いているポートを適当に選んで使います。
サーバのポートが起動する度に変わっては、クライアントが接続する時に困ります。
サーバ側は毎回同じポート番号で起動するようにしたほうがよいでしょう。
逆にクライアント側は自動的に取得させると、デバッグの時に1台のPCで、
複数のクライアントを起動出来るので便利かもしれません。

ポートの指定はAddComponentを使います。

IDirectPlay8Address *m_pDeviceAddress;

DWORD port = ポート番号;
hr = m_pDeviceAddress->AddComponent( L"port",
                    (void*)&port, sizeof(DWORD), DPNA_DATATYPE_DWORD );

DirectPlayの使用するポートは下記の範囲内という事です。
※市販のネットワークゲームなどで、うまく繋がらない時は、ルータで2300〜2400の
 ポートを静的マスカレードに設定してください。などと書いてあるのはこの為です。

 tcp 47624
 tcp 2300〜2400
 udp 2300〜2400







■サブサーフェイス・スキャタリング [2002.07.20]

★実行ファイルのダウンロード
dx8bssrdf.zip (281Kbyte)
スペースキーで、描画モード変更。
Zキーで、モデルカラー変更。(青赤緑)
マウスドラッグで視点移動、ホイールで視点距離調整。
ESCキーで終了。


サブサーフェイス・スキャタリング(Subsurface Scattering)とは、物体内の光の透過具合を表現する手法です。
例えば、手を太陽に向けた時、皮膚の輪郭が少し透けて見えるあの感じですね。
勿論、論文にあるような計算は全くしていませんが、それっぽく見えるような見えないような...。


●考え方
論文では物体内部に入った光が、内部で乱反射し、入射点と異なる点から光が出て行く現象を近似しているようなので、
簡単な計算で内部の光をどうやって干渉させるかを考えました。

1)頂点の内部に仮想の点Pがあると考えます。すべて頂点から一定距離(Len)、離れています。
2)モデルの頂点法線と光源ベクトルとの内積(dp)をとります。(法線が光源に向かうほど大きくなる)
3)点Pを中心とした半径dp*Rsの球体の判定をとります。
4)接触していた場合、自分のdpよりも相手のdpが小さかったら、自分のdp*Lsを相手に分け与えます。

この判定を全頂点に対して行い、最終的な各点Pのdpをデフューズカラーに変換して使用します。
結局やっている事は、球の判定とって、色を変えているだけだったりします。

必要なパラメーターはこの3つです。

 Len・・頂点から仮想点への距離
 Rs・・・球のスケーリング用係数
 Ls・・・光の伝導率

このクマさんは身長約0.3なのですが、3つのパラメータはこんな感じです。
Len = 0.020f
Rs = 0.075f
Ls = 0.026f


●概念図
図で説明すると下記のようになります。
下の例だと、光源に最も垂直な頂点aの内部にある仮想の球が一番大きくなります。
4つの球体のは次のような計算をとり、光を伝えて行きます。

 a.dp += 0;            ...球aより大きな球は接触していないのでゼロ
 b.dp += a.dp * Ls;  ...球bは a>b なのでa.dp*Lsを加算
 c.dp += b.dp * Ls;  ...球cは b>c なのでb.dp*Lsを加算
 e.dp += a.dp * Ls;  ...球eは a>e なのでa.dp*Lsを加算



●サブサーフェイス・スキャタリングのようなデフューズカラーを設定する関数
この関数で頂点に付いているデフューズカラーを書き換えています。
(下で書いているSSBとは頂点内部にある仮想球の事です)


/*------------------------------------------------------
  パラメータ
------------------------------------------------------*/
#define  SSBDepth         (0.020f)  //頂点とSSBとの距離
#define  SSBkr            (0.075f)  //SSB半径の係数
#define  SSBconductivity  (0.026f)  //SSB間の伝導率
#define  SSBkcolor        (0.95f)   //SSBカラー変換率

/*------------------------------------------------------
  定義
------------------------------------------------------*/
struct NDVERTEX {
  float     x,y,z;
  float     nx,ny,nz;
  D3DCOLOR  color;
  float     tu,tv;
};

#define  FVF_NDVERTEX  (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_DIFFUSE|D3DFVF_TEX1)

typedef struct {
  float        Pow;
  D3DXVECTOR3  Pos;
} SubsurfaceObj;

/*------------------------------------------------------
  頂点デフューズカラーを設定する関数

in  pMesh     メッシュ構造体
    pMeshVB   頂点バッファ
    pMatWorld ワールド行列
    pLightDir 光源ベクトル(正規化)
    pColor    RGBカラー(0〜1)
------------------------------------------------------*/
void SetSubsurfaceColor( LPD3DXMESH pMesh, LPDIRECT3DVERTEXBUFFER8 pMeshVB,
 D3DXMATRIX *pMatWorld, D3DXVECTOR3 *pLightDir, D3DXVECTOR3 *pColor ){
  D3DXVECTOR4    v40;
  D3DXVECTOR3    v30,WPos,WNormal;
  D3DXVECTOR4    InvLightDir;
  D3DXMATRIX     matInvWorld;
  NDVERTEX       *pVtx,*pTopVtx;
  SubsurfaceObj  *ssobj;
  float          dp;
  int            i,j;

  DWORD VerticesNum = pMesh->GetNumVertices();
  D3DXMatrixInverse( &matInvWorld, NULL, pMatWorld );
  D3DXVec3Transform( &InvLightDir, pLightDir, &matInvWorld );
  ssobj = (SubsurfaceObj*)malloc(VerticesNum * sizeof(SubsurfaceObj));
  pMeshVB->Lock(0,0,(BYTE**)&pTopVtx,0);

  // 光の強さ測定
  pVtx = pTopVtx;
  for(i=0;i<VerticesNum;i++){

    WPos.x = pVtx->x;
    WPos.y = pVtx->y;
    WPos.z = pVtx->z;
    WNormal.x = pVtx->nx;
    WNormal.y = pVtx->ny;
    WNormal.z = pVtx->nz;
    ssobj[i].Pos = WPos + (WNormal * (-SSBDepth)); // このPosは一度計算するとOK 

    //法線と光源ベクトルの内積
    dp = D3DXVec3Dot((D3DXVECTOR3*)&pVtx->nx, (D3DXVECTOR3*)&InvLightDir);
    if( dp < 0.f ){
      ssobj[i].Pow = 0.f;     // 暗いほう( dp = -1.f〜0 )
    } else {
      ssobj[i].Pow = dp;      // 明るいほう( dp = 0〜1.0f )
    }
    pVtx++;
  }

  // 接触判定
  for(i=0;i<VerticesNum;i++){
    for(j=0;j<VerticesNum;j++){
      if( i != j ){
        v30 = ssobj[i].Pos - ssobj[j].Pos;
        float Len = D3DXVec3Length( &v30 );
        float Pow = (ssobj[i].Pow * SSBkr) + (ssobj[j].Pow * SSBkr);
        if( Len <= Pow && Pow != 0.f ){
          float CrossLen = Pow - Len;
          float CrossP = CrossLen / Pow;
          if( ssobj[i].Pow > ssobj[j].Pow ){
            //自分よりもPowが小さければ、相手のPowへ加算
            ssobj[j].Pow += ssobj[i].Pow * SSBconductivity * CrossP;
            if( ssobj[j].Pow > 1.f ) ssobj[j].Pow = 1.f;
          }
        }
      }
    }
  }

  // 頂点カラーへ変換
  pVtx = pTopVtx;
  for(i=0;i<VerticesNum;i++){
    DWORD r,g,b;
    r = (DWORD)(ssobj[i].Pow * SSBkcolor * 255.f * pColor->x);
    g = (DWORD)(ssobj[i].Pow * SSBkcolor * 255.f * pColor->y);
    b = (DWORD)(ssobj[i].Pow * SSBkcolor * 255.f * pColor->z);
    pVtx->color = D3DCOLOR_RGBA(r,g,b,255);
    pVtx++;
  }

  pMeshVB->Unlock();
  free( ssobj );
}

VertexShaderは下記になります。
前回と違うのは v5 のデフューズカラーが追加された事です。

//--------------------------------------------------
// v5	DIFFUSE COLOR
//--------------------------------------------------

 vs.1.0
 dp4  oPos.x,  v0,    c0     // 透視変換
 dp4  oPos.y,  v0,    c1
 dp4  oPos.z,  v0,    c2
 dp4  oPos.w,  v0,    c3

 mov  oT0.xy,  v7            // テクスチャー

 dp4  r0.w,    v3,    c13    // 平行光源
 mul  r1.xyz,  r0.w,  c14
 add  r1.xyz,  r1,    v5     // Diffuse Color
 add  oD0.xyz, r1,    c15    // Light Ambient Color


●動作確認
実行中にスペースキーを押すと、内部にある「仮想球」を表示します。
光源に向かうほど、内部の仮想球が大きくなり、暗いほうの球へ伝わっているのが確認出来ます。



●レンダリングの比較
レンダリング画像の比較です。
これも実行中にスペースキーを押すと、ノーマルなレンダリング状態を見ることが出来ます。

ノーマルな描画。

Subsurface Scatteringのような描画。
物体の薄い部分ほど透けているような感じになります。


●問題点
・パラメータをモデルに合わせて調整しないと、いい具合にならない。
 モデルの大きさや、最も薄い部分などを考慮しつつパラメータを調整しないと、それなりに見えません。
 また、メッシュの頂点間隔が出来るだけ均一なモデルほどよいです。

・頂点の計算順番は単に格納されている順番なので、実は正しく伝達されていません。
 この方法で正しく伝達させるには「光の大きさ」でソートして...とかしないとダメでしょう。

・やはり全頂点に対しての操作なので、それなりに重いです。
 レンダリングの前段階で、全方向(64個くらい?)の光源計算をしてカラーセットを全部持ち、
 あとはカラー補間に徹するというのもいいかもしれません。


●ついでにレンズフレアも
やってみました。最初は光源位置のZバッファを読み出して、遮蔽物の判定をしようと
思ったのですが、Zバッファの読み出しは出来ないようなので、挫折。
おとなしく、D3DXIntersect関数を使いました。


●参考文献
・月刊CG WORLD vol39
・SIGGRAPH2001でスタンフォード大学のヘンリ・ヴァン・ジャンセン氏が発表した論文
 A Practical Model for Subsurface Light Transport の論文
 話題のこの本もHenrik Wann Jensen (著)ですね。




■落書きシェーダー [2002.07.11]

★実行ファイルのダウンロード
dx8graffiti.zip (579Kbyte)
スペースキーで、本体モデル描画ON/OFF。
マウスドラッグで視点移動、ホイールで視点距離調整。
ESCキーで終了。


バーテックスシェーダーの勉強という事で、落書き風(?)に見えるシェーダーを書いてみました。
と言っても、シェーダーを使ってみたかっただけで、実は用意したテクスチャーを合成しているだけです。


●VertexShader
バーテックスシェーダーは下記の通りになります。

//--------------------------------------------------
// v0     頂点座標
// v3     法線ベクトル
// v7     テクスチャ座標
// c0-3   World * View * Proj 行列
// c12    定数(0.0f, 0.5f, 1.0f, 2.0f)
// c13    Light Vector
// c14    Light Diffuse Color
// c15    Light Ambient Color
// c16-19 World * View * Proj 行列(Screen変換用)
//--------------------------------------------------

 vs.1.0
 dp4  oPos.x,  v0,    c0     // 透視変換
 dp4  oPos.y,  v0,    c1
 dp4  oPos.z,  v0,    c2
 dp4  oPos.w,  v0,    c3

 mov  oT0.xy,  v7            // ベーステクスチャー

 dp4  r0.w,    v3,    c13    // 平行光源
 mul  r1.xyz,  r0.w,  c14
 add  oD0.xyz, r1,    c15

 dp4  r0.x,    v0,    c16    // スクリーン座標からUV計算
 dp4  r0.y,    v0,    c17
 dp4  r0.w,    v0,    c19
 rcp  r0.w,    r0.w
 mul  r0.xy,   r0,    r0.w
 mul  r0.xy,   r0,    c12.yy
 add  oT1.xy,  r0,    c12.yy // マスクテクスチャー

c16-19は画面上でテクスチャを上下反転させる為に用意しました。

D3DXMATRIX mat;
D3DXMatrixIdentity( &mat );
mat(1,1) = mat(1,1) * -1.0f;
mat = matWorld * matView * mat * matProj;
D3DXMatrixTranspose( &mat,  &mat );
LPD3DDEV->SetVertexShaderConstant(16, &mat, 4);


●落書き風に見せるテクスチャー
合成しているテクスチャーです。それぞれ数枚パターン持っていて、パタパタアニメさせています。

アヒル本体へ合成しているテクスチャ。
(左がRGB画像、右がアルファ画像)

輪郭用テクスチャ(アヒルのボディー部分)。

輪郭用テクスチャ(アヒルのクチバシ部分)。

●輪郭
輪郭は、元のモデルを少しだけスケールアップして、裏側を表示させています。
(この単純な方法で輪郭抽出をする時は、モデリングソフト側で奇麗にスケールアップしたモデルを用意したほうがよいかもしれません)
本体モデルを消して、輪郭モデルだけを表示すると、こんな感じになります。


これにトゥーンシェーダーもプラスしようと考えていたのですが、自分のグラフィックカードでは、
テクスチャステージが2つまでしか使えないらしいので、とりあえずここで終了。




■クロスシミュレーション [2002.05.19]

★実行ファイルのダウンロード
dx8cloth.zip (253Kbyte)
スペースキーで、シーンリセット。
マウスドラッグで視点移動、ホイールで視点距離調整。
ESCキーで終了。
推奨:画面モード32bit


前回のMass-Spring Modelを少し変えてClothSimもどきのような感じにしてみました。
空気抵抗なし、布同士の判定なしの状態です。



CGWorldで「モンスターズインク」のクロスシミュレーション記事(ブーの服とか)を読みましたが
相当苦労されたようですね。次回作も楽しみにしております。




■スプリング・ボックス [2002.04.01]

★実行ファイルのダウンロード
dx8springbox.zip (250Kbyte)
[Z][X][C][V]キーで、各ボックスがジャンプ。
スペースキーで、描画モード切替(テクスチャー / スプリングのワイヤー)。
マウスドラッグで視点移動、ホイールで視点距離調整。
ESCキーで終了。
注意)画面モードを16bitにして下さい。


Mass-Spring Modelを実装してみました。
Soft Body Dynamics Simulationではよく使われる計算モデルの1つのようです。
また、今回はシンプルなシャドウマッピングも行いました。


●スプリング
Mass-Spring Modelとは、質量のある点を、スプリング(バネ)で繋げて、弾性変形させる計算モデルです。
この計算モデルをベースにした、布・旗・髪の毛などのシミュレーションも、多くあるようです。

サンプルのボックスは、8頂点を28本のスプリングで繋げています。

△上の図は「スペースキー」を押して、スプリングをワイヤー表示している様子。

※Soft Body Dynamicsは、その物体の運動と変形をシミュレーションします。
 物体が変形しない(剛体)シミュレーションは、Rigit Body Dynamicsと言います。


●衝突判定
物体間の判定は、かなり手抜き。
めり込むし、挙動が不自然です。
そして、たまに大暴れして、収拾がつかなくなる場合もあります。


●シャドウのシンプルな計算
床への投影は、XZ平面限定のシンプルなものにしました。
平面(床)に投影された点を、用意した投影テクスチャー上の(x,y)座標に
変換し、その座標を使って投影テクスチャーにポリゴンを描画します。


 Ts              投影テクスチャーのサイズ
 Len             XZ面にある平面の直径
 Py              XZ面にある平面のY座標(平面の高さ)
 L(x,y,z)        ライト座標
 Pos(x,y,z)      頂点座標
 S(x,y)          投影テクスチャー上の座標

 k = Ts / Len;
 kx = -L.x / (L.y - Py);
 kz = -L.z / (L.y - Py);
 S.x = (Pos.x + kx * (Pos.y - Py)) * k + (Ts / 2.f);
 S.y = (Pos.z + kz * (Pos.y - Py)) * k + (Ts / 2.f);

光源は、非常に遠くにあると仮定した平行光源の計算ですから、
影の遠くになる程、影が大きくなる現象は起きません。

△上の図は投影計算のイメージ図。シンプルです。


●シャドウのレンダリング
今回「影」は、3パスで生成したテクスチャーを、床にマルチテクスチャーで張っています。
処理の流れは、「被写界深度」でやった事と同じ感じです。


 1)256*256のテクスチャーに投影面を描画・・・[Tx0]
 2)128*128のテクスチャーに[Tx0]を描画・・・[Tx1]
 3)64*64のテクスチャーに[Tx1]を描画・・・[Tx2]
 4)[Tx2]のテクスチャーを床面に使う

なぜ、64*64のテクスチャーにダイレクトに描画して、1度で済ませずに、3回も描画するのか?
それは、エッジをソフトに「ぼかす」為です。(と言っても、まだブロックが見えますが)
拡縮して描画した時にバイリニアフィルターの効果で、画像がぼけますが、そのぼけた画像を
使って、もう一度バイリニアフィルターを通す事で「ぼけた画像」→「ぼけた画像」となり、
シャドウのエッジがソフトになって行きます。

ただし、そのまま張ると、影がはみ出した際に、逆サイドに影の一部が出てしまいます。
(バイリニアフィルターは逆サイドの色も拾う為)
安易ですが、UV座標を少しだけずらして、この問題を回避しています。
(0.05f,0.05f)-(0.95f,0.95f)

256*256サイズ 128*128サイズ 64*64サイズ

△上の図は投影テクスチャーのレンダリングの様子です。(実際にGeForce2が出力した画像)
 右側の64*64のテクスチャーを床にマッピングすると、下の図のように影のエッジがぼけた
 画像を得ることが出来ます。


DirectXでは、ステンシルバッファを用いてシャドウマップする方法が一般的なようですが、
その場合、影のエッジを「ぼかす」事は出来るのかな?

追記:できるみたいですね。




■グレア・フィルター [2002.03.17]

★実行ファイルのダウンロード
dx8glare.zip (197Kbyte)
マウスドラッグで視点移動。
ESCキーで終了。
注意)画面モードを16bitにして下さい。


Xboxの某レースゲームで表現されている「グレア・フィルター」の実験。
レンズフレアのような感じで、キラキラ光るような効果です。
アヒルモデルのゴールド化を試みましたが、うまくいきませんでした。

●光
フィルターの生成プロセスは下記のイメージです。
先ず、スペキュラーライトとマテリアル設定で、ハイライトの部分だけの画像を作ります。
次にその画像を、縦長・横長の2つのテクスチャーに描画します。
その2つのテクスチャーを加算合成で元画像に(引き伸ばして)重ねています。
バイリニアフィルターの「引き伸ばし」によって起こる現象を利用しています。





■水面シミュレーション [2002.03.08]

★実行ファイルのダウンロード
dx8water01.zip ver0.1 (425Kbyte)
カーソルキーの上下でアヒル前進後進、
左右で回転、Xキーで潜水、Zキーでジャンプ。
マウスドラッグで視点移動、ホイールで視点距離調整。
ESCキーで終了。
注意)画面モードを16bitにして下さい。

■更新履歴
初版->ver0.1 ドラッグで視点移動、バイリニアフィルタONにした。

■このサンプルの効果音は、下記サイトのフリー素材を使用させて頂きました。
ありがとうございます。

〜 音と声のOnLine素材集 〜 WEB WAVE LIB
WEB WAVE LIB〜音と声のOnLine素材集

T-CISUM TK's Music Laboratory


今ではよく目にする水面のシミュレーションをやってみました。
やはり、こういう固くないものは、いいですね。
サンプルは5匹のアヒルのうち、リボンを付けているアヒルを操作する事が出来ます。
他の4匹は、勝手に騒がしく動きますので、遊んでやって下さい。

●平均50fpsで動くこと前提
今回は、ちょと描画が重いので、画面サイズを256*256にして、
さらに、D3DSWAPEFFECT_COPY_VSYNCで同期を取るようにしています。
自分のPC(Pen4@1.7G / GeForce2MX200)だけでチューニングしたので、
平均50fps出ない環境では、うまく動作しないかもしれません。

●水面はFakeなマッピング
すべてFakeなマッピングです。
機会があれば、屈折マップ・環境マップ、そして流体力学・・・まじめに計算してる水面を作りたいと思います。
環境マップとして「空の画像」、屈折マッピングとして「水中の画像」をマルチテクスチャーで張っています。

水面の処理は、「Game Programming GEMS」を参考にしました。
水面をグリッドとして、隣接する頂点の高さが影響して、減衰して、、、という処理ですね。
こういうのに興味のある方は買って損はないかもしれません。

●描画処理の流れは次の通りです。

 1.テクスチャーレンダリング開始
 2.大理石プール描画
 3.アヒル描画
 4.アヒル(鏡面反射)描画
 5.テクスチャーレンダリング終了
 6.Zバッファクリア
 7.水面メッシュ描画
 8.大理石プール描画
 9.アヒル描画

途中で一度Zバッファをクリアしています。
なぜかここでクリアしてあげないと、Zバッファにノイズのような乱れが生じてしまい
うまく描画出来ませんでした。
本当はクリアしなくても、いけると思うのですが・・・。謎です。

●鏡面反射と平面クリッピング
アヒルの水面に映り込む部分は、鏡面反射のマトリックスで描画しています。
ただし、そのままだと、(本物の)アヒルが深く沈んだ時に、鏡面反射のアヒルが水面より
上に出て来てしまうので、どうしようか悩みましたが、SetClipPlaneという便利な
ピクセル単位でのクリッピング処理があるようなので、これを使いました。

△左の図は通常の状態、右の図は鏡面反射モデルだけ描画している状態です。
 ちゃんと水面でクリッピングされていますね。

鏡面反射と平面クリッピングは、こんな感じです。

D3DXMATRIX matRefView, matReflect;
D3DXPLANE plane;

//水面の平面を定義
D3DXPlaneFromPointNormal( &plane,
  &D3DXVECTOR3(0,WATER_Y_OFFSET,0),
  &D3DXVECTOR3(0,1,0) );

//平面の座標系を反転した行列を作成
D3DXMatrixReflect( &matReflect, &plane );

//現在のViewマトリックスと掛けて鏡面反射のマトリックスを作る
D3DXMatrixMultiply( &matRefView, &matReflect, &sysView );

//水面より上の部分は描画しない
lpD3DDEV->SetClipPlane( 0, plane );
lpD3DDEV->SetRenderState( D3DRS_CLIPPLANEENABLE, 0x01 );

〜matRefViewでモデル描画〜

//戻す
lpD3DDEV->SetRenderState( D3DRS_CLIPPLANEENABLE, 0x00 );

●PS2発売前に公開され話題を呼んだ「アヒルちゃん」デモのパクりです。
水中のFakeな屈折マッピングは、たぶん同じ方法だと思います。(根拠なし)

●効果音
今回は、DirectXで効果音を鳴らす事にも挑戦
WAVを再生しているだけですが、簡単に実装出来ました。
やはり効果音鳴るといいですね。
WEB WAVE LIB様、T-CISUM TK's Music Laboratory様、ありがとうございました。




■陽炎(かげろう) [2002.02.24]

★実行ファイルのダウンロード
dx8shimmer.zip (275Kbyte)
カーソルキーの上下でUV係数、
左右でスプライトのサイズを変更出来ます。
ESCキーで終了。

遠くの背景が、ゆらゆら揺れ動く現象、陽炎(かげろう)のような効果の実験。


●マルチテクスチャー
今回は、マルチテクスチャーを使って、輪郭部分をぼかそうと思います。
「光学迷彩」のように、背景のテクスチャーと、アルファでマスクする為のテクスチャー2つを合成して描画します。

マルチテクスチャーで使用する、マスク用のテクスチャー(アルファ値)。RGB値は白です。

●頂点の定義
2つのテクスチャーのUV座標を個別に指定出来るように、UV座標を2つもった頂点を定義します。

struct TLVERTEXUV2 {
  float     x,y,z;
  float     rhw;
  D3DCOLOR  color;
  float     tu0,tv0;
  float     tu1,tv1;
};
#define	FVF_TLVERTEXUV2	(D3DFVF_XYZRHW|D3DFVF_DIFFUSE|D3DFVF_TEX2)

●描画前の設定
この例では、頂点カラーのアルファも参照するようになっているので、全体の透明度も調整可

//αブレンド有効
lpD3DDEV->SetRenderState(D3DRS_ALPHABLENDENABLE,TRUE);
//ノーマルアルファ
lpD3DDEV->SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
lpD3DDEV->SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);
//ブレンディング設定
lpD3DDEV->SetTextureStageState( 0, D3DTSS_COLOROP,   D3DTOP_BLENDTEXTUREALPHA );
lpD3DDEV->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
lpD3DDEV->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
lpD3DDEV->SetTextureStageState( 0, D3DTSS_ALPHAOP,   D3DTOP_MODULATE );
lpD3DDEV->SetTextureStageState( 0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE );
lpD3DDEV->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE  );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_COLOROP,   D3DTOP_MODULATE );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT );
lpD3DDEV->SetTextureStageState( 1, D3DTSS_ALPHAOP,   D3DTOP_DISABLE );
//テクスチャー指定
lpD3DDEV->SetTexture( 0, texture0 );  //マスク用のテクスチャー
lpD3DDEV->SetTexture( 1, texture1 );  //レンダーテクスチャー

●UV座標の設定
1枚目のテクスチャーのUV座標(tu0,tv0)は、マスク用のテクスチャーがそのまま張られるようにします。
2枚目のテクスチャーのUV座標(tu1,tv1)は、背景画像を少しずらしたUV座標にします。

あとは、通常通りにスプライトとして描画するだけです。


また、パラメータを変えると、レンズに付いた水滴のようにも見えます。


●お手軽エフェクト第二弾「陽炎」、いかがだったでしょうか。
G○3(某レースゲーム)の陽炎も、こんな感じで処理している?




■モーションブラー [2002.02.21]


モーションブラーの実験。
と、言っても、これもテクスチャーレンダリングの応用です。
方法は簡単。バックバッファのカラーをクリアしないようにして、今期間のレンダリング画像をアルファで合成して重ね書きする。
これだけです。(Zバッファはクリアしましょう)
前回の画像を「被写界深度」の時に作ったような、ダウンサイズしたテクスチャーとして重ねてもいいかもしれません。
(残像部分がボケた画像になるでしょう)

●サンプル画像は、下記のアルファブレンディング設定で合成しています

lpD3DDEV->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE);
lpD3DDEV->SetTextureStageState(0,D3DTSS_ALPHAOP,D3DTOP_MODULATE);
lpD3DDEV->SetTextureStageState(0,D3DTSS_ALPHAARG1,D3DTA_TEXTURE);
lpD3DDEV->SetTextureStageState(0,D3DTSS_ALPHAARG2,D3DTA_DIFFUSE);
lpD3DDEV->SetTextureStageState(0,D3DTSS_COLOROP,D3DTOP_MODULATE);
lpD3DDEV->SetTextureStageState(0,D3DTSS_COLORARG1,D3DTA_TEXTURE);
lpD3DDEV->SetTextureStageState(0,D3DTSS_COLORARG2,D3DTA_DIFFUSE);

//後はこのARGB値(アルファが128)で描画
D3DCOLOR_ARGB( 128, 255, 255, 255 )





■光学迷彩 [2002.02.19]

★実行ファイルのダウンロード
dx8stealth.zip (304Kbyte)
カーソルキーの上下で係数を変更出来ます。
マウスでモデルを回転させる事が出来ます。
ESCキーで終了。

テクスチャーにレンダリング出来たので、次はこれを応用して、光学迷彩のような効果を実験してみます。
△上の写真は、トラが光学迷彩を羽織って、空中に浮いている様子。

●背景の画像をテクスチャーとして張り付ける
背景のテクスチャーが用意出来ていれば、モデルのテクスチャーに「背景テクスチャー」を指定し、
あとはUV座標を書き込んでやります。
今まで表示していた、ボックスに張ってみた所、うまくいったので、やはりある程度のポリゴン数を
使ったモデルで効果を見てみたくなり、Xファイルのモデルに挑戦してみました。
(Xファイルとは、DirectX標準のモデルフォーマット)
実は、このXファイル内の頂点やUVにアクセスするのが一番苦労しました(笑)

頂点法線のスクリーン上での成分(nx,ny)を計算

D3DXVECTOR4  Out;
D3DXVECTOR3  VerticesNormal;  //頂点法線
D3DXVec3Transform(&Out, &VerticesNormal, matWorld);
D3DXVec3Transform(&Out, (D3DXVECTOR3*)&Out, matView);
nx = Out.x;
ny = Out.y;

3D座標 -> スクリーン座標変換

D3DXVECTOR3  VerticesPos;  //頂点座標
D3DXVec3Transform(&Out, &VerticesPos, matWorld);
D3DXVec3Transform(&Out, (D3DXVECTOR3*)&Out, matView);
D3DXVec3Transform(&Out, (D3DXVECTOR3*)&Out, matProj);
Out.x /= Out.w;
Out.y /= Out.w;
Out.z /= Out.w;
Out.x = (SCREEN_WIDTH/2) + Out.x * (SCREEN_WIDTH/2);
Out.y = (SCREEN_HEIGHT/2) + Out.y * (SCREEN_HEIGHT/2);

UV座標をずらす計算

uv.x = (Out.x - nx * s) / SCREEN_WIDTH;
uv.y = 1 - ((Out.y - ny * s) / SCREEN_HEIGHT);

●UV計算について
UV計算の(-n * s)の部分が、背景の画像を歪ませている部分です。
s = 0 の場合、画像の歪みは起らず、背景に溶け込みます。
(誤差で完全に背景と一致する事はありませんが)

法線ベクトルをスケーリングする事によって、UV座標を移動しています。
視点ベクトルに対して、垂直になるほど、UVがズレます。
例えば球体モデルを見た時に、その輪郭部分になる程、UVがずれて行きます。


△上の図は、UV計算のイメージ図です。
VNが頂点の法線。Eyeが視点ベクトル。SCREENが投影面です。
スケール値sが0だと、視点ベクトルをそのまま伸ばした位置の点を指します。
sをプラス値にして行くと、UV座標(図の赤い点)はモデルの内側に向かって行くので、盛り上がって見えます。
sをマイナス値にして行くと、モデルの外側の背景を拾うので、環境マッピングのような見え方になります。


実際、どれくらい歪んでいるか分かりやすいように、グリッド上で表示してみました。



パラメータをマイナスの数値にしていくと、トラがデュラル(懐!)へと進化します。



■Xファイル

●Xファイル内のデータにアクセスする #1

光学迷彩やる為に、Xファイルのデータ構造を、調べました。
わかった事を、書き残しておきます。

先ずは、D3DXLoadMeshFromXでXファイルをロードします。
その後、メッシュから情報を受け取って、Xファイルにアクセスします。

準備

LPD3DXMESH Mesh;  //Xファイルの有効なメッシュ
IDirect3DVertexBuffer8  *pVtxBuff;
IDirect3DIndexBuffer8  *pIdxBuff;
DWORD  FVFSize;
WORD  *pIdx;
BYTE  *pVtx;
DWORD  VtxFmt;
DWORD  NumFaces;
DWORD  NumVertices;

Mesh->GetIndexBuffer( &pIdxBuff );  //頂点インデックスのポインタ取得
Mesh->GetVertexBuffer( &pVtxBuff );  //頂点バッファのポインタ取得

NumVertices = Mesh->GetNumVertices();  //頂点数
NumFaces = Mesh->GetNumFaces();  //フェース数

//頂点インデックスをロック
pIdxBuff->Lock( 0, 3*sizeof(WORD)*NumFaces, (BYTE**)&pIdx, D3DLOCK_READONL );

//頂点バッファをロック
pVtxBuff->Lock( 0, 0, &pVtx, D3DLOCK_READONLY );

//頂点フォーマットのサイズを取得
VtxFmt = Mesh->GetFVF();
FVFSize = D3DXGetFVFVertexSize( VtxFmt );

フェース単位のアクセス

WORD *idx = pIdx;
for(i=0;i<NumFaces;i++){
  //3つの頂点を取得
  D3DXVECTOR3 v0, v1, v2;
  v0 = *(D3DXVECTOR3*)( pVtx + FVFSize * idx[0] );
  v1 = *(D3DXVECTOR3*)( pVtx + FVFSize * idx[1] );
  v2 = *(D3DXVECTOR3*)( pVtx + FVFSize * idx[2] );

  〜v0,v1,v2がポリゴンの頂点座標〜

  idx += 3;
}
頂点単位のアクセス

for(i=0;i<NumVertices;i++){
  D3DXVECTOR3 v0;
  D3DXVECTOR2 uv;

  //D3DFVF_XYZは先頭にあるので、オフセットは0
  v0 = *(D3DXVECTOR3*)( pVtx + (FVFSize * i) + 0 );

  //次にUVがあるので、D3DFVF_XYZのサイズ分だけ進めた位置から取り出す
  uv = *(D3DXVECTOR2*)( pVtx + (FVFSize * i) + sizeof(D3DXVECTOR3) );

  〜v0が頂点座標,uvがその頂点のテクスチャー座標〜

}
ロックの解除とリリース

pIdxBuff->Unlock();
pVtxBuff->Unlock();
pIdxBuff->Release();
pVtxBuff->Release();


△上記の例は、頂点フォーマットが (D3DFVF_XYZ | D3DFVF_TEX1) の場合です。
流れとしては、頂点フォーマットを調べて、1頂点内のデータを、その構造体サイズで取り出していけばいいようです。

とりあえず、メッシュの頂点やUVなどを操作出来るようになりました。



■被写界深度 [2002.02.16]

テクスチャーにレンダリングし、その画像を加工(変形)表示する事によって、様々な効果を得ることが出来ます。
その1つに「被写界深度」効果があります。
フォーカスが合っていなくて、ボケた感じの画像にする処理です。
手法としては、もう古いかもしれませんが、手軽に被写界深度のような効果を得る方法を紹介します。

先ずはテクスチャーにレンダリングします。そのテクスチャーを使って、1/2サイズのテクスチャに描画、
1/4サイズのテクスチャに描画・・・という感じで、どんどん小さいサイズのテクスチャーに描画していきます。

1/2サイズ 1/4サイズ 1/8サイズ 1/16サイズ



次に、縮小したテクスチャーを拡大して描画すると、テクスチャフィルタリングの効果でボケた画像を生成する事が出来ます。
後は、これらのテクスチャをスプライトとして、3D空間の奥に描画すると、遠くの物体が、ボケたような感じに見えます。

ボケた画像の種類を数枚用意すると、ボカす対象となる物体までの距離で画像を使い分けたり、複数のボケた画像をアルファで合成して滑らかに変化させる、といった事が出来るので便利です。

ただ、この方法は、単にボケた画像の壁を置いているようなものなので、手前がボケているように見せるのには、ちょっと工夫が必要です。

1/2サイズ(128*128)を
256*256に拡大して描画

1/4サイズ(64*64)を
256*256に拡大して描画

1/8サイズ(32*32)を
256*256に拡大して描画

1/16サイズ(16*16)を
256*256に拡大して描画

テクスチャーにレンダリングする準備

LPDIRECT3DTEXTURE8  T;     //レンダリングテクスチャー
LPDIRECT3DSURFACE8  TS;    //テクスチャーサーフェス
LPDIRECT3DSURFACE8  BB;    //バックバッファ保存用

//バックバッファを保存する
lpD3DDEV->GetRenderTarget( &BB );

〜CreateTextureでテクスチャー[T]、サーフェス[TS]を作っておく〜

テクスチャーにレンダリングする

LPDIRECT3DSURFACE8  ZBUFF;

//レンダリングさせたいテクスチャーをセットする
lpD3DDEV->GetDepthStencilSurface( &ZBUFF );
lpD3DDEV->SetRenderTarget(TS, ZBUFF );
lpD3DDEV->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,D3DCOLOR_XRGB(0,0,0), 1.0f, 0 );

〜ここで描画したものがテクスチャーにレンダリングされる〜

// 描画環境を戻す
lpD3DDEV->GetDepthStencilSurface( &ZBUFF );
lpD3DDEV->SetRenderTarget( BB, ZBUFF );

あとは、テクスチャー[T]を使う!です。



■フォント表示

Windowsで用意されているフォントを画面に表示しましょう。
簡単に文字が表示出来ると、何かと便利です。
「文字列」を表示する方法は、いろいろ考えられますが、今回はデバッグ用として、
(たとえ遅くとも)DrawTextを使用して表示します。


●フォント表示の流れ
基本的には、CreateFontしてDrawTextするだけなのですが、タスク処理の都合上、
キューイングして、システムが一括で処理するような流れにします。
処理の流れは次の通りです。(4つの関数を追加)

InitFont();		//DirectXの初期化後に一度呼ぶ

lpD3DDEV->BeginScene();

〜ここでFont::Print使う〜

DrawFont();		//BeginSceneとEndSceneの間で呼ぶ
FlushFont();		//DrawFont後に呼ぶ

lpD3DDEV->EndScene();

ReleaseFont();		//アプリケーション終了時に一度呼ぶ


●表示の方法
使い方は、グローバルな Font8/Font16/Font32 に文字列と座標、カラーを
指定するだけです。

//動的な文字列を表示(カラー指定は省略)
char tmp[128];
sprintf(tmp,"%d\n%d\n%d",x,y,z);
Font8.Print(tmp,8,8);

//静的な文字列を表示+カラー指定
Font32.Print("DirectX8",8,100,D3DCOLOR_RGBA(0,255,32,64));


●ソースファイル

font.h

#ifndef _FONT_H_
#define _FONT_H_

#define	FONT_STR_MAX_NUM    (256)  //最大文字列数
#define	FONT_STR_BUFF_SIZE  (256)  //1文字列のバッファサイズ

class WinFont{
private:
  LPD3DXFONT  lpFont;
  int         str_num;
  char        str_buff[FONT_STR_MAX_NUM][FONT_STR_BUFF_SIZE];
  int         str_x[FONT_STR_MAX_NUM];
  int         str_y[FONT_STR_MAX_NUM];
  D3DCOLOR    str_color[FONT_STR_MAX_NUM];
public:
  WinFont(){
    lpFont = NULL;
  };
  virtual void Init(LPDIRECT3DDEVICE8 lpD3DDEV, int h);
  virtual void Print(char *str, int x, int y, D3DCOLOR color=D3DCOLOR_RGBA(255,255,255,255));
  virtual void StrDraw(char *str, int x, int y, D3DCOLOR color=D3DCOLOR_RGBA(255,255,255,255));
  virtual void Draw();
  virtual void Flush();
  virtual void Release();
};

extern WinFont  Font8;
extern WinFont  Font16;
extern WinFont  Font32;

extern void InitFont();
extern void DrawFont();
extern void FlushFont();
extern void ReleaseFont();

#endif

font.cpp

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <d3d8.h>
#include <d3dx8.h>

WinFont  Font8;
WinFont  Font16;
WinFont  Font32;

/*------------------------------------------------------
  フォント表示・初期化 (DirextX初期化後に一度呼ぶ)
------------------------------------------------------*/
void InitFont(){
  Font8.Init(lpD3DDEV, 8);
  Font16.Init(lpD3DDEV, 16);
  Font32.Init(lpD3DDEV, 32);
}

/*------------------------------------------------------
  フォント表示・描画 (毎期間呼ぶ)
------------------------------------------------------*/
void DrawFont(){
  Font8.Draw();
  Font16.Draw();
  Font32.Draw();
}

/*------------------------------------------------------
  フォント表示・クリア (DrawFont後に呼ぶ)
------------------------------------------------------*/
void FlushFont(){
  Font8.Flush();
  Font16.Flush();
  Font32.Flush();
}

/*------------------------------------------------------
  フォント表示・終了処理 (Winアプリの終了時に一度呼ぶ)
------------------------------------------------------*/
void ReleaseFont(){
  Font8.Release();
  Font16.Release();
  Font32.Release();
}

/*------------------------------------------------------
  テキスト描画の初期化
------------------------------------------------------*/
void WinFont::Init(LPDIRECT3DDEVICE8 lpD3DDEV, int h){
  HDC    hDC = NULL;
  HFONT  hFont = NULL, hFontH = NULL;

  str_num = 0;
  hDC = CreateCompatibleDC(NULL);
  hFont = CreateFont( h, 0, 0, 0,
    FW_REGULAR,FALSE,FALSE,FALSE,SHIFTJIS_CHARSET,OUT_DEFAULT_PRECIS,
    CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH,
    "MS ゴシック" );
  if(!hFont) return;
  hFontH = (HFONT)SelectObject(hDC, hFont);
  if( FAILED( D3DXCreateFont( lpD3DDEV, hFont, &lpFont ) ) ) {
    MessageBox(NULL,"D3DXCreateFontの作成に失敗しました。","WinFont",MB_OK | MB_ICONSTOP);
    return;
  }
  SelectObject(hDC, hFontH);
  DeleteObject(hFont);
}

/*------------------------------------------------------
  テキスト描画
------------------------------------------------------*/
void WinFont::StrDraw( char *str, int x, int y, D3DCOLOR color ){
  RECT rect;

  rect.left = x;
  rect.top = y;
  rect.right = SCREEN_WIDTH;
  rect.bottom = SCREEN_HEIGHT;
  lpFont->DrawText(str, -1, &rect, DT_LEFT|DT_NOCLIP|DT_EXPANDTABS, color );
}

/*------------------------------------------------------
------------------------------------------------------*/
void WinFont::Release(){
  lpFont->Release();
  lpFont = NULL;
}

/*------------------------------------------------------
------------------------------------------------------*/
void WinFont::Flush(){
  str_num = 0;
}

/*------------------------------------------------------
  テキストを登録
------------------------------------------------------*/
void WinFont::Print( char *str, int x, int y, D3DCOLOR color ){
  if( FONT_STR_MAX_NUM > str_num ){
    if( FONT_STR_BUFF_SIZE > strlen( str ) ){
      str_x[str_num] = x;
      str_y[str_num] = y;
      str_color[str_num] = color;
      strcpy(str_buff[str_num], str);  //コピーしてしまう
      str_num++;
    }
  }
}

/*------------------------------------------------------
  登録されたテキストを一括で描画
------------------------------------------------------*/
void WinFont::Draw(){
  int i;
  for(i=0;i<str_num;i++){
    StrDraw( str_buff[i], str_x[i], str_y[i], str_color[i] );
  }
}


このサンプルでは、8/16/32のフォントサイズ3つを用意しています。
好みに合わせて、追加変更しましょう。




■入力系

DirectXのアプリケーションで、キーボード、マウス、ジョイスティック、ハンドルなどの
入力デバイスから情報を得るには、DirectInputを使用します。
少し長くなりますが、一気に行きます。

入力データの取得方法には、「直接データ」と「バッファデータ」の2種類あります。
「直接データ」は、取得する関数を呼んだ瞬間のデータ。
「バッファデータ」の方は、用意されたバッファにバッファリングされて行く方式です。
「直接データ」のほうは、データの取りこぼしが考えられます。
ここでは、最初に「直接データ」取得の説明をします。

●プロジェクトにインクルードとライブラリの追加が必要になります。

インクルードファイルの追加
#include <dinput.h>

ライブラリの追加
dinput8.lib

今のところ、必要なライブラリは下記の4ファイルになります。
d3d8.lib d3dx8.lib dinput8.lib dxguid.lib

●キーボードとマウスを初期化 (ジョイスティックはまだ持っていないので、今はこれだけ)

LPDIRECTINPUT8        lpDINPUT = NULL;
LPDIRECTINPUTDEVICE8  DIDeviceMouse = NULL;
LPDIRECTINPUTDEVICE8  DIDeviceKeyboard = NULL;

//入力デバイスの初期化
if(FAILED(DirectInput8Create(hInst, DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&lpDINPUT, NULL))){
  MessageBox(NULL,"DirectInput8Createに失敗しました。",CAPTION,MB_OK | MB_ICONSTOP);
} else {
  //マウスの初期化
  if(FAILED(lpDINPUT->CreateDevice(GUID_SysMouse, &DIDeviceMouse, NULL))){
  } else {
    if(FAILED(DIDeviceMouse->SetDataFormat(&c_dfDIMouse))){
    } else {
      // 協調レベルを設定する。
      if(FAILED(DIDeviceMouse->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE))){
      } else {
        //マウスへのアクセス権の取得
        DIDeviceMouse->Acquire();
      }
    }
  }
  //キーボードの初期化
  if(FAILED(lpDINPUT->CreateDevice(GUID_SysKeyboard, &DIDeviceKeyboard, NULL))){
  } else {
    if(FAILED(DIDeviceKeyboard->SetDataFormat(&c_dfDIKeyboard))){
    } else {
      // 協調レベルを設定する。
      if(FAILED(DIDeviceKeyboard->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE))){
      } else {
        //キーボードへのアクセス権の取得
        DIDeviceKeyboard->Acquire();
      }
    }
  }
}

●キーボードとマウスからのデータを受け取る
下記のInputControl()関数を毎期間呼ぶと、バッファにデータが入ってきます。
あとはそのデータを参照するだけです。
ちなみにDIMOUSESTATEは4ボタン対応、DIMOUSESTATE2は8ボタン対応の構造体す。

自分がアクティブではなくなった時にGetDeviceStateは、DIERR_INPUTLOSTを返します。
そして再度アクティブになると、DIERR_NOTACQUIREDを返してきますから、
その時は、Acquire()で再起動させます。

//キーボードとマウスのバッファ
char          KeyboardBuff[256];
DIMOUSESTATE  MouseBuff;

//キーボードのOn/Offをチェックするマクロ
#define  GetKB(dik_id)  (KeyboardBuff[dik_id] & 0x80)

//入力デバイスから毎期間データを受け取る関数
void InputControl(){
  HRESULT  hr;

  //キーボード読み込み
  hr = DIDeviceKeyboard->GetDeviceState(sizeof(KeyboardBuff), &KeyboardBuff);
  if(SUCCEEDED(hr)){
  } else {
    //取得エラー発生
    if(hr == DIERR_INPUTLOST ){
      ZeroMemory(&KeyboardBuff,sizeof(KeyboardBuff));
      DIDeviceKeyboard->Acquire();
    }
  }

  //マウス読み込み
  hr = DIDeviceMouse->GetDeviceState(sizeof(DIMOUSESTATE), &MouseBuff);
  if(SUCCEEDED(hr)){
  } else {
    //取得エラー発生
    if(hr == DIERR_INPUTLOST ){
      ZeroMemory(&MouseBuff,sizeof(DIMOUSESTATE));
      DIDeviceMouse->Acquire();
    }
  }
}

●キーボードとマウスからのデータを参照する
分かりやすくする為に、直接バッファ内を参照していますが、実際にはマクロか何か作りましょう。
DIK_UPなどのキーボードデバイス定数はdinput.hに定義されています。

if( MouseBuff.rgbButtons[0] & 0x80 ){
  //マウスの左ボタンが押されている
}
if( MouseBuff.rgbButtons[1] & 0x80 ){
  //マウスの右ボタンが押されている
}
if( MouseBuff.rgbButtons[2] & 0x80 ){
  //マウスのホイールボタンが押されている
}

//マウスの移動量を加算してみる
x += MouseBuff.lX / 100.f;
y += MouseBuff.lY / 100.f;
z += MouseBuff.lZ / 100.f;  //ホイールの移動量

//キーボードのボタンが押されているか?
if( GetKB( DIK_UP ) ){
  //キーボードのカーソル↑が押されている
}

●キーボードとマウスの終了処理

//入力デバイス終了処理
if( DIDeviceKeyboard != NULL ){
  DIDeviceKeyboard->Unacquire();
  DIDeviceKeyboard->Release();
}
if( DIDeviceMouse != NULL ){
  DIDeviceMouse->Unacquire();
  DIDeviceMouse->Release();
}
if( lpDINPUT != NULL ){
  lpDINPUT->Release();
}



■3D描画基礎

●簡単なポリゴン表示

スプライト表示から増える要素は、簡単に言うと「頂点がXYからXYZになる(3次元)」
「視点(カメラ)が必要」「透視変換の行列計算」の3つ。
まだ光源計算を行っていないので、マットな感じ。


//ワールド行列の設定
D3DXMatrixIdentity( &WorldMatrix );

//ビュー行列の設定 (左手座標系ビュー行列)
D3DXMatrixLookAtLH( &ViewMatrix, &pEye, &pAt, &pUp );

//プロジェクション行列の設定 (左手座標系パースペクティブ射影行列)
D3DXMatrixPerspectiveFovLH( &Proj, fovy, Aspect, zn, zf );

//視点行列設定
SetTransform( D3DTS_WORLD, &WorldMatrix );
SetTransform( D3DTS_VIEW, &ViewMatrix );
SetTransform( D3DTS_PROJECTION, &Proj );


●マテリアルとライティング

マテリアルとライティングを有効にする為には、頂点情報にNORMAL(法線ベクトル)が必要になります。
画像はD3DLIGHT_DIRECTIONALタイプの光源1つ、スペキュラ計算が有効になっている状態です。

DirectX8には3つの光源が用意されています。

D3DLIGHT_DIRECTIONAL ディレクショナルライト。平行な光源。
D3DLIGHT_POINT       ポイントライト。全方向に光を照射する光源。
D3DLIGHT_SPOT        スポットライト。照射がコーン内に制限される光源。

ライトの設定

D3DMATERIAL8  mat;
D3DLIGHT8     light;

//ライティング計算を有効にする
SetRenderState( D3DRS_LIGHTING, TRUE );

//スペキュラ計算を有効にする(必須ではない)
SetRenderState( D3DRS_SPECULARENABLE, TRUE );

〜 matとlightを設定(省略) 〜

//マテリアル設定
SetMaterial( &mat );

//1つのライトを設定し有効にする
SetLight( 0, &light );
LightEnable( 0, TRUE );




■2D描画系基礎

●とりあえず、表示してみる

タスク処理の動作確認の為、複数のタスクで大量のスプライトを表示。


●TGAファイルでアルファテクスチャ表示

今度はD3DUtil_CreateTexture()を使用して、TGAフォーマットの画像を表示。
TGAファイルはアルファ付テクスチャの時に便利。

アルファ設定は次の通り。

SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);



●加算アルファ

次は加算アルファです。
重なって行くと加算されて行くので、光っている効果等に使えます。

アルファ設定は次の通り。

SetRenderState(D3DRS_SRCBLEND,D3DBLEND_ONE);
SetRenderState(D3DRS_DESTBLEND,D3DBLEND_ONE);

もしくは、

SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
SetRenderState(D3DRS_DESTBLEND,D3DBLEND_ONE);



●頂点カラー

次は頂点カラーを有効にしてみましょう。
各4頂点に設定したRGBAを操作出来るようにすると、フェードイン・アウト処理や
1つのテクスチャソースの色を変えて、使いまわす事が出来ます。
例えばグレースケールの煙テクスチャ1つを、白ベースにすると水蒸気、黒ベースにすると
黒煙に見せるような事も可能です。
左の画像は、各頂点のRGB要素だけを変更した様子。
右の画像は、さらに上の2頂点のアルファ値を0にした様子です。
スプライトの上部になるほど透明になります。

頂点カラーのアルファを有効にしたい時は、テクスチャブレンディングの設定が必要です。

SetTextureStageState(0,D3DTSS_ALPHAOP,D3DTOP_MODULATE);
SetTextureStageState(0,D3DTSS_ALPHAARG1,D3DTA_TEXTURE);
SetTextureStageState(0,D3DTSS_ALPHAARG2,D3DTA_DIFFUSE);
SetTextureStageState(0,D3DTSS_COLOROP,D3DTOP_MODULATE);
SetTextureStageState(0,D3DTSS_COLORARG1,D3DTA_TEXTURE);
SetTextureStageState(0,D3DTSS_COLORARG2,D3DTA_DIFFUSE);



●回転

次はスプライトを回転出来るようにします。
4頂点の(x , y)座標を任意の回転角で回転させます。

sin/cosで回転させる場合は下記の式で計算出来ます。

x' = x * cosθ - y * sinθ
y' = x * sinθ + y * cosθ


MATRIXで回転させる場合は下記の式で計算出来ます。
in(x,y)に座標を入れて、Z軸だけを回転させたマトリックスで頂点を回転させています。
out(x,y)が回転後の座標になります。

D3DXMatrixIdentity( &mat );
D3DXMatrixRotationZ( &mat, θ );
D3DXVec3Transform( &out, &in, &mat );



●拡大縮小

回転の次は拡大縮小。
スケーリングは先ほどの回転後の座標(x ,y)にスケール値を掛けるだけです。

Scaleがスケール値です。
out(x,y)が回転拡縮後の座標になります。

D3DXMatrixIdentity( &mat );
D3DXMatrixRotationZ( &mat, θ );
D3DXVec3Transform( &out, &in, &mat );
out *= Scale;




■タスク処理

まずは擬似タスク処理が出来るように、タスクの登録・削除処理とタスク関数
呼び出しを作ります。ここで重要なのは、タスク固有のワークエリアがある事です。
例えば全く同じタスクAを2つ登録した場合でも、2つのタスク内で使用される
ワーク(変数やバッファ等)は、別の場所(メモリ)にするという事です。

例えば

void sample(){
 int x, y;
 スプライト表示( x, y );
}

というスプライトを表示する関数があり


entry(sample);
entry(sample);

この関数(タスク)を2回登録した場合でも、スプライト2個がそれぞれ個別の位置に
表示出来るように、変数( x ,y )は、別々の( x ,y )であるような工夫が必要です。

2002/02/09 start
流体力学研究所

戻る