200.計算ライブラリ

 この項では、BaseCrossに内蔵されている計算ライブラリについて説明します。
 計算ライブラリといいましても、ライブラリ内で計算用のコードが実装されているのではなく、ほとんどのケースでDirectXMathの関数を呼び出しているものです。
 DirectXMathというのはXNAMathというXBox用の計算ライブラリの互換のある関数群で、WindowsとDirectXという環境で高速演算できる関数群です。ここではDirectXMath関数群の特徴と基本的な使い方を述べるとともに、BaseCrossとのインターフェイス実現のためにどのような実装がされていて、DirectXMath関数群を使うためにどのような使い方をするのかを述べたいと思います。

■計算ライブラリの役割■

 BaseCrossの、行列やベクトル計算はDirectXMathを使用しています。これにより、ストリーミング SIMD 拡張命令 2 (いわゆるSSE2)による計算が実装できます。これは浮動小数点の計算が最適化されている、ということです。3Dプログラムは、複雑な浮動小数点計算が大量に必要なプログラムです。ですから、こういったハードウェア依存の計算は、できれば実装したほうが良い機能でしょう。
 BaseCrossでは、DirectXMathを利用するにあったっての手間を、ゲームプログラマに、なるべく負担を掛けないように、bsm::Vec3bsm::Mat4x4などの計算クラスがあります。こういった計算クラスがある理由は、DirectXMathの利用にあたってややこしい部分があるからです。
 まずは、DirectXMathの利用の、どこがややこしいのかを説明します、
 DirectXMathの関数群はXMMatrixAffineTransformation()のようにXMで始まる関数名を持ちます。
 ほとんど(というか、ほぼ全部)、XMVECTORもしくはXMMATRIXという型の変数を返す、あるいは引数に必要とします。これらの型はSSE2とのデータのやり取りに使用します。
 このXMVECTORXMMATRIXの2つの型が曲者です。基本的な情報 - MSDN - Microsoftというマイクロソフトのページに、その要件について記されています)。(かなり下のほうです)
 XMVECTORXMMATRIXについての内容をまとめると以下のような特徴があります。
1、これらの変数は、16 バイトのアライメント、が必要。
2、スタック上の場合は問題ない
3、データセグメントの場合は問題ない
4、ヒープ領域で、Xbox 360 および Windows x64では問題ない
5、ヒープ領域で、Windows x86 では 8 バイトアラインメントなので問題になる
 ここでのケースがまさにBaseCrossによくある環境です。ならばWindows x64にすればいいだろうと思うかもしれませんが、Windows x64の場合、size_tの大きさの違いなど、x64の環境に合わせなければいけないので、将来的にはそうすべきですが、現状、Windows x86を使用しています。
 ここでスタック、データセグメント、ヒープ領域の意味が分からない方のために説明しますと、まずスタック領域に変数が展開される例は、関数の内部変数や、呼び出しの引数です。
 例えば以下の例は、わかりやすいスタック上の変数です。
void test(){
    //単位クォータニオンの取得
    XMVECTOR q = XMQuaternionIdentity();
    //単位行列の取得
    XMMATRIX m = XMMatrixIdentity()
}
 これは、関数内のローカル変数に代入しています。この場合は問題ないということです。
 データセグメントは、グローバル変数がそうです。
XMVECTOR q;
XMMATRIX m;
void test(){
    //単位クォータニオンの取得
    q = XMQuaternionIdentity();
    //単位行列の取得
    m = XMMatrixIdentity()
}
 これも問題がありません。しかし、以下がヒープ領域です。
struct teststr{
    XMVECTOR q;
    XMMATRIX m;
};

void test(){
    //以下は実行できない
    auto t = new teststr();
    //単位クォータニオンの取得
    t->q = XMQuaternionIdentity();
    //単位行列の取得
    t->m = XMMatrixIdentity()
}
 この実行ができないのです。エラーをなくすためにはnew演算子を多重定義する必要がありますが、これは負担の重いことです。
 解決策として、上記マイクロソフトサイトではXMFLOAT3、XMFLOAT4、XMFLOAT4X3、XMFLOAT4X4を使用すべき、という記述があります。これはどういいうことかというと、実際にコード化してみます。
struct teststr{
    XMFLOAT4 q;
    XMFLOAT4X4 m;
};

