テクニック:3Dモデルテクスチャ

charatbeatHDXでは3Dモデルの描画時に一部のテクスチャを自分で用意した画像に変更することが出来ます。ここではこれを利用して、某筐体の3Dモデルのモニター部分をゲーム画面に置き換えることで、あたかも3Dモデル内でゲームが動作しているような演出を行ってみます。

これにはレンダーテクスチャという機能を使用します。

通常は画像などを描画すると直接画面に描画されますが、まずはこの描画先をレンダーテクスチャという画面には見えない専用のメモリ領域(テクスチャ)に描画します。そして、最後に3Dモデルの一部のマテリアルをこのレンダーテクスチャに置き換えることで、結果的にその部分だけ見た目を変えることが出来ます。
概要
このサンプルでは、ネットでDLしたMMD用のモデルデータを自作ソフトMMDTestを使ってXファイルにコンバートしたものを使用しています。また、この中の一部のテクスチャを変更しています。
※元データにもXファイルが入っていますが、こちらはスケールが1/10になっていたため使用していません


まずはゲーム画面の描画先をレンダーテクスチャに変更します。これによりゲーム画面はそのレンダーテクスチャに描画されるため、画面には何も表示されなくなります。

次にモデルを画面に表示しますが、このモデルは4番目のマテリアルがモニター部分のテクスチャとなっているので、マテリアル4の描画時に先ほど作成したレンダーテクスチャに変更することで、上記のようにモニター部分だけをゲーム画面にすることが出来ます。

マテリアルごとに描画を制御するにはhdxDrawModel2()関数を使用します。この関数の引数にコールバックを指定することで、マテリアルの描画ごとにそのコールバックが呼び出されるので、その中で必要ならテクスチャを変更します。

1点重要な問題として、実はモニター部分は周りのフレームを含んだ状態でモデリングされているため、ゲーム画面をそのまま貼ってしまうと周囲がフレーム部分に隠れてしまいます。このため、実際に表示される領域に合わせてテクスチャ内の画像を縮小してあげなければなりません。


このサンプルではシステムスキンをベースとしているため、ゲーム画面は1280x720となっています。そして上記のフレームを含めた大きさが1280x720だったとすると、実際にゲーム画面が表示される領域はおよそ1000x500(赤い面)となるので、最終的にゲーム画面をこのサイズに縮小したレンダーテクスチャを作成します。

charatbeatHDXの内部ではレンダーテクスチャが2枚存在しており、hdxBeginRenderTexture()hdxEndRenderTexture()を呼び出すごとに切り替わるようになっています。また、現在対象になっていないレンダーテクスチャは以前描画した内容が残っている状態となります。

ここで再びhdxBeginRenderTexture()を呼び出すと、今度はもう1枚のレンダーテクスチャが描画先となりますが、ここでhdxDrawRenderTexture2()を呼び出すことで1つ前に描画したレンダーテクスチャから現在のレンダーテクスチャに対して、任意の領域を指定の位置とサイズで描画することが出来ます。
※レンダーテクスチャを開始していない場合は実際の画面に描画されます

これを利用することで、まずはゲーム画面を1280x720で最初のレンダーテクスチャに描画し、次のレンダーテクスチャに以前のレンダーテクスチャの内容を縮小描画することで、最終的に3Dモデルで使用するテクスチャが作成できます。
※最初から1000x500で全てを描画出来ればこのような処理は必要ありませんが、ここでは既存の描画処理を変更したくないためこのようにしています

最初のレンダーテクスチャ 次のレンダーテクスチャ
※上記の白い部分は見えない部分なので特に何も描画していません
スクリプト
ゲーム画面の描画はシステムスキンの処理をそのまま利用するため、実装はさほど難しくはありません。

また、ここではゲーム画面のみを変更するため、変更するスクリプトは「_06_game.lua」となります。
※script.luaも改変されていますが、ゲーム画面以外の必要の無いスクリプトのincludeを削除しているだけです

このサンプルではカメラを移動出来るようにカメラ用の変数が定義されています。この変数はグローバル変数として以下のように定義しています。
// カメラ制御
g_iCamState         = 0;                // ステート(0=待機、1=バック、2=回転)
g_iCamMoveCount     = 0;                // カメラ移動カウンタ
g_fModelRotCount    = 0;                // モデル回転カウンタ

また、ゲーム画面に入るごとにこれらの変数を初期化するため、OnStartMainGame()にて同じく初期化します。
function OnStartMainGame()
   :
    g_iCamState         = 0;
    g_iCamMoveCount     = 0;
    g_fModelRotCount    = 0;
