【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

pc01_02 for C#

n7shi2009-10-11


id:m107:20090924で公開されているmikotoローダ(pc01_02)をC#に移植しました。ある程度C#風にリファクタリングしています。SDLOpenGLは既存のラッパーライブラリを使わずに必要なものだけP/Invokeしています。Windows XP/Vista/7(32/64)用のソースとバイナリを置いておきます。

C++からC#への移植は手動で、コピペしながらエラーが出ないように書き換えました。時間は掛かりますがそれほど難しくありません。しかし一発で動作しなかったため問題点の特定が大変でした。原因は3箇所の小さなミスで、アルゴリズムが壊れた等の構造に関わるバグではありませんでした。

リファクタリング

id:n7shi:20091010の方法でSDL_image.dllへの依存をなくしています。

プロパティへの配列アクセスで代用できるメソッドは削除しました。使用例は以下の通りです。

// 変更前
pO.SetFaceVertex(i, 0, pv[n0]);

// 変更後
pO.Faces[i].Vertices[0] = pv[n0];

リファクタリングと言ってもこのような小規模な変更だけです。全体的な構造が変化するような大規模な変更は行っていません。

問題点の特定

一気にC#に移植したため、動作しなかったときにどこが悪いのか見当が付きませんでした。自分で作ったプログラムではないため何が正しい動作なのかの判断は困難で、動作を追いかけるのは諦めました。そのためクラスを1つずつC++からC#に置き換えながら、どのクラスに問題があるのか調べました。その際にC#のクラスはC++/CLIから呼び出しています。参考までに作業中のアーカイブを置いておきます。

これはベクトル関係の構造体だけをC#で置き換えた段階のものです。正常動作しているため、この範囲内のC#移植は成功していると判断できます。このような方法で3箇所の問題点を特定して、無事に完全動作するようになりました。

修正した問題点は以下の通りです。

glGetFloatv()

MKMatrixを置き換えたところ、ポリゴンが崩れるという問題が発生しました。

MKMatrixは代入を直感的に記述できるようにするため、C#への移植時に構造体にしています。構造体ではフィールドの初期化ができないため、値を1つ1つfloatとして定義するように変更しました。

// 移植前 (C++)
class MKMatrix
{
protected:
    float p[16];
};

// 移植後 (C#)
struct MKMatrix
{
    public float p00, p01, p02, p03, p04, p05, p06, p07;
    public float p08, p09, p10, p11, p12, p13, p14, p15;

    public float[] ToArray()
    {
        return new[]
        {
            p00, p01, p02, p03, p04, p05, p06, p07,
            p08, p09, p10, p11, p12, p13, p14, p15
        };
    }
}

glGetFloatv()にはToArray()で作成した配列を渡していたのですが、取得系の関数に一時配列を渡して値を捨ててしまっては意味がありません。そのためglGetFloatv()のP/Invokeオーバーロードして対応しました。

[DllImport("OpenGL32.dll")]
public static extern void glGetFloatv(GLenum pname, float[] @params);

[DllImport("OpenGL32.dll")]
public static extern void glGetFloatv(GLenum pname, out MKMatrix @params);

out MKMatrixによって(float*)thisに相当するキャストが実現します。

glClearDepth()

game.cppを置き換えたところ、ポリゴンが表示されたりされなかったりして、動作が不安定になりました。C++/CLI上でOpenGLの関数呼び出しをP/Invoke経由にしながら1つずつ確認した結果、glClearDepth()が怪しいということが分かりました。

gl.hとP/Invokeを見比べると、引数depthはdoubleが正しいのに、P/Invokeでは誤ってfloatにしていました。doubleであるべき引数にfloatを渡したため、引数がスタックをはみ出して参照され値が不定となっていました。

正しいP/Invokeは以下の通りです。

[DllImport("OpenGL32.dll")]
public static extern void glClearDepth(double depth);

x86では32bit以下の引数はパディングされて32bitで渡されるため、intにshortを渡してもこのような問題は発生しません。doubleは64bitです。

acosf()

MLMathを置き換えたところ、スカートが表示されなくなりました。ちなみにスカートの下はモデリングされていないため、何も表示されなかったので念のため。;-)

MLMathに含まれる関数を1つずつ置き換えた結果、GetRadBetweenVecs()が怪しいということが分かりました。その中で使われているacosf()をMath.Acos()に置き換えたのですが、Math.Acos()の引数がdoubleのためfloatでは丸められている誤差が表面化して、acosf(1.0f)=0.0fとなるべき処理がMath.Acos(1.00000001)=NaNとなっていました。ゼロと非数では超えられない壁があり過ぎです。

引数をキャストして対処しました。

// 移植前 (C++)
float MLMath::GetRadBetweenVecs( const MQVector3& from, const MQVector3& to )
{
    return acosf( DotProduct( from, to ) / ( from.Length() * to.Length() ) );
}

// 移植後 (C#)
public static float GetRadBetweenVecs(this MQVector3 from, MQVector3 to)
{
    return (float)Math.Acos((float)(
        DotProduct(from, to) / (from.Length() * to.Length())));
}

引数自体がfloatで算出されているため、有効範囲としてfloatで丸めるのは正しい処理です。

ちなみにC#で最初の引数にthisを付けているのは拡張メソッドと呼びます。以下のようにメンバに見せかけることが出来ます。

// 拡張メソッド不使用
float f = MLMath.GetRadBetweenVecs(from, to);

// 拡張メソッド使用
float f = from.GetRadBetweenVecs(to);

定義上、構造体とメソッドを分離するのは、ちょっとだけアスペクト指向かもしれません。

文字列処理

C#への移植とは別に、C++についてです。

char配列を頑張って処理している部分はstd::stringで簡略化できます。以下に例を示します。

    // 変更前
    char cResPath[512];
    char path[512];
#ifdef __OpenBSD__
    strlcpy( path, cResPath, 512 );
    strlcat( path, "/images/bg.png", 512 );
#elif defined(_MSC_VER)
    strcpy_s( path, cResPath );
    strcat_s( path, "/images/bg.png" );
#else
    strcpy( path, cResPath );
    strcat( path, "/images/bg.png" );
#endif
    pMatBG->LoadTexture( path );

    // 変更後
    std::string cResPath;
    std::string path = cResPath + "/images/bg.png";
    pMatBG->LoadTexture( path.c_str() );
    // 変更前
    char cs[32];
    char ct[32];
    memset( cs, '\0', 32 );
    memset( ct, '\0', 32 );
    char* cp = cs;
#ifdef __OpenBSD__
    strncpy( cs, str.c_str(), 32 );
#elif defined(_MSC_VER)
    strncpy_s( cs, str.c_str(), 32 );
#else
    strncpy( cs, str.c_str(), 32 );
#endif
    cp += 7;
#ifdef __OpenBSD__
    strncpy( ct, cp, strlen(cs)-6 );
#elif defined(_MSC_VER)
    strncpy_s( ct, cp, strlen(cs)-6 );
#else
    strncpy( ct, cp, strlen(cs)-6 );
#endif
    m_pAnchor[idxObj].SetName( ct );

    // 変更後
    m_pAnchor[idxObj].SetName( str.substr(7).c_str() );

C#への移植もstd::stringに近い書き方になっています。

最後に

今回の移植は複雑骨折レベルで全治一週間ほどでした。もっと規模の大きい移植を計画中ですが、何らかのトランスレータを導入しないと全治三ヶ月くらいになってしまいそうです。;-)