【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜

C++
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜〜
マイケル
マイケル
今日も前回に引き続き、C++でのゲーム開発を進めていくよ!
前回は3Dゲーム開発基礎編ということで、座標変換周りを解説していきましたが、
今回も基礎編で fbx読込とシェーダ編 になります!

↑前回の記事

ScreenShot 2021 10 09 22 00 13 2
↑本記事での解説範囲
エレキベア
エレキベア
シェーダは聞いたことあるクマ〜〜〜
マイケル
マイケル
サンプルプロジェクトは前回と同じもので、
4種類のシェーダで3Dモデルを描画するサンプルになっています。
01 ALL
↑4種類のシェーダを実装している

GitHub – cpp-opengl-sample

マイケル
マイケル
それでは早速進めていきましょう!
スポンサーリンク

参考書籍

マイケル
マイケル
前回と同様、基本的な実装については下記書籍をベースに作成しています。

ゲームプログラミングC++

マイケル
マイケル
そして数式や理論に関してはところどころ
下記2冊と照らし合わせながら整理しました。

ゲームプログラマになる前に覚えておきたい技術

ゲームを動かす数学・物理 R

エレキベア
エレキベア
どの本も広く網羅されているから
おすすめクマ〜〜〜〜

使用したモデル

マイケル
マイケル
書籍ではgpmeshファイルを読み込む方向で記載されていましたが、
やはり fbxファイル の方が馴染みがあるので、こちらを読み込むよう実装していこうと思います!
マイケル
マイケル
というわけで3D読込で使用したサンプルモデルは以下になります。
Blenderで作成した立方体にサイコロテクスチャを貼っただけの
シンプルなfbxモデル
です。
ScreenShot 2021 10 14 22 11 37
↑Blenderで作成
Saikoro tex
↑サイコロのテクスチャ
マイケル
マイケル
こちらもプロジェクト内に含めているので、
ご自由にお使いください!
エレキベア
エレキベア
これはクマでも作れるクマ〜〜〜

fbxsdkでのロード処理

マイケル
マイケル
今回fbxモデルを読み込むにあたって、
AutoDesk社が提供している fbxsdk というライブラリを使用させていただきました!
マイケル
マイケル
インストール方法については省略しますが、
基本的にインストールしてパスを通すだけで使えるかと思います!
エレキベア
エレキベア
準備完了クマ〜〜〜〜

最終的な座標の形式

マイケル
マイケル
まず最終的に座標をどのように設定したいかについてですが、
下記のようにVertexArrayクラスに 頂点バッファとインデックスバッファ を渡して
シェーダに設定する形となります。
VertexArray::VertexArray(const float *vertices,

                         unsigned int numVertices,
                         const unsigned int *indices,
                         unsigned int numIndices)
:mNumVertices(numVertices)
,mNumIndices(numIndices)
{
    // 頂点配列オブジェクトの作成
    glGenVertexArrays(1, &mVertexArray);
    glBindVertexArray(mVertexArray);

    // 頂点バッファの作成
    glGenBuffers(1, &mVertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, mVertexBuffer);
    glBufferData(GL_ARRAY_BUFFER,                 // バッファの種類
                 numVertices * 8 * sizeof(float), // コピーするバイト数(位置(xyz), 法線(xyz), u, v)
                 vertices,                        // コピー元
                 GL_STATIC_DRAW);                 // データの利用方法

    // インデックスバッファの作成
    glGenBuffers(1, &mIndexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,           // バッファの種類
                 numIndices * sizeof(unsigned int), // コピーするバイト数
                 indices,                           // コピー元
                 GL_STATIC_DRAW);                   // データの利用方法

    // 頂点レイアウトの指定
    // 頂点属性0: 位置(x,y,z)
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, 0);
    // 頂点属性1: 法線(x,y,z)
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8,
                          reinterpret_cast<void*>(sizeof(float) * 3)); // オフセット値
    // 頂点属性2: u,v
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 8,
                          reinterpret_cast<void*>(sizeof(float) * 6)); // オフセット値
}
マイケル
マイケル
そして今回はライティングに必要なパラメータとして、
位置座標、法線座標、UV座標 も必要となります。
そのため頂点バッファは下記のように、8つで1セットとして渡す形にします。
ScreenShot 2021 10 14 22 02 24 01
↑最終的に位置座標、法線座標、UV座標を1セットとして渡す
エレキベア
エレキベア
読み込んだ座標情報をこの形に直して設定するクマね

