RenderTargetのDirectX的描画処理

DXRubyはいろいろな機能を詰め込んではあるが、基本は描画まわりである。音に関しては別のライブラリを使うという選択肢があるし、入力はWin32APIを使ってどーにかすればなんとかなるんじゃないの、と言ったところで、でもDirectXによる描画だけはどうにもならない。何かしら直接叩く手があるにしても、DirectXAPIRubyから叩いていては処理時間が大変なことになる。いかに最低限のAPIを作り、C側の処理をまとめて行うか、Ruby側で必要な手続きを減らすか、という部分がキーだ。
Window/RenderTargetのdraw系メソッドは最も実行されるであろうメソッドなので、その処理は前回の記事のようにあの手この手でとことん削り倒してある。RubyAPIは呼ばず、標準関数なども呼ばず、必要なデータのみ保存してすぐ終わる。もちろんその部分を減らしたところでそれを使って描画する部分がそれ以上に遅くなってしまっては元も子も無い。そっちのほうまで含めてトータルで速くなるように考えた結果だ。
描画予約情報のメモリ管理やソート処理、RenderTargetの生成/GCなど関連するネタは多いが後まわしにして、とりあえずDXRubyの中核である描画処理を見てみることにしよう。


描画処理であるRenderTarget#updateは以下のようになっている。ちょっと長いがこれを貼り付けて困る人などいないと思うので問題ない。やってることは単純だ。

/*--------------------------------------------------------------------
   (内部関数)画面更新
 ---------------------------------------------------------------------*/