※localが付いていないためわざわざグローバル変数として定義する必要はありませんが、ここではどの変数を使っているのかを見た目で分かるようにしたいため敢えてグローバル領域にも定義しています

さらに3Dモデルのロードと、使用するレンダーテクスチャのサイズをセットして初期化を終わります。
    // モデルロード
    hdxLoadModel( 0,"model.x",-1 );
    
    // レンダーテクスチャのサイズ変更
    hdxSetRenderTextreSize( 1280,720 );
    
end


次にゲーム画面の描画処理部分を変更します。

まずはゲーム画面の描画を全てレンダーテクスチャに対して描画するように設定します。
function OnRunGame( upd_frm )

    ///////////////////////////////////
    // 処理
    ///////////////////////////////////
   :

    ///////////////////////////////////
    // 描画
    ///////////////////////////////////

    // 以降の描画は全てレンダーテクスチャに描く
    hdxBeginRenderTexture();

    // BGAの表示
    hdxSetFilterMode( FILTER_LINEAR );                  // バイリニア

   :

    // フェードアウト
    if( g_dwMainState==MNST_FINISH ) then
        hdxDrawAnime( 2, 640, 360, 1.0, 1.0 );
    end
    
    // 描画終了
    hdxEndRenderTexture();
描画処理をhdxBeginRenderTexture()hdxEndRenderTexture()で囲むことで、本来画面に描画されるものが全てレンダーテクスチャに描画されます。また、初期化時にレンダーテクスチャを1280x720に設定しているので、レンダーテクスチャいっぱいにゲーム画面が描画されたことになります。

次にもう1枚のレンダーテクスチャに対して、上記で描画した画像を1000x500に縮小して描画します。
    // 描画サイズをモデル内の画面のUVに合わせるため、
    // 先ほど描画したテクスチャをもう1枚のレンダーテクスチャに縮小して描画する    
    hdxBeginRenderTexture()
    hdxSetFilterMode( FILTER_LINEAR );
    hdxDrawRenderTexture2( 139,105,1000,500, 0,0,1280,720, 1 );
    hdxSetFilterMode( FILTER_POINT );
    hdxEndRenderTexture();
再びhdxBeginRenderTexture()を呼び出すことで、もう1枚のレンダーテクスチャを描画先に設定し、ここでhdxDrawRenderTexture2()を使って縮小表示しています。描画が終わったらhdxEndRenderTexture()を呼び出してレンダーテクスチャを終わらせています。


最後に3Dモデルを描画しますが、このサンプルでは上下左右キーでカメラを移動させることが出来るようになっています。

まずモデルを表示するための初期設定を行います。
    // モデル表示
    hdxSetBlendMode( BLEND_NORMAL );
    hdxSetZBuffer( TRUE );
    hdxClearZBuffer();
    hdxSetLightDirection( 0,1,1, 0,0,0 );
    hdxSetLightAmbient( 0.5,0.5,0.5 );
//  hdxSetLightSpecular( 4.0,0.2,0.2 );
    hdxSetLight( TRUE );
    hdxSetViewport( 0,0,1280,720 );
3Dモデルは陰面消去が必要となるため、始めにhdxSetZBuffer()を使ってZバッファを有効にし、次にhdxClearZBuffer()を使ってZバッファをクリアしています。

本来はZバッファへの描画をしていなければ特にクリアする必要はありませんが、2Dアニメでマスクを使用している場合は内部でZバッファが使用されるため、ここでは明示的にZバッファのクリアを行っています。

あとは平行光源を使用するためライトの位置や向き、環境光などをセットしたり、全画面への描画を明示的に指定するためhdxSetViewport()を呼び出しています。

次にカメラの制御を行うため、グローバル変数として定義したカメラ用変数を使ってキーの入力を制御します。
    // 60fps制御
    if( upd_frm==TRUE ) then
        if( g_iCamState==0 ) then
            // ロードが終わるのを待つ
            
        elseif( g_iCamState==1 ) then
            // カメラ移動中
            g_fModelRotCount = g_fModelRotCount + 0.5;      // 0.5を60回足して30度まで回転
            g_iCamMoveCount = g_iCamMoveCount + 1;
            if( g_iCamMoveCount>=60 ) then
                g_iCamState = 2;
            end

        elseif( g_iCamState==2 ) then
            // 上下キーでカメラの距離を変更
            local key = hdxSysGetInput();                   // キーの押下状態を取得
            if( key["is_press_up"]==1 ) then
                g_iCamMoveCount = g_iCamMoveCount - 1;
                if( g_iCamMoveCount<0 ) then
                    g_iCamMoveCount = 0;
                end
            elseif( key["is_press_down"]==1 ) then
                g_iCamMoveCount = g_iCamMoveCount + 1;
                if( g_iCamMoveCount>300 ) then
                    g_iCamMoveCount = 300;
                end
            end

            // 左右キーでモデル回転
            if( key["is_press_left"]==1 ) then
                g_fModelRotCount = g_fModelRotCount + 1;
                if( g_fModelRotCount>=360 ) then
                    g_fModelRotCount = 0;
                end
            elseif( key["is_press_right"]==1 ) then
                g_fModelRotCount = g_fModelRotCount - 1;
                if( g_fModelRotCount<=0 ) then
                    g_fModelRotCount = 360;
                end
            end
        end
    end

