文字描画関連その3

DXRubyでわりと最近追加したメソッドがRenderTarget/Imageのdraw_font_exだ。これはImage#draw_fontのようにGetGlyphOutlineWで受け取ったビットマップ情報を使って、影や縁取りを行う機能が追加されている。Imageのほうは地味だが厄介な編集処理が入っているだけなので興味がある人は見てくれればいいが、面倒なだけなのでここでは説明しない。ただ、CPUでやっているので非常に遅い。GetGlyphOutlineWが遅いうえに影と縁取りも遅い。
さらにRenderTargetのほうはもっと遅い。DirectXでの描画はポリゴンベースなため、文字を描画するにはまず何よりもテクスチャが必要で、ImageならオブジェクトがCPUから描き込めるテクスチャを持っているからいいが、RenderTargetは持っていないから、内部で生成する必要がある。そして描画したあとで破棄する。Imageのほうの処理に単純にこれが追加される。
このあたりはとりあえずGetGlyphOutlineWの結果をキャッシュすれば少しは速くなるのだろうが、それとは別に影や縁取りをした結果もキャッシュしたほうがいいのかどうか、するとしたらどこにどうロジックを入れればいいのか、みたいな困った話があって、なかなか難しい。
Imageのほうは直接オブジェクトのテクスチャに描き込んでしまうからキャッシュする情報が無い。間に1枚保存用のテクスチャをはさむと単純に遅くなるし、RenderTargetのほう専用のキャッシュ機構を作るにしても、そうなると果たしてそんなに必要なもんなのかもよくわからない。それこそImage作ってそこに描いてから描画してくれ、みたいな?とりあえずGetGlyphOutlineWのキャッシュだけ作ってどうなるかを確認してからだろうか。

今回はRenderTarget#draw_font_exの実装について。


これは実はかなり手抜きで、内部でImageオブジェクトを生成→Image#draw_font_exを発行→RenderTarget#draw_exを発行→Image#delayed_disposeを発行ということをやっているだけだ。直接呼び出してるからRubyシステム経由で呼び出すよりもちょっとだけ速い。

/*--------------------------------------------------------------------
   高品質フォント描画
 ---------------------------------------------------------------------*/
static VALUE RenderTarget_drawFontEx( int argc, VALUE *argv, VALUE obj )
{
    struct DXRubyRenderTarget *rt = DXRUBY_GET_STRUCT( RenderTarget, obj );
    int fontsize, edge_width, shadow_x, shadow_y;
    VALUE vimage, voption, vedge_width, vedge, vshadow;

    DXRUBY_CHECK_DISPOSE( rt, surface );
    DXRUBY_CHECK_TYPE( Font, argv[3] );

    if( argc < 4 || argc > 5 ) rb_raise( rb_eArgError, "wrong number of arguments (%d for %d..%d)", argc, 4, 5 );
    Check_Type(argv[2], T_STRING);

    if( argc < 5 || argv[4] == Qnil )
    {
        voption = rb_hash_new();
    }
    else
    {
        Check_Type( argv[4], T_HASH );
        voption = argv[4];
    }

    /* 内部Imageオブジェクト生成*/

    /* エッジオプション補正 */
    vedge = hash_lookup( voption, symbol_edge );
    if( vedge == Qnil || vedge == Qfalse )
    {
        edge_width = 0;
    }
    else
    {
        vedge_width = hash_lookup( voption, symbol_edge_width );
        edge_width = vedge_width == Qnil ? 2 : NUM2INT( vedge_width );
    }

    /* 影オプション補正 */
    vshadow = hash_lookup( voption, symbol_shadow );
    if( vshadow == Qnil || vshadow == Qfalse )
    {
        shadow_x = 0;
        shadow_y = 0;
    }
    else
    {
        VALUE vshadow_x, vshadow_y;

        vshadow_x = hash_lookup( voption, symbol_shadow_x );
        shadow_x = vshadow_x == Qnil ? NUM2INT( Font_getSize( argv[3] ) ) / 24 + 1 : NUM2INT(vshadow_x);

        vshadow_y = hash_lookup( voption, symbol_shadow_y );
        shadow_y = vshadow_y == Qnil ? NUM2INT( Font_getSize( argv[3] ) ) / 24 + 1 : NUM2INT(vshadow_y);
    }

    fontsize = NUM2INT( Font_getSize( argv[3] ) );
    vimage = Image_allocate( cImage );
    {
        VALUE arr[2] = {INT2FIX(NUM2INT( Font_getWidth( argv[3], argv[2] ) ) + fontsize / 2 + edge_width * 2 + shadow_x), INT2FIX(fontsize + edge_width * 2 + shadow_y)};
        Image_initialize( 2, arr, vimage );
    }

    /* Imageに描画 */
    {
        VALUE arr[5] = {INT2FIX(0), INT2FIX(0), argv[2], argv[3], voption};
        Image_drawFontEx( 5, arr, vimage );
    }

    /* RenderTarget_drawEx呼び出し */
    {
        VALUE arr[4] = {argv[0], argv[1], vimage, voption};
        RenderTarget_drawEx( 4, arr, obj );
    }

    rb_ary_push( rt->array, vimage );

    return obj;
}