static VALUE RenderTarget_update( VALUE self )
{
    HRESULT hr;
    int x_2d, width_2d;
    int y_2d, height_2d;
    struct DXRubyRenderTarget *rt = DXRUBY_GET_STRUCT( RenderTarget, self ); /* 出力先 */
    int i;

    DXRUBY_CHECK_DISPOSE( rt, surface );

    /* シーンのクリア */
    {
        D3DVIEWPORT9 vp;
        vp.X       = x_2d = 0;
        vp.Y       = y_2d = 0;
        if( rt->texture == NULL )
        {
            vp.Width   = width_2d = g_D3DPP.BackBufferWidth;
            vp.Height  = height_2d = g_D3DPP.BackBufferHeight;
        }
        else
        {
            vp.Width   = width_2d = rt->texture->width;
            vp.Height  = height_2d = rt->texture->height;
        }
        vp.MinZ    = 0.0f;
        vp.MaxZ    = 1.0f;
        g_pD3DDevice->lpVtbl->SetRenderTarget( g_pD3DDevice, 0, rt->surface );
        g_pD3DDevice->lpVtbl->SetViewport( g_pD3DDevice, &vp );
        g_pD3DDevice->lpVtbl->Clear( g_pD3DDevice, 0, NULL, D3DCLEAR_TARGET,
                                     D3DCOLOR_ARGB( rt->a, rt->r, rt->g, rt->b ), 1.0f, 0 );
    }

    /* シーンの描画開始 */
    if( SUCCEEDED( g_pD3DDevice->lpVtbl->BeginScene( g_pD3DDevice ) ) )
    {
        i = 0;

        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_ZENABLE,D3DZB_FALSE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_ZWRITEENABLE, FALSE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_LIGHTING, FALSE);
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_FOGENABLE, FALSE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SHADEMODE, D3DSHADE_GOURAUD );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRGBWRITEENABLE, FALSE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_VERTEXBLEND, FALSE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_WRAP0, 0 );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_CULLMODE, D3DCULL_NONE);

        g_pD3DDevice->lpVtbl->SetTextureStageState( g_pD3DDevice, 0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE );
        g_pD3DDevice->lpVtbl->SetTextureStageState( g_pD3DDevice, 0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE );
        g_pD3DDevice->lpVtbl->SetTextureStageState( g_pD3DDevice, 0, D3DTSS_ALPHAOP, D3DTOP_MODULATE );
        g_pD3DDevice->lpVtbl->SetTextureStageState( g_pD3DDevice, 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
        g_pD3DDevice->lpVtbl->SetTextureStageState( g_pD3DDevice, 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
        g_pD3DDevice->lpVtbl->SetTextureStageState( g_pD3DDevice, 0, D3DTSS_COLOROP, D3DTOP_MODULATE );

        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_ALPHABLENDENABLE, TRUE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLEND, D3DBLEND_SRCALPHA );

        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SEPARATEALPHABLENDENABLE, TRUE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLENDALPHA, D3DBLEND_ONE );
        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA );

        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_FOGENABLE, FALSE );

        g_pD3DDevice->lpVtbl->SetSamplerState( g_pD3DDevice, 0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP );
        g_pD3DDevice->lpVtbl->SetSamplerState( g_pD3DDevice, 0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP );

        /* 拡大縮小フィルタ設定 */
        g_pD3DDevice->lpVtbl->SetSamplerState(g_pD3DDevice, 0, D3DSAMP_MINFILTER,
                                         rt->minfilter);
        g_pD3DDevice->lpVtbl->SetSamplerState(g_pD3DDevice, 0, D3DSAMP_MAGFILTER,
                                         rt->magfilter);

        if( rt->PictureCount > 0 )
        {
            D3DMATRIX matrix, matrix_t;
            int oldflag = 0;

            RenderTarget_SortPictureList( rt );

            /* 2D描画 */
            D3DXMatrixScaling    ( &matrix, 1, -1, 1 );
            D3DXMatrixTranslation( &matrix_t, (float)-(width_2d)/2.0f, (float)(height_2d)/2.0f, 0 );
            D3DXMatrixMultiply( &matrix, &matrix, &matrix_t );
            g_pD3DDevice->lpVtbl->SetTransform( g_pD3DDevice, D3DTS_VIEW, &matrix );
            matrix._11 = 2.0f / width_2d;
            matrix._12 = matrix._13 = matrix._14 = 0;
            matrix._22 = 2.0f / height_2d;
            matrix._21 = matrix._23 = matrix._24 = 0;
            matrix._31 = matrix._32 = 0;matrix._33 = 0; matrix._34 = 0;
            matrix._41 = matrix._42 = 0;matrix._43 = 1; matrix._44 = 1;
            g_pD3DDevice->lpVtbl->SetTransform( g_pD3DDevice, D3DTS_PROJECTION, &matrix );

            for( i = 0; i < rt->PictureCount; i++ )
            {
                struct DXRubyPicture_draw *temp = (struct DXRubyPicture_draw *)rt->PictureList[i].picture;

                if( temp->blendflag != oldflag ) 
                {
                    switch( temp->blendflag )
                    {
                    case 0:          /* 半透明合成 */
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLEND, D3DBLEND_SRCALPHA );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLENDALPHA, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA );
                        break;
                    case 1:          /* 単純上書き */
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLEND, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLEND, D3DBLEND_ZERO );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLENDALPHA, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLENDALPHA, D3DBLEND_ZERO );
                        break;
                    case 4:          /* 加算合成1の設定 */
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLEND, D3DBLEND_SRCALPHA );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLEND, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLENDALPHA, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA );
                        break;
                    case 5:          /* 加算合成2の設定 */
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLEND, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLENDALPHA, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA );
                        break;
                    case 6:          /* 減算合成1の設定 */
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLEND, D3DBLEND_ZERO );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLENDALPHA, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA );
                        break;
                    case 7:          /* 減算合成2の設定 */
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLEND, D3DBLEND_ZERO );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLEND, D3DBLEND_INVSRCCOLOR );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_SRCBLENDALPHA, D3DBLEND_ONE );
                        g_pD3DDevice->lpVtbl->SetRenderState( g_pD3DDevice, D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA );
                        break;
                    }
                }

                oldflag = temp->blendflag;
                temp->func( temp );
            }
        }

        /* シーンの描画終了 */
        g_pD3DDevice->lpVtbl->EndScene( g_pD3DDevice );
    }

    rt->PictureCount = 0;
    rt->PictureSize = 0;
    rt->PictureDecideCount = 0;
    rt->PictureDecideSize = 0;

    for( i = 0; i < RARRAY_LEN(rt->array); i++ )
    {
        Image_dispose( RARRAY_PTR(rt->array)[i] );
    }
    rb_ary_clear( rt->array );

    return self;
}

