Skip to content

Instantly share code, notes, and snippets.

@kumar8600
Last active September 9, 2023 10:36
Show Gist options
  • Save kumar8600/7615693 to your computer and use it in GitHub Desktop.
Save kumar8600/7615693 to your computer and use it in GitHub Desktop.
チュートリアル 38 - Assimpでスケルタルアニメーション (OpenGL Step by Stepのチュートリアルの訳です。お気づきの点あれば勝手にフォークして直してください。ちなみに、翻訳はそのサイトのQ&Aで許可されています。)

チュートリアル 38 - Assimpでスケルタルアニメーション

原文 : Tutorial 38

OpenGL Step by Step - OpenGL Development on Linux より

ソースコード

背景

ついにここまで来た。このチュートリアルは100万人の読者(誇張表現)が知りたがっているアレをやる。スケルタルアニメーション、別名スキニングをAssimpライブラリでやる方法についてだ。

スケルタルアニメーションの処理は、2つのパートに分けられる。1つ目はアーティストによって実行されるもので、2つ目は君、すなわちプログラマー(というか君が書いたエンジン)が実行するものだ。1つ目のパートはモデリングソフトウェアの内部で行われ、リギングと呼ばれている。ここで起こるのは、アーティストがメッシュの下のボーンの骨格を定義することだ。メッシュはオブジェクトの皮膚を表し(それが人や怪物のものだろうと何だろうと)、ボーンは現実世界での動きを真似てメッシュを動かすために使われる。これはそれぞれの頂点を一つかそれ以上のボーンに割り当てることで行われる。頂点がボーンに割り当てられるとき、あるボーンの動きの頂点への影響度を示す 重み が定義される。一般的な方法は、すべての重みの合計が 1 になるようにする(頂点ごとで)。例えば、頂点が正確に2つのボーンの間に位置するなら、おそらく我々はそれらのボーンの頂点への影響を等しくしたいので、ボーンへの重みにそれぞれ 0.5 を割り当てるだろう。逆に、頂点が完全に単独のボーンの影響範囲内なら、重みは 1 だろう。(そのボーンが単独で頂点の動きを制御することを意味する)。

blenderで作ったボーン構造の例:

blenderで作ったボーン構造の例

上に載せたのは、アニメーションに本当に重要な部分だ。アーティストはボーン構造と一緒にリグをつけ、そしてそれぞれのアニメーション("歩く"、"走る"、"死ぬ"、など)のキーフレームのセットを定義する。キーフレームは、アニメーションパスに沿う臨界点での、すべてのボーンの変換情報を含む。グラフィックスエンジンは、キーフレーム間の変換を補完し、滑らかなモーションを作成する。

スケルタルアニメーションで使われるボーン構造は多くの場合、階層的だ。これはボーンに 子供/親 関係があり、それで一つのボーンの木が作られることを意味する。根のボーンを除いて、すべてのボーンは親を持つ。例えば人間の体の場合、君は背骨を根に割り当て、手足、そして指のような子のボーンを続く階層に割り当てるだろう。親のボーンが動いたとき、すべての子のボーンを動かす。だが子のボーンが動いたとき、親は動かさない(我々の指は手を動かさずに動かせるが、手を動かすとすべての指が動く)。実際には、これはボーンの変換を処理するとき、すべての親のボーンの変換を根からたどって合成する必要が有ることを意味する。

ここでは任意のリギングの話をするつもりはない。これは複雑な話題だし、グラフィックスプログラマーには門外だ。アーティストがこの仕事をする助けになるツールをモデリングソフトウェアが持っているし、君が格好いいメッシュと骨格を作るには、良いアーティストになる必要がある。グラフィックスエンジンでスケルタルアニメーションをするために必要なものを見ていこう。

第一段階では、頂点ごとのボーン情報のための頂点バッファを確保する。やり方には幾つか選択肢があるが、我々がやろうとしているのは非常に簡単な方法だ。我々は頂点ごとに、ボーンIDと重みを、要素に持つ配列を追加していく。コトを簡単にするために、4つの要素をもつ配列を使う。これは頂点が4つより多くのボーンから影響を受けないことを意味する。もし、より沢山ボーンを持つモデルを読み込むなら配列を大きくする必要があるが、このチュートリアルデモで使うDoom 3モデルは4つのボーンで十分だ。したがって、新しい頂点構造体は以下のようになる:

新しい頂点構造体