初期化処理

マイケル
マイケル
渡す形を認識した上で、fbxsdkを使って読み込んでいきます!
まずは下記のように初期化処理を行い、ファイルを読み込みましょう!
    // マネージャーの初期化
    FbxManager* manager = FbxManager::Create();

    // インポーターの初期化
    FbxImporter* importer = FbxImporter::Create(manager, "");
    if (!importer->Initialize(filePath.c_str(), -1, manager->GetIOSettings()))
    {
        SDL_Log("failed fbx initialize importer.");
        return false;
    }

    // シーンの作成
    FbxScene* scene = FbxScene::Create(manager, "");
    importer->Import(scene);
    importer->Destroy();

    // 三角ポリゴンへのコンバート
    FbxGeometryConverter geometryConverter(manager);
    if (!geometryConverter.Triangulate(scene, true))
    {
        SDL_Log("failed fbx convert triangle.");
        return false;
    }

    // メッシュ取得
    FbxMesh* mesh = scene->GetSrcObject<FbxMesh>();
    if (!mesh)
    {
        SDL_Log("failed fbx scene get mesh.");
        return false;
    }

頂点座標の読込

マイケル
マイケル
ファイルを読み込んでメッシュ取得までできたら、
GetControlPointsCount() で頂点座標を取得することができます。
    // 頂点座標の読込
    std::vector<std::vector<float>> vertexList;
    for (int i = 0; i < mesh->GetControlPointsCount(); i++)
    {
        FbxVector4 point = mesh->GetControlPointAt(i);
        std::vector<float> vertex;
        vertex.push_back(point[0]);
        vertex.push_back(point[1]);
        vertex.push_back(point[2]);
        vertexList.push_back(vertex);
    }
エレキベア
エレキベア
これだけで取得できるのは便利クマ〜〜〜〜〜

インデックス座標の読込

マイケル
マイケル
そしてインデックス座標に関しては、GetPolygonVertex() で取得できます。
このインデックスは頂点座標と対応しているため、ここから頂点座標を取得することもできます!
    // インデックス座標の読込
    std::vector<int> vertexIndexList;
    // ポリゴンごとにループ
    int polCount = mesh->GetPolygonCount();
    for (int polIndex = 0; polIndex < polCount; polIndex++)
    {
        // 頂点ごとにループ
        int polVertexCount = mesh->GetPolygonSize(polIndex);
        for (int polVertexIndex = 0; polVertexIndex < polVertexCount; polVertexIndex++)
        {
            // 頂点インデックスの取得
            int vertexIndex = mesh->GetPolygonVertex(polIndex, polVertexIndex);
            std::vector<float> vertex = vertexList[vertexIndex];

・・・略・・・
            // インデックスバッファを追加
            vertexIndexList.push_back(vertexIndex);
        }
    }
マイケル
マイケル
ここまで読み込んだ情報は下記のようになります。
ScreenShot 2021 10 14 22 02 24 02
↑頂点座標とインデックス座標の対応
エレキベア
エレキベア
ここまでは余裕クマね

法線座標、UV座標の読込

マイケル
マイケル
法線座標、UV座標に関しても、インデックス座標と同様、
下記のように取得することができます。
            // 法線座標の取得
            FbxVector4 normalVec4;
            mesh->GetPolygonVertexNormal(polIndex, polVertexIndex, normalVec4);

            // UV座標の取得
            FbxVector2 uvVec2;
            bool isUnMapped;
            mesh->GetPolygonVertexUV(polIndex, polVertexIndex, uvSetName, uvVec2, isUnMapped);