ちなみにここで呼び出しているRenderTarget_drawExはRenderTargetのフルオプション対応描画で、この関数そのものはRenderTarget#draw_exで呼ばれる。中身は地味なので置いといて、これを使って描画予約されたものは以下の関数で描画される。回転・スケーリング・シェーダ描画にまで対応したものだが、通常描画と比べて少し座標計算が複雑になっていたり、Shader用の処理が追加されていたりするだけで、実際のところあまり変わらない。

void RenderTarget_drawEx_func( struct DXRubyPicture_drawEx *picture )
{
    TLVERTX VertexDataTbl[6];
    struct DXRubyImage *image;
    float angle = 3.141592653589793115997963468544185161590576171875f / 180.0f * picture->angle;
    float sina = sin(angle);
    float cosa = cos(angle);
    float data1x = picture->scalex * cosa;
    float data2x = picture->scalex * sina;
    float data1y = picture->scaley * sina;
    float data2y = picture->scaley * cosa;
    float tu1;
    float tu2;
    float tv1;
    float tv2;
    float centerx = -picture->centerx;
    float centery = -picture->centery;
    float width, height;
    float basex = picture->x - centerx;
    float basey = picture->y - centery;
    int i;
    UINT pass;

    if( TYPE(picture->value) == T_ARRAY )
    {
        image = DXRUBY_GET_STRUCT( Image, RARRAY_PTR( picture->value )[0] );
    }
    else
    {
        image = DXRUBY_GET_STRUCT( Image, picture->value );
    }

    DXRUBY_CHECK_DISPOSE( image, texture );

    width = image->width;
    height = image->height;

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

    /* 頂点1 */
    VertexDataTbl[0].x =  centerx * data1x - centery * data1y + basex - 0.5f;
    VertexDataTbl[0].y =  centerx * data2x + centery * data2y + basey - 0.5f;
    /* 頂点2 */
    VertexDataTbl[1].x = VertexDataTbl[3].x =  (centerx+width) * data1x - centery * data1y + basex - 0.5f;
    VertexDataTbl[1].y = VertexDataTbl[3].y =  (centerx+width) * data2x + centery * data2y + basey - 0.5f;
    /* 頂点3 */
    VertexDataTbl[4].x =  (centerx+width) * data1x - (centery+height) * data1y + basex - 0.5f;
    VertexDataTbl[4].y =  (centerx+width) * data2x + (centery+height) * data2y + basey - 0.5f;
    /* 頂点4 */
    VertexDataTbl[2].x = VertexDataTbl[5].x =  centerx * data1x - (centery+height) * data1y + basex - 0.5f;
    VertexDataTbl[2].y = VertexDataTbl[5].y =  centerx * data2x + (centery+height) * data2y + basey - 0.5f;
    /* 頂点色 */
    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->SetFVF(g_pD3DDevice, D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1);

    if( TYPE(picture->value) == T_ARRAY ) /* Shaderあり */
    {
        struct DXRubyShaderCore *core = DXRUBY_GET_STRUCT( ShaderCore, RARRAY_PTR( picture->value )[1] );
        DXRUBY_CHECK_DISPOSE( core, pD3DXEffect );

        rb_hash_foreach( RARRAY_PTR( picture->value )[2], Window_drawShader_func_foreach, RARRAY_PTR( picture->value )[1] );

        core->pD3DXEffect->lpVtbl->SetTexture( core->pD3DXEffect, "tex0",
                                                 (IDirect3DBaseTexture9*)image->texture->pD3DTexture );

        core->pD3DXEffect->lpVtbl->Begin( core->pD3DXEffect, &pass, 0 );
        for( i = 0; i < pass; i++ )
        {
            /* 描画 */
            core->pD3DXEffect->lpVtbl->BeginPass( core->pD3DXEffect, i );
            g_pD3DDevice->lpVtbl->DrawPrimitiveUP( g_pD3DDevice, D3DPT_TRIANGLELIST, 2, VertexDataTbl, sizeof(TLVERTX) );
            core->pD3DXEffect->lpVtbl->EndPass( core->pD3DXEffect );
        }
        core->pD3DXEffect->lpVtbl->End( core->pD3DXEffect );
    }
    else
    {
        /* テクスチャをセット */
        g_pD3DDevice->lpVtbl->SetTexture( g_pD3DDevice, 0, (IDirect3DBaseTexture9*)image->texture->pD3DTexture );

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

この中でpicture->valueがT_ARRAYになっていたりするというあたりがトリッキーな部分で、描画予約の基本構造はVALUE型(Rubyオブジェクト)を一つしか持てないから、Shader描画用の情報が入らないという問題があって。後ろに追加するとRenderTargetのマーク関数が困ったことになってしまうし。なのでShader描画のときだけはvalue変数にはImageオブジェクトじゃなく、ArrayオブジェクトにImageとShader描画用情報を突っ込んでそれを入れたわけだ。おかげで非常にややこしいことになった。まあ、Shader描画についてはまた後日。