ボーンIDはボーン変換配列のインデックスだ。これらの変換情報はWVP行列を掛ける前に、位置と法線に適用される(すなわち頂点を"ボーン空間"からローカル空間に変換する)。重みは、いくつかの変換を合成して一つにするために使われ、どんな場合でも重みの合計は正確に 1 にならなければならない(ここはモデリングソフトウェアが責任を負う)。普通なら毎フレーム、アニメーションのキーフレームを補間して、ボーン変換配列を更新するだろう。

ボーン変換配列を作る方法は通常だと複雑だ。変換情報が階層的構造(つまり木)にセットされる。一般的な方法ではスケーリングベクトル、回転クォータニオン、移動ベクトルを、木のすべてのノードに持つ。実際には、各ノードはこれらの項目の配列が含まれている。配列の各エントリはタイムスタンプを持っている必要がある。アプリケーションの時間が正確にタイムスタンプと一致することはまれなので、特定の時点の正しい変換を得るため、スケーリング/回転/移動を補間しなければならない。現在のボーンから根までの各ノードで同じ処理をし、最終的な結果を得るために一緒にこの変換の連なりを掛ける。各ボーンにそれをしたら、シェーダーを更新する。

ここまでは、非常に一般的なことを話した。しかしこれは、Assimpでのスケルタルアニメーションのチュートリアルなので、そのライブラリについて深く触れて、それでスキニングする方法を語らなければならない。Assimpの良いところは色々な形式からボーン情報を読み込めることで、悪いところはシェーダーに渡すためのボーン情報を生成するために、データ構造にかなりの量の仕事をしなければならないことだ。

頂点レベル(訳注:頂点シェーダに送る準備段階の、という意味と思われる)でのボーン情報から始めよう。Assimpデータ構造で関連する部分は、以下のとおりだ:

Assimpデータ構造で関連する部分

Assimpのチュートリアルを思い出すと、すべてはaiSceneクラス(メッシュファイルをインポートした時に得るオブジェクトだ)に含まれていた。aiSceneはaiMeshオブジェクトの配列を含んでいる。aiMeshはモデルの一部で、位置、法線、テクスチャ座標、その他、といった頂点レベルの要素を含んでいる。そしてaiMeshはaiBoneオブジェクトの配列を含んでいることが見えるだろう。驚きに値しないが、aiBoneはメッシュの骨格の、一本のボーンを表現している。それぞれのボーンは、ボーン階層構造で見つけられる名前(下を見て)、頂点の重みの配列と4x4のオフセット行列を持っている。この行列が必要な理由は、頂点がローカル空間で保持されているからだ。これはスケルタルアニメーションのサポートなしで、既存のコードベースでモデルを読み込み正しく描画できることを意味する。しかし階層構造のボーン変換はボーン空間で動く。(そしてすべてのボーンが、一緒に変換を掛ける必要がある自分の空間を持っている)だから、オフセット行列の仕事は頂点の位置を、メッシュのローカル空間から、その特定のボーンのボーン空間に動かすことだ。

面白くなり始めるところが、頂点重み配列だ。この配列の各エントリはaiMeshの中の頂点の配列のインデックスと重さを含んでいる。頂点のすべての重みの合計は 1 であるはずであるが、これらを得るためには、すべてのボーンを歩きまわって各頂点のリストのようなものに重みを蓄積する必要がある。

ボーン情報を頂点レベルで構築したら、ボーン変換構造を処理して、シェーダーに読み込ませる最終的な変換を生成する必要がある。以下の絵はデータ構造の関係を示している:

データ構造の関係

もう一度aiSceneから始めよう。aiSceneオブジェクトは、ノード構造(木)の根であるaiNodeクラスオブジェクトへのポインターを含んでいる。木の各ノードは親へ戻るポインターと子へのポインターの配列を持っている。これのお陰で木を戻ったり進んだりして辿る事ができる。加えて、ノードはノード空間から親の空間へ変換する変換行列を抱えている。最後に、ノードは名前を持っていたり持っていなかったりする。もしノードが階層構造の中のボーンを表現していたら、ノードの名前はボーンの名前と一致している必要がある。しかし、ノードが名前を持たないことは時々あり(これは関係するボーンがないことを意味する)、そしてこれらの仕事は、単にモデラーがモデルを分解し、いくつかの中間の変換を方法に沿って配置することを助けることだ。