エレキベア
エレキベア
これで完了クマね〜〜〜〜
案外ちょろかったクマ
マイケル
マイケル
そう、これで全ての座標が取得できたからそのまま渡せば完了!
・・・と思いきや・・・
99 NG
↑崩壊したサイコロ達
マイケル
マイケル
そのまま設定してしまうとこのようにモデルが崩壊してしまいます・・・
エレキベア
エレキベア
なんてことだクマ・・・・
マイケル
マイケル
というのも法線、UV座標に関しては下記のように、
同じインデックス座標でも異なる座標のものがある からなんですね・・・。
ScreenShot 2021 10 14 22 03 06
↑インデックス座標と法線・UV座標の対応
ScreenShot 2021 10 14 22 03 25
↑同じ頂点で異なるものがあるため、法線・UV座標が上書きされてしまう
エレキベア
エレキベア
これは困ったクマ〜〜〜・・・・
マイケル
マイケル
そのため、今回設定する形に直すには、
法線、UV座標が異なるものについてインデックス座標を振り直す必要がある
ということになります。
マイケル
マイケル
これを力技で実装したのが下記になります。
他にいい方法はあると思いますが、今回はこれで進めようと思います・・・。
bool Mesh::Load(const std::string &filePath, Game* game)
{

・・・略・・・

    // インデックス座標の読込
    std::vector<int> vertexIndexList;
    std::vector<std::vector<int>> newVertexIndexList;
    // ポリゴンごとにループ
    int polCount = mesh->GetPolygonCount();
    for (int polIndex = 0; polIndex < polCount; polIndex++)
    {
        // 頂点ごとにループ
        int polVertexCount = mesh->GetPolygonSize(polIndex);
        for (int polVertexIndex = 0; polVertexIndex < polVertexCount; polVertexIndex++)
        {
            // 頂点インデックスの取得
            int vertexIndex = mesh->GetPolygonVertex(polIndex, polVertexIndex);
            std::vector<float> vertex = vertexList[vertexIndex];

            // 法線座標の取得
            FbxVector4 normalVec4;
            mesh->GetPolygonVertexNormal(polIndex, polVertexIndex, normalVec4);

            // UV座標の取得
            FbxVector2 uvVec2;
            bool isUnMapped;
            mesh->GetPolygonVertexUV(polIndex, polVertexIndex, uvSetName, uvVec2, isUnMapped);

            if (vertex.size() == 3)
            {
                // 法線座標とUV座標が未設定の場合、頂点情報に付与して設定
                std::vector<float> vertexInfo = CreateVertexInfo(vertex, normalVec4, uvVec2);
                vertexList[vertexIndex] = vertexInfo;
            }
            else if (!IsEqualNormalUV(vertex, normalVec4, uvVec2))
            {
                // *同一頂点インデックスの中で法線座標かUV座標が異なる場合、
                // 新たな頂点インデックスとして作成する

                // 新たな頂点インデックスとして作成済かどうか?
                bool isNewVertexCreated = false;
                for (int i = 0; i < newVertexIndexList.size(); i++)
                {
                    int oldIndex = newVertexIndexList[i][0];
                    int newIndex = newVertexIndexList[i][1];
                    if (oldIndex == vertexIndex
                        && IsEqualNormalUV(vertexList[newIndex], normalVec4, uvVec2))
                    {
                        isNewVertexCreated = true;
                        vertexIndex = newIndex;
                        break;
                    }
                }
                // 作成済でない場合
                if (!isNewVertexCreated)
                {
                    // 新たな頂点インデックスとして作成
                    std::vector<float> vertexInfo = CreateVertexInfo(vertex, normalVec4, uvVec2);
                    vertexList.push_back(vertexInfo);
                    // 作成したインデックス情報を設定
                    int newIndex = vertexList.size() - 1;
                    std::vector<int> newVertexIndex;
                    newVertexIndex.push_back(vertexIndex); // old index
                    newVertexIndex.push_back(newIndex);    // new index
                    newVertexIndexList.push_back(newVertexIndex);
                    vertexIndex = newIndex;
                }
            }
            // インデックスバッファを追加
            vertexIndexList.push_back(vertexIndex);
        }
    }

・・・略・・・

}

// 頂点情報作成処理
std::vector<float> Mesh::CreateVertexInfo(const std::vector<float>& vertex, const FbxVector4& normalVec4,
                         const FbxVector2& uvVec2)
{
    std::vector<float> vertexInfo;
    // 位置座標
    vertexInfo.push_back(vertex[0]);
    vertexInfo.push_back(vertex[1]);
    vertexInfo.push_back(vertex[2]);
    // 法線座標
    vertexInfo.push_back(normalVec4[0]);
    vertexInfo.push_back(normalVec4[1]);
    vertexInfo.push_back(normalVec4[2]);
    // UV座標
    vertexInfo.push_back(uvVec2[0]);
    vertexInfo.push_back(uvVec2[1]);
    return vertexInfo;
}