void test(){
    //以下は実行できる
    auto t = new teststr();
    //
    //ここでtの変数を何らかの形で設定
    //
    //変数のXMVECTORへのロード
    XMVECTOR q = XMLoadFloat4(&t->q);
    //クォータニオンの正規化
    q = XMQuaternionNormalize(q);
    //tの変数に戻す
    XMStoreFloat4&t->q,q);
    //変数のXMMATRIXへのロード
    XMMATRIX m = XMLoadFloat4x4A(&t->m);
    //行列の転置
    m = XMMatrixTranspose(m);
    //tの変数に戻す
    XMStoreFloat4x4(&t->m,m);
}
 この処理でnewで作成したtの変数を計算することができます。
 これで、めでたしめでたし、のようですが、実際にプログラムを組んでみると常にロードトストアをしないといけないという現実にぶち当たります。これでは意味がありません。
 それで、bsm::Mat4x4、bsm::Quat、bsm::Vec3などの構造体があります。以下、bsm::Quat構造体の宣言ですが
namespace basecross {
namespace bsm {
//中略
    //--------------------------------------------------------------------------------------
    /// Quat(クオータニオン)
    //--------------------------------------------------------------------------------------
    struct Quat : public XMFLOAT4
    {
        //--------------------------------------------------------------------------------------
        /*!
        @brief  コンストラクタ
        */
        //--------------------------------------------------------------------------------------
        inline Quat();
//中略
        //--------------------------------------------------------------------------------------
        /*!
        @brief  コンストラクタ
        @param[in]  v   XMFLOAT3構造体
        */
        //--------------------------------------------------------------------------------------
        explicit inline Quat(const XMFLOAT4& v);
//中略
        //--------------------------------------------------------------------------------------
        /*!
        @brief  コンストラクタ(XMVECTORで初期化)
        @param[in]  vec 値
        */
        //--------------------------------------------------------------------------------------
        explicit inline Quat(const XMVECTOR& other);
        //--------------------------------------------------------------------------------------
        /*!
        @brief  XMVECTORへのキャスト(thisをキャストして返す)
        @return XMVECTOR型の値
        */
        //--------------------------------------------------------------------------------------
        inline operator XMVECTOR() const;
//中略
        //--------------------------------------------------------------------------------------
        /*!
        @brief  XMFLOAT4からの代入
        @param[in]  other   相手
        @return *thisの参照
        */
        //--------------------------------------------------------------------------------------
        inline Quat& operator=(const XMFLOAT4& other);
        //--------------------------------------------------------------------------------------
        /*!
        @brief  XMVECTORからの代入
        @param[in]  other   相手
        @return *thisの参照
        */
        //--------------------------------------------------------------------------------------
        inline Quat& operator=(const XMVECTOR& other);
//中略
    };
}
//end bsm
}
//end basecross
 こんな感じで宣言されています。namespaceのbsmBaSecrossMathの略です。関数名が単純なので、他クラスと混同がないようにbsmネームスペースに入ってます。ただし、サンプルはあらかじめusing句によってbsmが指定されてますので省略されています。
 このように、bsm::QuatXMFLOAT4の派生クラスとして作成したうえで
1、XMFLOAT4を引数としたコンストラクタ
2、XMVECTORを引数としたコンストラクタ
3、XMFLOAT4を引数としたコピー
4、XMVECTORを引数としたコピー
5、XMVECTORへのキャスト演算子の多重定義
 を作成し、その中でロード関数とストア関数が実装されています。
 以下、上記の実体です。
namespace basecross {
namespace bsm {
//中略
    //--------------------------------------------------------------------------------------
    /// Quatインライン関数
    //--------------------------------------------------------------------------------------
    inline Quat::Quat():
        XMFLOAT4()
    {
        identity();
    }

//中略

    inline Quat::Quat(const XMFLOAT4& v) :
        XMFLOAT4(v)
    {
    }

//中略

    inline Quat::Quat(const XMVECTOR& other) :
        XMFLOAT4() 
    {
        XMVECTOR temp = other;
        XMStoreFloat4((XMFLOAT4*)this, temp);
    }

    inline Quat::operator XMVECTOR() const {
        XMFLOAT4 temp = *this;
        XMVECTOR Vec = XMLoadFloat4(&temp);
        return Vec;
    }


    inline Quat& Quat::operator=(const XMFLOAT4& other) {
        (XMFLOAT4)*this = other;
        return *this;
    }


    inline Quat& Quat::operator=(const XMVECTOR& other) {
        XMVECTOR temp = other;
        XMStoreFloat4((XMFLOAT4*)this, temp);
        return *this;
    }
}
//end bsm
}
//end basecross
 このようにロードとストアが実装されています。ですので、
        bsm::Quat Qt;

        //Qtに対する何らかの操作...

        //正規化
        Qt = XMQuaternionNormalize(Qt);

 のように記述できます。上記Qtがヒープ上にある変数であっても問題ありません。

計算クラスの種類

 計算ライブラリは、実装はbasecross::bsmというネームスペースに実装があります。DxLibプロジェクトのBaseMath.hおよびBaseMathInc.hが実装ファイルです。
 以下のような種類があります。ちょっと気を付けたいのは、Vec2やVec3などは、Flt2やFlt3の別名として実装されているところです。