パズルの最後のピースは、これもまたaiSceneオブジェクトに格納されたaiAnimation配列だ。単独のaiAnimationオブジェクトは"歩く"、"走る"、"撃つ"、などのようなアニメーションフレームの連続をひとつ表現している。フレーム間の補間により、アニメーションの名前に一致した、お望みの見た目への作用を得る。アニメーションはティック(訳注:tick。「システム・クロック」によって生成される一定の「テンポ」)で期間を持ち、一秒毎にいくつのティックをもつか(例えば、25ティック毎秒で100ティックなら、4秒のアニメーションを表現している)はすべてのハードウェアでアニメーションを同じ見た目にするのを助けてくれる。加えて、アニメーションはチャンネルと呼ばれる、aiNodeAnimオブジェクトの配列を持っている。各チャンネルは実際には、ボーンとすべてのそれの変換情報だ。チャンネルは階層構造のノードの一つに一致していなければならない名前と、3つの変換の配列を持っている。

特定の時間の最終的なボーン変換を計算するために、それら3つの配列から時間が一致する2つのエントリーをそれぞれ見つけて補間する必要がある。次に、変換を一つの行列に合成する必要がある。そうすると、階層構造の関連するノードを探す必要があり、そして親に進む。次に、親の関係があるチャンネルを必要とし、同じ補完処理をする。根に到達するまで、2つの変換を掛けあわせ続ける。

コード解説

(mesh.cpp:77)

bool Mesh::LoadMesh(const string& Filename)
{
    // 前に読んだメッシュを開放する (存在するなら)

    Clear();

    // VAOを作成
    glGenVertexArrays(1, &m_VAO); 
    glBindVertexArray(m_VAO);

    // 頂点属性のためのバッファーを作成
    glGenBuffers(ARRAY_SIZE_IN_ELEMENTS(m_Buffers), m_Buffers);

    bool Ret = false; 
    
// ここから追加分
    m_pScene = m_Importer.ReadFile(Filename.c_str(), aiProcess_Triangulate | aiProcess_GenSmoothNormals | 
                                    aiProcess_FlipUVs);
// ここまで
    
    if (m_pScene) { 
// ここから追加分 
       m_GlobalInverseTransform = m_pScene->mRootNode->mTransformation;
       m_GlobalInverseTransform.Inverse();
       Ret = InitFromScene(m_pScene, Filename);
// ここまで
    }
    else {
// ここから追加分
       printf("Error parsing '%s': '%s'\n", Filename.c_str(), m_Importer.GetErrorString());
// ここまで
    }

    // VAOが外部から変更されないことを確かめろ
    glBindVertexArray(0);	

    return Ret;
}

これはMeshクラスの初期化部分を書き換えたもので、変更点は強調してある。特記すべき変更点は2つ。1つ目は、importerとaiSceneオブジェクトはスタック変数だったが、今はクラスメンバ変数であることだ。その理由は、実行時に何度も何度もaiSceneオブジェクトに戻ってくる、というのと、importerとsceneの両方のスコープを拡張する必要がある、ということだ。実際のゲームでは、より最適化された形式でデータをコピーして保持したいはずだが、教育的観点からこれで十分とする。

2つ目の変更点は、階層構造の根の逆変換行列を保持していることだ。これは将来使うつもりだ。ちなみに、行列を逆行列にするコードを、Assimpライブラリーからコピーして我々のMatrix4fクラスに追加していることに注意。

(mesh.h:69)

struct VertexBoneData
{ 
    uint IDs[NUM_BONES_PER_VEREX];
    float Weights[NUM_BONES_PER_VEREX];
}

(mesh.cpp:109)

bool Mesh::InitFromScene(const aiScene* pScene, const string& Filename)
{ 
    ...
    vector<VertexBoneData> Bones;
    ...
    Bones.resize(NumVertices);
    ...
    glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[BONE_VB]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Bones[0]) * Bones.size(), &Bones[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(BONE_ID_LOCATION);
// ↓注意
    glVertexAttribIPointer(BONE_ID_LOCATION, 4, GL_INT, sizeof(VertexBoneData), (const GLvoid*)0);
    glEnableVertexAttribArray(BONE_WEIGHT_LOCATION); 
    glVertexAttribPointer(BONE_WEIGHT_LOCATION, 4, GL_FLOAT, GL_FALSE, sizeof(VertexBoneData), (const GLvoid*)16);
    ...
}