// vertexInfoに法線、UV座標が設定済かどうか?
bool Mesh::IsEqualNormalUV(const std::vector<float> vertexInfo,
                           const FbxVector4 &normalVec4, const FbxVector2 &uvVec2)
{
    return fabs(vertexInfo[3] - normalVec4[0]) < FLT_EPSILON
            && fabs(vertexInfo[4] - normalVec4[1]) < FLT_EPSILON
            && fabs(vertexInfo[5] - normalVec4[2]) < FLT_EPSILON
            && fabs(vertexInfo[6] - uvVec2[0]) < FLT_EPSILON
            && fabs(vertexInfo[7] - uvVec2[1]) < FLT_EPSILON;
}
マイケル
マイケル
これにより、振り直されたインデックス座標は下記のようになります。
ScreenShot 2021 10 14 22 03 47 01
↑インデックス座標を振り直した結果
ScreenShot 2021 10 14 22 03 47 02
↑振り直されたインデックス座標
マイケル
マイケル
これを並び替えると、下記のように
1つの頂点について3つの頂点バッファがある状態になります。
ScreenShot 2021 10 14 22 04 02
エレキベア
エレキベア
これは立方体だからクマね
マイケル
マイケル
これで必要な座標情報は一通り揃いました!

シェーダの描画

マイケル
マイケル
それではシェーダの描画を行なっていきましょう!
シェーダは前回も少し触れましたが、受け取った座標を元に計算を行い描画を行うもの
になります。
マイケル
マイケル
そしてシェーダは大きく
・頂点シェーダ
・フラグメントシェーダ

の2つに分かれており、それぞれ座標の計算と見た目の調整を行います!
ScreenShot 2021 10 09 21 59 57
↑シェーダの描画プロセス
エレキベア
エレキベア
いよいよ後は描画するだけクマね
マイケル
マイケル
今回はサンプルプロジェクトでも実装した、
基本となる下記4種類のシェーダを紹介します!
01 ALL


[シェーダ種類]
・単色描画
・スプライト描画
・ランバート反射モデル
・フォン反射モデル

エレキベア
エレキベア
楽しみクマ〜〜〜

単色描画

マイケル
マイケル
まずは単色での描画について!
今回は下記のように青色一色で描画しています!
02 BASIC
エレキベア
エレキベア
これは簡単そうクマね
マイケル
マイケル
受け取った座標情報を元に 頂点シェーダ内でクリップ座標まで変換 して、
フラグメントシェーダ内で出力色を指定 しているだけになるね!
#version 330

uniform mat4 uWorldTransform; // ワールド変換座標
uniform mat4 uViewProjection; // ビュー射影行列

in vec3 inPosition;

void main() {
    // w成分を加える
    vec4 pos = vec4(inPosition, 1.0);
    // ローカル座標 * ワールド変換座標 * ビュー射影行列
    // を逆に計算して、クリップ空間座標に変換
    gl_Position = uViewProjection * uWorldTransform * pos;
}
#version 330

out vec4 outColor;

void main() {
    // 青色を出力
    outColor = vec4(0.0, 0.0, 1.0, 1.0);
}
マイケル
マイケル
このシェーダでは法線座標、UV座標は使用せずに描画することができます!
1つ注意点をあげるとすれば、座標計算の前には vec4(inPosition, 1.0) というように
w成分を足して4要素にする必要があるくらいです。
エレキベア
エレキベア
はやく次にいくクマ〜〜〜

スプライト描画

マイケル
マイケル
次はテクスチャを貼って描画したものになります。
ここからUV座標を使用しますが、法線座標についてはまだ使用しません。
03 SPRITE
エレキベア
エレキベア
3Dモデルっぽくなってきたクマね
マイケル
マイケル
頂点シェーダでの計算は同じですが、受け取ったUV座標について
フラグメントシェーダに渡すようにしています。
フラグメントシェーダでは 受け取ったUV座標とテクスチャ情報から描画処理 を行います。
#version 330

uniform mat4 uWorldTransform; // ワールド変換座標
uniform mat4 uViewProjection; // ビュー射影行列

layout(location = 0) in vec3 inPosition; // 位置座標
layout(location = 1) in vec3 inNormal;   // 法線座標
layout(location = 2) in vec2 inTexCoord; // UV座標

out vec2 fragTexCoord;