このサンプルでは、ゲームが開始した直後ゲームがスタートした状態ユーザーがキーで操作可能な状態を把握するため、g_iCamStateの値を見て処理を変えています。ちなみにカメラが移動するのはゲームが開始した直後からのため、ゲームが開始されたらg_iCamStateを1にする制御が計算処理部分に追加されています。
    ///////////////////////////////////
    // 処理
    ///////////////////////////////////
      :
        if( hdxGetTime()>=g_dwWaitTitleTime ) then
            // 最低3秒間表示し終わっていたらゲーム開始
            hdxStartGame();
            g_iCamState = 1;                // カメラ移動開始
            g_dwMainState = MNST_GAME;
        end

カメラの移動は回転と移動カウントを変化させているだけで、このあたりは適当な処理になっています。

上下左右キーの入力状態はhdxSysGetInput()から取得出来ます。この関数は本来はキーの押下状態を表示するために使用するものですが、今回の用途ではゲームの進行には影響しないため、裏技としてこれをキー入力処理に使用しています。あとは押されたキーによりカメラの距離を変化させたり、モデルの回転角度を変化させています。

なおVSYNCを切っている場合、フレームレートが60以上になるとOnRunGame()もそれに合わせて呼ばれてしまいキーの処理も早くなってしまうため、カメラの移動やモデルの回転が高速に動いてしまいます。これを防ぐため60fps間隔で1回だけTRUEとなる引数upd_frmを見て、TRUEの時だけキー入力処理を行うようにしています。


次にカメラの位置から実際にモデルを画面に表示する処理を行います。
    hdxMatrixTranslation( 0, 0, 0, 4.5 );
    hdxMatrixRotationY( 1,hdxToRadian(g_fModelRotCount) );
    hdxMatrixMultiply2( 0,1 );
    hdxSetLookAt( 0, 19.02+g_iCamMoveCount/10, g_iCamMoveCount/4+6.4,
                  0, 19.02-g_iCamMoveCount/10, 0,
                  0, 1, 0 );

    hdxSetFilterMode( FILTER_LINEAR );
    hdxDrawModel2( 0,0,"_DrawModelCallback" );
    hdxSetFilterMode( FILTER_POINT );
    
    hdxSetLight( FALSE );
    hdxSetZBuffer( FALSE );

ここではモデルのモニター部分が原点からZ方向に-4.5の位置にあるため、これを原点に戻すためにhdxMatrixTranslation()にてZ方向を+4.5移動するための行列をマトリクスバッファ0にセットしています。


次にモデルをY軸方向(縦)に対して回転するため、Y軸の回転行列をhdxMatrixRotationY()にてマトリクスバッファ1にセットしています。

そして移動してから回転をするためhdxMatrixMultiply2()を使って行列を掛け算し、結果を再びマトリクスバッファ0にセットしています。
※ここではmat[0] = mat[0] * mat[1]の計算を行っています

これで3Dモデルをワールド原点に置いた場合のモニター部分の中心が(0.0, 19.02, 0.0)となるので、次にこの場所を見るようにhdxSetLookAt()を使ってカメラを設定します。

全ての設定が終わったら実際にモデルを表示するためhdxDrawModel2()を呼び出します。この時コールバックとして_DrawModelCallback()関数を指定しており、これでhdxDrawModel2()内でマテリアルごとに_DrawModelCallback()が呼び出されるようになります。

コールバックは以下のように定義しています。
function _DrawModelCallback( id )

    // モニター内ならレンダーテクスチャを使用する
    if( id==4 ) then
        hdxSetTexture( 1 );
        return TRUE;
    end

    return FALSE;
end
この3Dモデルのモニター部分のマテリアルは4なので、4の時だけhdxSetTexture()にてレンダーテクスチャを指定してTRUEを返し、それ以外はFALSEを返すことでモデル自体のマテリアルで描画するように制御しています。

最後に全ての描画が終わったら念のためライトとZバッファをOFFにして終了です。
ダウンロード
ここでは上記のサンプルスキンと元となったモデルをダウンロードすることが出来ます。