上側の構造体は頂点レベルで必要とするすべてを含んでいる。デフォルトでは、4つのボーン(ボーンごとの、IDと重み)を格納するのに十分な領域を確保する。VertexBoneData構造体はシェーダーに単純に送れるように組んだ。位置、テクスチャ座標、法線にはそれぞれ、location 0, 1, 2を使う。したがって、ボーンIDにはlocation 3、重みにはlocation 4をバインドするように、VAOを構成する。とても注意してほしいのが、glVertexAttribPointerではなくglVetexAttribIPointerをIDをバインドするために使っている点だ。これはIDは浮動小数点ではなく整数であるからだ。これには注意を払っておかないと、シェーダーでは壊れたデータを受け取ることになる。

(mesh.cpp:215)

void Mesh::LoadBones(uint MeshIndex, const aiMesh* pMesh, vector& Bones)
{
    for (uint i = 0 ; i < pMesh->mNumBones ; i++) { 
        uint BoneIndex = 0; 
        string BoneName(pMesh->mBones[i]->mName.data);

        if (m_BoneMapping.find(BoneName) == m_BoneMapping.end()) {
            BoneIndex = m_NumBones;
            m_NumBones++; 
            BoneInfo bi;	
            m_BoneInfo.push_back(bi);
        }
        else {
            BoneIndex = m_BoneMapping[BoneName];
        }

        m_BoneMapping[BoneName] = BoneIndex;
        m_BoneInfo[BoneIndex].BoneOffset = pMesh->mBones[i]->mOffsetMatrix;

        for (uint j = 0 ; j < pMesh->mBones[i]->mNumWeights ; j++) {
            uint VertexID = m_Entries[MeshIndex].BaseVertex + pMesh->mBones[i]->mWeights[j].mVertexId;
            float Weight = pMesh->mBones[i]->mWeights[j].mWeight; 
            Bones[VertexID].AddBoneData(BoneIndex, Weight);
        }
    } 
}

上記の関数は、ひとつのaiMeshオブジェクトから頂点ボーン情報を読み込む。これはMesh::InitMesh()から呼び出される。VertexBoneData構造体への格納に加え、この関数もボーン名とボーンIDを繋ぐマップを更新し(この関数により実行中のインデックスは管理される)、ボーンIDを基にしたvectorへオフセット行列を格納する。頂点IDがどうやって計算されているかに注意。頂点IDは一つのメッシュに関連し、そして、すべてのメッシュを、絶対的な頂点IDを得るために配列mWeightsの頂点IDへと現在のaiMeshの基点頂点IDを加えた一つのvectorに格納する。

(mesh.cpp:31)

void Mesh::VertexBoneData::AddBoneData(uint BoneID, float Weight)
{
    for (uint i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(IDs) ; i++) {
        if (Weights[i] == 0.0) {
            IDs[i] = BoneID;
            Weights[i] = Weight;
            return;
        } 
    }

    // 確保した空間よりも多くのボーンが無い限り、ここに到達することはない。
    assert(0);
}

このお役立ち関数は、VertexBoneData構造体の空きスロットを見つけて、その中にボーンIDと重みを配置する。いくつかの頂点は4つより少ないボーンに影響を受けるが、まだ存在しないボーンの重みはゼロのまま(VertexBoneDataのコンストラクタを見よ)であるので、同じ重み計算を任意の数のボーンに使えることを意味している。

(mesh.cpp:469)

Matrix4f Mesh::BoneTransform(float TimeInSeconds, vector<Matrix4f>& Transforms)
{
    Matrix4f Identity;
    Identity.InitIdentity();

    float TicksPerSecond = m_pScene->mAnimations[0]->mTicksPerSecond != 0 ? 
                            m_pScene->mAnimations[0]->mTicksPerSecond : 25.0f;
    float TimeInTicks = TimeInSeconds * TicksPerSecond;
    float AnimationTime = fmod(TimeInTicks, m_pScene->mAnimations[0]->mDuration);

    ReadNodeHeirarchy(AnimationTime, m_pScene->mRootNode, Identity);

    Transforms.resize(m_NumBones);

    for (uint i = 0 ; i < m_NumBones ; i++) {
        Transforms[i] = m_BoneInfo[i].FinalTransformation;
    }
}

今まで見てきた頂点レベルでのボーン情報の読み込みは、起動中にメッシュを読み込むときに一回だけ実行される。今から2つ目のパートに突入し、ボーンの変換を毎フレーム算出してシェーダーへ送る話をする。上記の関数はこの活動の実行開始部分だ。関数を呼び出す者は現在時間を秒(端数を取りうる)で伝え、更新しなければならない行列のvectorを渡す。 アニメーションサイクル内部の相対的時間を見つけ、ノード階層構造を処理している。変換の配列を結果として返す。

