
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
クマ〜〜〜〜

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

↑本記事での解説範囲

エレキベア
シェーダは聞いたことあるクマ〜〜〜

マイケル
サンプルプロジェクトは前回と同じもので、
4種類のシェーダで3Dモデルを描画するサンプルになっています。
4種類のシェーダで3Dモデルを描画するサンプルになっています。

↑4種類のシェーダを実装している

マイケル
それでは早速進めていきましょう!
参考書籍

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

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

エレキベア
どの本も広く網羅されているから
おすすめクマ〜〜〜〜
おすすめクマ〜〜〜〜
使用したモデル

マイケル
書籍ではgpmeshファイルを読み込む方向で記載されていましたが、
やはり fbxファイル の方が馴染みがあるので、こちらを読み込むよう実装していこうと思います!
やはり fbxファイル の方が馴染みがあるので、こちらを読み込むよう実装していこうと思います!

マイケル
というわけで3D読込で使用したサンプルモデルは以下になります。
Blenderで作成した立方体にサイコロテクスチャを貼っただけの
シンプルなfbxモデル です。
Blenderで作成した立方体にサイコロテクスチャを貼っただけの
シンプルなfbxモデル です。

↑Blenderで作成

↑サイコロのテクスチャ

マイケル
こちらもプロジェクト内に含めているので、
ご自由にお使いください!
ご自由にお使いください!

エレキベア
これはクマでも作れるクマ〜〜〜
fbxsdkでのロード処理

マイケル
今回fbxモデルを読み込むにあたって、
AutoDesk社が提供している fbxsdk というライブラリを使用させていただきました!
AutoDesk社が提供している fbxsdk というライブラリを使用させていただきました!

マイケル
インストール方法については省略しますが、
基本的にインストールしてパスを通すだけで使えるかと思います!
基本的にインストールしてパスを通すだけで使えるかと思います!

エレキベア
準備完了クマ〜〜〜〜
最終的な座標の形式

マイケル
まず最終的に座標をどのように設定したいかについてですが、
下記のようにVertexArrayクラスに 頂点バッファとインデックスバッファ を渡して
シェーダに設定する形となります。
下記のように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セットとして渡す形にします。
位置座標、法線座標、UV座標 も必要となります。
そのため頂点バッファは下記のように、8つで1セットとして渡す形にします。

↑最終的に位置座標、法線座標、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() で頂点座標を取得することができます。
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);
}
}

マイケル
ここまで読み込んだ情報は下記のようになります。

↑頂点座標とインデックス座標の対応

エレキベア
ここまでは余裕クマね
法線座標、UV座標の読込

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

エレキベア
これで完了クマね〜〜〜〜
案外ちょろかったクマ
案外ちょろかったクマ

マイケル
そう、これで全ての座標が取得できたからそのまま渡せば完了!
・・・と思いきや・・・
・・・と思いきや・・・

↑崩壊したサイコロ達

マイケル
そのまま設定してしまうとこのようにモデルが崩壊してしまいます・・・

エレキベア
なんてことだクマ・・・・

マイケル
というのも法線、UV座標に関しては下記のように、
同じインデックス座標でも異なる座標のものがある からなんですね・・・。
同じインデックス座標でも異なる座標のものがある からなんですね・・・。

↑インデックス座標と法線・UV座標の対応

↑同じ頂点で異なるものがあるため、法線・UV座標が上書きされてしまう

エレキベア
これは困ったクマ〜〜〜・・・・

マイケル
そのため、今回設定する形に直すには、
法線、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;
}

マイケル
これにより、振り直されたインデックス座標は下記のようになります。

↑インデックス座標を振り直した結果

↑振り直されたインデックス座標

マイケル
これを並び替えると、下記のように
1つの頂点について3つの頂点バッファがある状態になります。
1つの頂点について3つの頂点バッファがある状態になります。


エレキベア
これは立方体だからクマね

マイケル
これで必要な座標情報は一通り揃いました!
シェーダの描画

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

マイケル
そしてシェーダは大きく
・頂点シェーダ
・フラグメントシェーダ
の2つに分かれており、それぞれ座標の計算と見た目の調整を行います!
・頂点シェーダ
・フラグメントシェーダ
の2つに分かれており、それぞれ座標の計算と見た目の調整を行います!

↑シェーダの描画プロセス

エレキベア
いよいよ後は描画するだけクマね

マイケル
今回はサンプルプロジェクトでも実装した、
基本となる下記4種類のシェーダを紹介します!
基本となる下記4種類のシェーダを紹介します!

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

エレキベア
楽しみクマ〜〜〜
単色描画

マイケル
まずは単色での描画について!
今回は下記のように青色一色で描画しています!
今回は下記のように青色一色で描画しています!