この関数は大きく分けると上から順に、
(a) クリア処理
(b) 描画ステートの設定
(c) 2D用ビュー変換/透視座標変換行列の設定
(d) 描画ループ
(e) 終了処理
という感じになる。このうち、(b)と(c)および(d)のほとんどについてはD3DXのD3DXSpriteを使うと必要なくなるのだが、ラクなぶん遅いので全部自前でやるようにずーっと昔に書き直した。順番に軽く説明しよう。

(a)は画面の場合はバックバッファ、RenderTargetオブジェクトの場合はテクスチャを指定色でクリアする。ここを見るとわかるがWindowモジュールが持つスクリーン用バッファへの描画の場合でもこの関数が呼ばれる。画面の場合は前後に少し処理が追加される。デバイスロスト対策とか。
(b)から(d)までの処理はBeginSceneとEndSceneで囲まれていて、これがDirectXの描画処理を表す。(b)のステート変更はこの範囲内でのみ効果があるから、変更しっぱなしで戻さなくても他のRenderTargetの描画には影響しない。
これらのステートはDirectXの描画処理の各種機能設定となる。光源処理をするとか、フォグを計算するとか、色の合成方法とか、Zバッファを使うとか、その他もろもろ、そういったDirectXの描画そのものをこれで設定する。DXRubyは2D用だからほとんどの機能はオフにしているが、微妙に調整をしている部分もある。
(c)の部分は前にブログにちょこちょこ書いていた変換行列で、3D用の変換をしないからシンプルになっているが、2Dと3Dの座標系では上下が逆転するからそのへんだけ細工してある。描画時にDirectXに指定する座標をスクリーン座標にして、DirectXの変換システムを通した結果、描画して欲しい場所に描画できるような計算式だ。DirectXで2D描画をしようという人には参考になるかもしれない。
(d)のループは描画予約を片っ端から描画する部分となる。(c)の上のところでRenderTarget_SortPictureListを呼んであらかじめソートしてある。あとは一つずつ取り出して、合成方法にあわせてステートを変更して、描画関数を呼び出すだけというシンプル仕様。描画関数のほうは次で。
(e)は描画予約をクリアして、不要になったImageオブジェクトのテクスチャを捨てて、Image配列をクリアする。
不要になったImageオブジェクトとは、Image#delayed_disposeを呼ばれたものと、draw_font_exを使って描画する際に内部的に生成されたImageオブジェクトである。これらはrt->arrayというRuby配列内にpushされているから、それを順にdisposeして、rb_ary_clearで空っぽにしているのだ。
以上が描画処理なのだが、一通り説明してわかるように、DirectXのマニュアルを見ながら必要な処理を順番に並べただけである。この部分については特に高度なことは何も無い。とはいえ始めからこのような形になっていたわけではない。いまの状態を見るとシンプルにできているが、このようにするまでに結構な労力と時間がかかっているのだ。なんせDirectX9のプログラムなんてDXRubyが初めてだったし。

では次、上記関数から呼ばれる描画処理本体。前回の記事で見た通常描画のところだけ抜き出そう。