void main() {
    // w成分を加える
    vec4 pos = vec4(inPosition, 1.0);
    // ローカル座標 * ワールド変換座標 * ビュー射影行列
    // を逆に計算して、クリップ空間座標に変換
    gl_Position = uViewProjection * uWorldTransform * pos;
    // テクスチャ座標を設定
    fragTexCoord = inTexCoord;
}
#version 330

uniform sampler2D uTexture; // テクスチャ(自動で設定される)

in vec2 fragTexCoord;

out vec4 outColor;

void main() {
    // テクスチャを出力
    outColor = texture(uTexture, fragTexCoord);
}
マイケル
マイケル
テクスチャ情報については、下記のように読込処理を行い、
描画前にActiveにすることで自動的に設定 されます。
テクスチャの読込については第二回で使用した SDL_Imageライブラリ を使用しました。
bool Texture::Load(const std::string &filePath)
{
    // ファイル読込
    mTexture = IMG_Load(filePath.c_str());
    if (!mTexture)
    {
        SDL_Log("Failed load texture.");
        return false;
    }

    // RGBフォーマットの指定
    int rgbFormat = GL_RGB;
    if (mTexture->format->BitsPerPixel >= 4) rgbFormat = GL_RGBA;

    // テクスチャオブジェクトの作成
    glGenTextures(1, &mTextureID);
    glBindTexture(GL_TEXTURE_2D, mTextureID);
    glTexImage2D(GL_TEXTURE_2D, 0, rgbFormat, mTexture->w, mTexture->h, 0, rgbFormat,
                 GL_UNSIGNED_BYTE, mTexture->pixels);

    // バイリニアフィルタを有効にする
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    return true;
}

void Texture::Unload()
{
    glDeleteTextures(1, &mTextureID);
}

void Texture::SetActive()
{
    glBindTexture(GL_TEXTURE_2D, mTextureID);
}
エレキベア
エレキベア
ここまでもまあ簡単クマね

ランバート反射モデル

マイケル
マイケル
そして次はランバート反射モデル!
下記のように光の反射を考慮したモデルになります。
04 LAMBERT
エレキベア
エレキベア
なんか難しそうなのが来たクマ・・・
マイケル
マイケル
まず物がどのようにして見えているのかだけど、
これは 光源から出た光がいろんなところに反射して最終的に目に入ることで
見えていることになります。
影の中のものがうっすら見えるのも、周りの光がある程度反射しているためなのです。
ScreenShot 2021 10 16 12 05 34
↑光は反射しまくっている
マイケル
マイケル
しかしこの反射を全て考慮してしまうと、計算負荷がとんでもないことになってしまう・・・。
そのためN次的な反射は考慮せずに、光源からの光が当たる量のみを考慮したモデル
ランバート反射モデルといいます。
当たった光が全方位に拡散するという特性から 拡散反射モデル とも呼ばれます。
エレキベア
エレキベア
それなら計算量はがくっと減りそうクマね
マイケル
マイケル
計算方法としては、
頂点からの法線をN、頂点から光源へ向かうベクトルをL とした場合、
下記の計算で表すことができます。
ScreenShot 2021 10 16 0 28 45 01
ScreenShot 2021 10 16 0 28 54 01
マイケル
マイケル
全てのオブジェクトに均一に当たる光(環境色)を最低値として設定し、
そこに 光の拡散反射量を加えたもの を反射量とする考え方です!
マイケル
マイケル
具体的な実装は下記になります。
ここから法線座標も使用して計算することになります。
#version 330

uniform mat4 uWorldTransform; // ワールド変換座標
uniform mat4 uViewProjection; // ビュー射影行列

layout(location = 0) in vec3 inPosition; // 位置座標
layout(location = 1) in vec3 inNormal;   // 法線座標
layout(location = 2) in vec2 inTexCoord; // UV座標

// テクスチャ情報
out vec2 fragTexCoord; // 位置座標
out vec3 fragNormal;   // 法線座標
out vec3 fragWorldPos; // ワールド座標

void main() {
    // w成分を加える
    vec4 pos = vec4(inPosition, 1.0);
    // クリップ空間座標に変換して設定
    gl_Position = uViewProjection * uWorldTransform * pos;

    // テクスチャ情報を設定
    fragTexCoord = inTexCoord;
    // ワールド座標に変換する
    fragNormal = (uWorldTransform * vec4(inNormal, 0.0f)).xyz;
}
#version 330