エレキベア
これは簡単そうクマね

マイケル
受け取った座標情報を元に 頂点シェーダ内でクリップ座標まで変換 して、
フラグメントシェーダ内で出力色を指定 しているだけになるね!
フラグメントシェーダ内で出力色を指定 しているだけになるね!
#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要素にする必要があるくらいです。
1つ注意点をあげるとすれば、座標計算の前には vec4(inPosition, 1.0) というように
w成分を足して4要素にする必要があるくらいです。

エレキベア
はやく次にいくクマ〜〜〜
スプライト描画

マイケル
次はテクスチャを貼って描画したものになります。
ここからUV座標を使用しますが、法線座標についてはまだ使用しません。
ここからUV座標を使用しますが、法線座標についてはまだ使用しません。


エレキベア
3Dモデルっぽくなってきたクマね

マイケル
頂点シェーダでの計算は同じですが、受け取ったUV座標について
フラグメントシェーダに渡すようにしています。
フラグメントシェーダでは 受け取った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ライブラリ を使用しました。
描画前に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);
}

エレキベア
ここまでもまあ簡単クマね
ランバート反射モデル

マイケル
そして次はランバート反射モデル!
下記のように光の反射を考慮したモデルになります。
下記のように光の反射を考慮したモデルになります。


エレキベア
なんか難しそうなのが来たクマ・・・

マイケル
まず物がどのようにして見えているのかだけど、
これは 光源から出た光がいろんなところに反射して最終的に目に入ることで
見えていることになります。
影の中のものがうっすら見えるのも、周りの光がある程度反射しているためなのです。
これは 光源から出た光がいろんなところに反射して最終的に目に入ることで
見えていることになります。
影の中のものがうっすら見えるのも、周りの光がある程度反射しているためなのです。

↑光は反射しまくっている

マイケル
しかしこの反射を全て考慮してしまうと、計算負荷がとんでもないことになってしまう・・・。
そのためN次的な反射は考慮せずに、光源からの光が当たる量のみを考慮したモデルを
ランバート反射モデルといいます。
当たった光が全方位に拡散するという特性から 拡散反射モデル とも呼ばれます。
そのためN次的な反射は考慮せずに、光源からの光が当たる量のみを考慮したモデルを
ランバート反射モデルといいます。
当たった光が全方位に拡散するという特性から 拡散反射モデル とも呼ばれます。

エレキベア
それなら計算量はがくっと減りそうクマね

マイケル
計算方法としては、
頂点からの法線をN、頂点から光源へ向かうベクトルをL とした場合、
下記の計算で表すことができます。
頂点からの法線をN、頂点から光源へ向かうベクトルをL とした場合、
下記の計算で表すことができます。



マイケル
全てのオブジェクトに均一に当たる光(環境色)を最低値として設定し、
そこに 光の拡散反射量を加えたもの を反射量とする考え方です!
そこに 光の拡散反射量を加えたもの を反射量とする考え方です!

マイケル
具体的な実装は下記になります。
ここから法線座標も使用して計算することになります。
ここから法線座標も使用して計算することになります。
#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;
・・・略・・・
}
}

エレキベア
光の原理が少しわかった気がして感動クマ〜〜

マイケル
ちなみに今回は頂点ごとに法線座標を持たせていますが、
面ごとに均一な法線座標を持たせる方法 もあります。
その場合はポリゴンの境界がくっきり見える結果となるので、カクツキが欲しい場合にはそちらの方法も試してみましょう!
面ごとに均一な法線座標を持たせる方法 もあります。
その場合はポリゴンの境界がくっきり見える結果となるので、カクツキが欲しい場合にはそちらの方法も試してみましょう!
フォン反射モデル

マイケル
そして最後はフォン反射モデル!
これは ランバート反射モデルに鏡面反射量を追加したモデル になります!
これは ランバート反射モデルに鏡面反射量を追加したモデル になります!


エレキベア
妙につやつやしてるクマ〜〜〜

マイケル
基本的な考え方もランバート反射モデルと同じですが、
加えて 光源から反射した向きRと頂点から視点までの向きV から鏡面反射量を計算します。
加えて 光源から反射した向きRと頂点から視点までの向きV から鏡面反射量を計算します。



マイケル
実装は下記のようになります。
#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ゲームを開発していくのでお楽しみに!!
次はいよいよ3Dゲームを開発していくのでお楽しみに!!

エレキベア
苦労した分楽しみクマね

マイケル
それでは今日はこの辺で!
アデュー!!!
アデュー!!!

エレキベア
クマ〜〜〜〜〜〜
【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜 〜完〜
※続きはこちら!
コメント