void RenderTarget_draw_func( struct DXRubyPicture_draw *picture )
{
    TLVERTX VertexDataTbl[6];
    struct DXRubyImage *image = DXRUBY_GET_STRUCT( Image, picture->value );
    float basex = picture->x - 0.5f;
    float basey = picture->y - 0.5f;
    float width = image->width;
    float height = image->height;
    float tu1;
    float tu2;
    float tv1;
    float tv2;

    DXRUBY_CHECK_DISPOSE( image, texture );
    tu1 = image->x / image->texture->width;
    tu2 = (image->x + width) / image->texture->width;
    tv1 = image->y / image->texture->height;
    tv2 = (image->y + height) / image->texture->height;

    /* 頂点1 */
    VertexDataTbl[0].x = basex;
    VertexDataTbl[0].y = basey;
    /* 頂点2 */
    VertexDataTbl[1].x = VertexDataTbl[3].x = basex + width;
    VertexDataTbl[1].y = VertexDataTbl[3].y = basey;
    /* 頂点3 */
    VertexDataTbl[4].x = basex + width;
    VertexDataTbl[4].y = basey + height;
    /* 頂点4 */
    VertexDataTbl[2].x = VertexDataTbl[5].x = basex;
    VertexDataTbl[2].y = VertexDataTbl[5].y = basey + height;
    /* 頂点色 */
    VertexDataTbl[0].color = VertexDataTbl[1].color =
    VertexDataTbl[2].color = VertexDataTbl[3].color =
    VertexDataTbl[4].color = VertexDataTbl[5].color = D3DCOLOR_ARGB(picture->alpha,255,255,255);
    /* Z座標 */
    VertexDataTbl[0].z  = VertexDataTbl[1].z =
    VertexDataTbl[2].z  = VertexDataTbl[3].z =
    VertexDataTbl[4].z  = VertexDataTbl[5].z = picture->z;
    /* テクスチャ座標 */
    VertexDataTbl[0].tu = VertexDataTbl[5].tu = VertexDataTbl[2].tu = tu1;
    VertexDataTbl[0].tv = VertexDataTbl[1].tv = VertexDataTbl[3].tv = tv1;
    VertexDataTbl[1].tu = VertexDataTbl[3].tu = VertexDataTbl[4].tu = tu2;
    VertexDataTbl[4].tv = VertexDataTbl[5].tv = VertexDataTbl[2].tv = tv2;

    /* テクスチャをセット */
    g_pD3DDevice->lpVtbl->SetTexture(g_pD3DDevice, 0, (IDirect3DBaseTexture9*)image->texture->pD3DTexture);

    /* デバイスに使用する頂点フォーマットをセット */
    g_pD3DDevice->lpVtbl->SetFVF(g_pD3DDevice, D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1);

    /* 描画 */
    g_pD3DDevice->lpVtbl->DrawPrimitiveUP(g_pD3DDevice, D3DPT_TRIANGLELIST, 2, VertexDataTbl, sizeof(TLVERTX));
}

DirectXのDrawPrimitiveUPを使って描画するための、頂点情報を設定しているのがほとんどとなる。三角形2つ(頂点6つ)で矩形を表現して、テクスチャをセット、頂点フォーマットを指定して描画だ。ここでDXRubyPicture_draw構造体のデータと、そのvalueに入っているImageオブジェクト、更にその中のテクスチャ情報を取り出して使う。テクスチャ座標計算はテクスチャの部分参照に対応している。
これも出来上がりだけを見れば見たまんまなのだが、例えばなんでDrawPrimitiveじゃなくてDrawPrimitiveUPなのか、わざわざ頂点6つを編集してD3DPT_TRIANGLELISTで描画しているのか、というあたりにポイントがある。試行錯誤した結果、これが一番速かったからなのだ。

しかしまあ、このへんはRubyはあまり関係なくて、DirectXでの2D描画はこうする、みたいな話だ。DXRubyがDirectXでの描画を基本にするライブラリなのだから、こういうのが中核になっているのは当たり前といえば当たり前である。

あ、そうそう、ここで頂点色にα値を指定しているところも仕掛け的にはミソで、これを使って半透明描画を実現しているのだな。問題はDirectXのいわゆる固定機能パイプラインを使うときにのみこれが反映されるところで、プログラマブルシェーダを使った描画の場合にはシェーダ側をこの頂点色を反映させるように作らないと半透明効果が得られない。draw_exで:shaderと:alphaを同時に指定すると:alphaが無視されるのはこのやり方が原因なのだが、実際のところシェーダの出力結果に無理矢理α値を適用する方法はよくわからない。仕様だ。
あと、一番上のところで描画座標を-0.5しているのは2D描画用のポイントで、これが無いと画面のピクセルとテクスチャのテクセルがズレて画像がボヤける。このへんはMicrosoftDirectXヘルプにも丁寧に説明がされていて、おおざっぱな説明はずっと前にブログにも書いたような気がする。