uniform sampler2D uTexture; // テクスチャ(自動で設定される)
uniform vec3 uAmbientColor; // 環境色 ka

// 平行光源
struct DirectionalLight
{
    vec3 mDirection;    // 向き
    vec3 mDiffuseColor; // 拡散反射色 kd
};
uniform DirectionalLight uDirLight;

in vec2 fragTexCoord; // 位置座標
in vec3 fragNormal;   // 法線座標

out vec4 outColor;

void main() {
    // 計算に必要な向きを正規化して求める
    vec3 N = normalize(fragNormal);                // 法線
    vec3 L = normalize(-uDirLight.mDirection);     // 表面→光源

    // 反射色の計算
    // ベースとなる環境色を設定
    vec3 Lambert = uAmbientColor; // ka
    // N・Lが0より大きい場合
    float NdotL = dot(N, L);
    if (NdotL > 0)
    {
        // 拡散反射色を加える
        vec3 Diffuse = uDirLight.mDiffuseColor * NdotL; // kd * N・L
        Lambert += Diffuse;
    }
    // テクスチャ色 * 反射色
    outColor = texture(uTexture, fragTexCoord) * vec4(Lambert, 1.0f);
}
エレキベア
エレキベア
この話を聞いてから見ると分かってきたクマ
マイケル
マイケル
環境色や光源についての情報は基本的にRendererクラスで定義して設定
するようにしています。
bool Renderer::LoadData()
{

・・・略・・・

    // ライティングパラメータ設定
    mAmbientLight = Vector3(0.35f, 0.35f, 0.35f);
    mDirLightDirection = Vector3(0.3f, 0.3f, 0.8f);
    mDirLightDiffuseColor = Vector3(0.8f, 0.9f, 1.0f);

・・・略・・・

}
// ライティングuniform設定
void Shader::SetLightingUniform(const Renderer* renderer)
{
    switch (mType) {
・・・略・・・
        case ShaderType::LAMBERT:
            SetVectorUniform(UNIFORM_AMBIENT_COLOR, renderer->GetAmbientLight());
            SetVectorUniform(UNIFORM_DIR_LIGHT_DIRECTION, renderer->GetDirLightDirection());
            SetVectorUniform(UNIFORM_DIR_LIGHT_DIFFUSE_COLOR, renderer->GetDirLightDiffuseColor());
            break;
・・・略・・・
    }
}
エレキベア
エレキベア
光の原理が少しわかった気がして感動クマ〜〜
マイケル
マイケル
ちなみに今回は頂点ごとに法線座標を持たせていますが、
面ごとに均一な法線座標を持たせる方法 もあります。
その場合はポリゴンの境界がくっきり見える結果となるので、カクツキが欲しい場合にはそちらの方法も試してみましょう!

フォン反射モデル

マイケル
マイケル
そして最後はフォン反射モデル!
これは ランバート反射モデルに鏡面反射量を追加したモデル になります!
05 PHONG
エレキベア
エレキベア
妙につやつやしてるクマ〜〜〜
マイケル
マイケル
基本的な考え方もランバート反射モデルと同じですが、
加えて 光源から反射した向きRと頂点から視点までの向きV から鏡面反射量を計算します。
ScreenShot 2021 10 16 0 28 45 02
ScreenShot 2021 10 16 0 28 54 02
マイケル
マイケル
実装は下記のようになります。
#version 330

uniform mat4 uWorldTransform; // ワールド変換座標
uniform mat4 uViewProjection; // ビュー射影行列

layout(location = 0) in vec3 inPosition; // 位置座標
layout(location = 1) in vec3 inNormal;   // 法線座標
layout(location = 2) in vec2 inTexCoord; // UV座標

// テクスチャ情報
out vec2 fragTexCoord; // 位置座標
out vec3 fragNormal;   // 法線座標
out vec3 fragWorldPos; // ワールド座標

void main() {
    // w成分を加える
    vec4 pos = vec4(inPosition, 1.0);
    // クリップ空間座標に変換して設定
    gl_Position = uViewProjection * uWorldTransform * pos;

    // テクスチャ情報を設定
    fragTexCoord = inTexCoord;
    // ワールド座標に変換する
    fragNormal = (uWorldTransform * vec4(inNormal, 0.0f)).xyz;
    fragWorldPos = (uWorldTransform * pos).xyz;
}
#version 330