(mesh.cpp:424)

void Mesh::ReadNodeHeirarchy(float AnimationTime, const aiNode* pNode, const Matrix4f& ParentTransform)
{ 
    string NodeName(pNode->mName.data);

    const aiAnimation* pAnimation = m_pScene->mAnimations[0];

    Matrix4f NodeTransformation(pNode->mTransformation);

    const aiNodeAnim* pNodeAnim = FindNodeAnim(pAnimation, NodeName);

    if (pNodeAnim) {
        // スケーリングを補間し、スケーリング変換行列を生成する
        aiVector3D Scaling;
        CalcInterpolatedScaling(Scaling, AnimationTime, pNodeAnim);
        Matrix4f ScalingM;
        ScalingM.InitScaleTransform(Scaling.x, Scaling.y, Scaling.z);

        // 回転を補間し、回転変換行列を生成する
        aiQuaternion RotationQ;
        CalcInterpolatedRotation(RotationQ, AnimationTime, pNodeAnim); 
        Matrix4f RotationM = Matrix4f(RotationQ.GetMatrix());

        // 移動を補間し、移動変換行列を生成する
        aiVector3D Translation;
        CalcInterpolatedPosition(Translation, AnimationTime, pNodeAnim);
        Matrix4f TranslationM;
        TranslationM.InitTranslationTransform(Translation.x, Translation.y, Translation.z);

        // これら上記の変換を合成する
        NodeTransformation = TranslationM * RotationM * ScalingM;
    }

    Matrix4f GlobalTransformation = ParentTransform * NodeTransformation;

    if (m_BoneMapping.find(NodeName) != m_BoneMapping.end()) {
        uint BoneIndex = m_BoneMapping[NodeName];
        m_BoneInfo[BoneIndex].FinalTransformation = m_GlobalInverseTransform * GlobalTransformation * 
                                                    m_BoneInfo[BoneIndex].BoneOffset;
    }

    for (uint i = 0 ; i < pNode->mNumChildren ; i++) {
        ReadNodeHeirarchy(AnimationTime, pNode->mChildren[i], GlobalTransformation);
    }
}

この関数はノードツリーを辿り、各ノード/ボーンの最終的な変換を、指定されたアニメーション時間で生成する。メッシュは単一のアニメーションシーケンスだけを持つと仮定して、これはそう制限されている。もし君が複数のアニメーションをサポートしたいなら、アニメーション名を伝え、配列m_pScene->mAnimations[]からそれを探す必要がある。上記のコードは、今回のデモのメッシュには十分である。

ノードの変換はそのノードのmTransformationメンバにて初期化される。もしノードがボーンと一致しないなら、その時はこれがそれの最後の変換だ。それをしたら、生成した行列でそれを上書きする。これは以下のように行われる: まずアニメーションの配列、チャンネルの中からノード名を探す。その後、アニメーション時間に基づいて、スケーリングベクトル、回転クォータニオン、そして移動ベクトルを補間する。それらを一つの行列に合成して、引数としてとった行列(GlobalTransformation)を掛ける。この関数は再帰関数で、まずルートノードに対して引数GlobalTransformationに単位行列をとって呼び出される。再帰的に各ノードですべての子に対して呼び出し、自身の変換をGlobalTransformationとして渡す。頂上から始めてふもとへ処理していくので、ノード毎に繋がった合成変換を得る。

配列m_BoneMappingは生成したインデックスにノード名をマッピングしており、このインデックスを最終的な変換が格納されている配列m_BoneInfoの添字として使っている。最終的な変換は以下のように算出される: ローカル空間からノード空間へ頂点位置を移動させる、ノードのオフセット行列から始める。次に、ノードの親に加えて、アニメーション時間に応じてノードのために計算された特定の変換を合成した変換を掛ける。

数学のものを処理するためにAssimpのコードを使っていることに注意。このように単純にAssimpを使っているのは我々のコードベースにそれを写す利点を見いだせなかったからだ。

(mesh.cpp:383)