型名 用途 親クラス 備考
Vec2 2次元ベクトル XMFLOAT2 Flt2の別名
Vec3 3次元ベクトル XMFLOAT3 Flt3の別名
Vec4 4次元ベクトル XMFLOAT4 Flt4の別名
Pt2 2次元ポイント XMFLOAT2 Flt2の別名
Pt3 3次元ポイント XMFLOAT3 Flt3の別名
Col4 RGBAカラー XMFLOAT4 Flt4の別名
Plane4 平面方程式 XMFLOAT4 Flt4の別名
Quat クォータニオン XMFLOAT4 オリジナル名
Mat3x3 3x3行列 XMFLOAT3X3 オリジナル名
Mat4x4 4x4行列 XMFLOAT4X4 オリジナル名
 この表で、例えばVec3Flt3の別名で、Flt3XMFLOAT3の派生クラスとして実装されています。
 float型の変数が3つある型が、まず、Flt3型として実装されていて、その別名としてVec3およびPt3があるという形になります。つまりVec3とPt3は、名前こそ違いますが、内容は全く同じということです。便宜上、ベクトルあるいはポイントといういい方をしたほうが扱いやすい場合は多々あります。
 float型の変数が4つある型Flt4型として実装されていて、その別名としてVec4、Col4、Plane4があるとなってます。クォータニオンだけ特別で直接XMFLOAT4の派生となっています。
 Mat3x3とMat4x4は行列です。(4x3行列の機能はMat4x4で代用できると思います)。

計算クラスの実装

 このように、計算クラスはDirectXMath関数をより使いやすくするためにありますが、あらかじめラッピング実装されている関数があります。正規化、単位行列(クォータニオン)、行列転置、逆行列などです。
 以下に実装済みの関数の使用方法を紹介します。SimplSample021Player.cppにありますPlayer::GetMoveVector()関数です。
Vec3 Player::GetMoveVector() const {
    Vec3 Angle(0, 0, 0);
    auto PtrGameStage = GetStage<GameStage>();
    Vec3 CameraEye, CameraAt;
    PtrGameStage->GetCamera().GetCameraEyeAt(CameraEye, CameraAt);

    //コントローラの取得
    auto CntlVec = App::GetApp()->GetInputDevice().GetControlerVec();
    if (CntlVec[0].bConnected) {
        if (CntlVec[0].fThumbLX != 0 || CntlVec[0].fThumbLY != 0) {
            float MoveLength = 0;   //動いた時のスピード
            //進行方向の向きを計算
            Vec3 Front = m_Pos - CameraEye;
            Front.y = 0;
            Front.normalize();
            //進行方向向きからの角度を算出
            float FrontAngle = atan2(Front.z, Front.x);
            //コントローラの向き計算
            float MoveX = CntlVec[0].fThumbLX;
            float MoveZ = CntlVec[0].fThumbLY;
            Vec2 MoveVec(MoveX, MoveZ);
            float MoveSize = length(MoveVec);
            //コントローラの向きから角度を計算
            float CntlAngle = atan2(-MoveX, MoveZ);
            //トータルの角度を算出
            float TotalAngle = FrontAngle + CntlAngle;
            //角度からベクトルを作成
            Angle = Vec3(cos(TotalAngle), 0, sin(TotalAngle));
            //正規化する
            Angle.normalize();
            //移動サイズを設定。
            Angle *= MoveSize;
            //Y軸は変化させない
            Angle.y = 0;
        }
    }
    return Angle;
}
 赤くなっているところが、ライブラリでの実装を使用しているところです。例えば
        //進行方向の向きを計算
        Vec3 Front = m_Pos - CameraEye;
 はベクトル同士の引き算ですが、実体はFlt3型として実装され
    inline const Flt3 Flt3::operator -(const Flt3 & vec) const
    {
        return (Flt3)XMVectorSubtract(*this, vec);
    }
 のように実装されています。単なる引き算であっても、XMVectorSubtract()関数というDirectXMath関数を使用する形で実装されています。
 また、ちょっと気を付けたいのは
    float MoveSize = length(MoveVec);
 ここでlength()という関数は構造体の外部で実装されている
    inline float length(const Flt2 & vec)
    {
        return ((Flt2)XMVector2Length(vec)).x;
    }
 という関数が呼ばれます。normalize(正規化を返す)、length(長さを返す)、lengthSqr(長さの2乗を返す)、dot(内積を返す)、cross(外積を返す。Flt3のみ)などの外部関数があります。
 そのほか、詳しくはDxLibプロジェクトのBaseMath.hおよびBaseMathInc.hを参照してください。