uniform sampler2D uTexture; // テクスチャ(自動で設定される)
uniform vec3 uCameraPos;    // カメラ座標
uniform float uSpecPower;   // 鏡面反射指数 a
uniform vec3 uAmbientColor; // 環境色 ka

// 平行光源
struct DirectionalLight
{
    vec3 mDirection;    // 向き
    vec3 mDiffuseColor; // 拡散反射色 kd
    vec3 mSpecColor;    // 鏡面反射色 ks
};
uniform DirectionalLight uDirLight;

in vec2 fragTexCoord; // 位置座標
in vec3 fragNormal;   // 法線座標
in vec3 fragWorldPos; // ワールド座標

out vec4 outColor;

void main() {
    // 計算に必要な向きを正規化して求める
    vec3 N = normalize(fragNormal);                // 法線
    vec3 L = normalize(-uDirLight.mDirection);     // 表面→光源
    vec3 V = normalize(uCameraPos - fragWorldPos); // 表面→視点
    vec3 R = normalize(reflect(-L, N));            // Nを軸としてLを反射させたもの

    // 反射色の計算
    // ベースとなる環境色を設定
    vec3 Phong = uAmbientColor; // ka
    // N・Lが0より大きい場合
    float NdotL = dot(N, L);
    if (NdotL > 0)
    {
        // 拡散反射色、鏡面反射色を加える
        vec3 Diffuse = uDirLight.mDiffuseColor * NdotL; // kd * N・L
        vec3 Specular = uDirLight.mSpecColor * pow(max(0.0, dot(R, V)), uSpecPower); // ks * (R・V)^a
        Phong += Diffuse + Specular;
    }
    // テクスチャ色 * 反射色
    outColor = texture(uTexture, fragTexCoord) * vec4(Phong, 1.0f);
}
エレキベア
エレキベア
ランバート反射モデルを理解していると簡単クマね
マイケル
マイケル
パラメータについても下記のように増やして設定してあげればOKです!
bool Renderer::LoadData()
{

・・・略・・・

    // ライティングパラメータ設定
    mAmbientLight = Vector3(0.35f, 0.35f, 0.35f);
    mDirLightDirection = Vector3(0.3f, 0.3f, 0.8f);
    mDirLightDiffuseColor = Vector3(0.8f, 0.9f, 1.0f);
    mDirLightSpecColor = Vector3(0.8f, 0.8f, 0.8f); // 追加

・・・略・・・

}
// ライティングuniform設定
void Shader::SetLightingUniform(const Renderer* renderer)
{
・・・略・・・
        case ShaderType::PHONG:
            SetVectorUniform(UNIFORM_CAMERA_POS, renderer->GetCamera()->GetPosition());
            SetVectorUniform(UNIFORM_AMBIENT_COLOR, renderer->GetAmbientLight());
            SetVectorUniform(UNIFORM_DIR_LIGHT_DIRECTION, renderer->GetDirLightDirection());
            SetVectorUniform(UNIFORM_DIR_LIGHT_DIFFUSE_COLOR, renderer->GetDirLightDiffuseColor());
            SetVectorUniform(UNIFORM_DIR_LIGHT_SPEC_COLOR, renderer->GetDirLightSpecColor());
            SetFloatUniform(UNIFORM_SPEC_POWER, mSpecPower);
            break;
    }
}
マイケル
マイケル
シェーダについての解説は以上になります!!
エレキベア
エレキベア
楽しかったクマ〜〜〜〜〜

おわりに

マイケル
マイケル
というわけでfbxモデルの読込とシェーダについて解説したけど
どうだったかな?
エレキベア
エレキベア
シェーダと聞くと難しそうなイメージだったクマが
やってみると案外理解できたクマ〜〜〜
マイケル
マイケル
シェーダいじるのは目に見える形で反映されるから楽しいよね
マイケル
マイケル
さて、これで座標変換から描画まで一通り3Dゲーム開発に必要な環境は整いました!
次はいよいよ3Dゲームを開発していくのでお楽しみに!!
エレキベア
エレキベア
苦労した分楽しみクマね
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!!
エレキベア
エレキベア
クマ〜〜〜〜〜〜

【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜 〜完〜

※続きはこちら!

コメント