void Mesh::CalcInterpolatedRotation(aiQuaternion& Out, float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    // 補間には最低でも2つの値が必要…
    if (pNodeAnim->mNumRotationKeys == 1) {
        Out = pNodeAnim->mRotationKeys[0].mValue;
        return;
    }

    uint RotationIndex = FindRotation(AnimationTime, pNodeAnim);
    uint NextRotationIndex = (RotationIndex + 1);
    assert(NextRotationIndex < pNodeAnim->mNumRotationKeys);
    float DeltaTime = pNodeAnim->mRotationKeys[NextRotationIndex].mTime - pNodeAnim->mRotationKeys[RotationIndex].mTime;
    float Factor = (AnimationTime - (float)pNodeAnim->mRotationKeys[RotationIndex].mTime) / DeltaTime;
    assert(Factor >= 0.0f && Factor <= 1.0f);
    const aiQuaternion& StartRotationQ = pNodeAnim->mRotationKeys[RotationIndex].mValue;
    const aiQuaternion& EndRotationQ = pNodeAnim->mRotationKeys[NextRotationIndex].mValue;
    aiQuaternion::Interpolate(Out, StartRotationQ, EndRotationQ, Factor);
    Out = Out.Normalize();
}

この関数は特定のアニメーション時間を基にしたチャンネルの回転クォータニオンだ。(チャンネルがクォータニオンのキーの配列を含むことに注意)はじめにアニメーション時間のちょうど前にあるクォータニオンのキーのインデックスを探し出す。アニメーション時間とその前のキーの間の距離と、そのキーと次のキーの間の距離の、比を計算する。それを因数にとって2つのキーの間を補間する必要がある。Assimpのコードを補間と結果の正規化に使っている。この関数の位置とスケーリング版はとても似ているので、ここには書かない。

(mesh.cpp:335)

uint Mesh::FindRotation(float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    assert(pNodeAnim->mNumRotationKeys > 0);

    for (uint i = 0 ; i < pNodeAnim->mNumRotationKeys - 1 ; i++) {
        if (AnimationTime < (float)pNodeAnim->mRotationKeys[i + 1].mTime) {
            return i;
        }
    }

    assert(0);
}

このお役立ち関数はアニメーション時間のすぐ前の、回転のキーを探し出す。もしN個の回転のキーをがあれば、結果は0からN-2を取りうる。アニメーション時間は常にチャンネルの期間の内側で、つまり最後のキー(N-1)は正しい結果ではない。

(skinning.glsl)

struct VSInput
{
    vec3 Position;
    vec2 TexCoord;
    vec3 Normal;
    ivec4 BoneIDs; // 注目
    vec4 Weights;  // 注目
};

interface VSOutput
{ 
    vec2 TexCoord; 
    vec3 Normal; 
    vec3 WorldPos; 
};

const int MAX_BONES = 100;

uniform mat4 gWVP;
uniform mat4 gWorld;
uniform mat4 gBones[MAX_BONES];
a
shader VSmain(in VSInput VSin:0, out VSOutput VSout)
{ 
    mat4 BoneTransform = gBones[VSin.BoneIDs[0]] * VSin.Weights[0];
    BoneTransform += gBones[VSin.BoneIDs[1]] * VSin.Weights[1];
    BoneTransform += gBones[VSin.BoneIDs[2]] * VSin.Weights[2];
    BoneTransform += gBones[VSin.BoneIDs[3]] * VSin.Weights[3];

    vec4 PosL = BoneTransform * vec4(VSin.Position, 1.0);
    gl_Position = gWVP * PosL;
    VSout.TexCoord = VSin.TexCoord;
    vec4 NormalL = BoneTransform * vec4(VSin.Normal, 0.0);
    VSout.Normal = (gWorld * NormalL).xyz;
    VSout.WorldPos = (gWorld * PosL).xyz; 
}

メッシュクラスへの変更についての話は今終わった。シェーダーレベルでしなくてはならないことについて語ろう。まず、VSInput構造体にボーンIDと重みを追加した。次に、ボーン変換を含む新しいuniform配列がある。シェーダーでは、頂点の変換行列の組み合わせと、重みの組み合わせとして、最終的なボーン変換を自分自身で計算する。この最終的な行列は、ボーン空間の位置と法線をローカル空間へと変換するのに使われる。ここから先は全部変更無しだ。

(tutorial38.cpp:140)

float RunningTime = CalcRunningTime();

m_mesh.BoneTransform(RunningTime, Transforms);

for (uint i = 0 ; i < Transforms.size() ; i++) {
    m_pEffect->SetBoneTransform(i, Transforms[i]);
}

最後にやらなければならないのは、アプリケーションコードにこのようなものをすべて統合することだ。これは上記の単純なコードで完了する。この関数CalcRunningTime()はアプリケーションが起動してからの時間を秒単位で返す。(浮動小数点数の範囲で、であることに注意)

すべてを正しくやったなら、最終結果はこれに似た感じになるはずだ。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment