クロの制作日記

初心者が頑張るゲームプログラミングC++の練習問題6(3Dグラフィックス)

はじめに

お久しぶりです。なんと前回の練習問題6の記事から約一年が経過してしまいました。この記事を参考にしていてくれていた方々には謝罪の意しかございません。

何でこんなに期間が空いてしまったのかというと、就活が忙しかったんですね。最初はこの本を参考にしたゲームを制作して企業にアピールしようとしていたのですが、色々と方向転換しまして、結局Unityで制作したゲームで乗り切りました。

それでも何だかんだで内々定を頂けたので、方向転換して良かったなと思いました。C++で制作すると絶対に間に合わなかったので。まあ、この辺りの詳しい話は長くなりそうなので、需要があればまた別の機会でお話しようと思います。

取り敢えずゲーム企業に何とか潜り込めそうなので、C++の勉強をする必要がでてちゃいました。なので、不定期にはなるかと思いますが、練習問題の解説記事を頑張って書いていきますので、応援よろしくお願いします。

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

以下の本でSDLOpenGLを用いたC/C++ゲーム開発の勉強を進めています。


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

各章には練習問題があるので、その問題の私なりの解答コードと詰まった箇所をここに残しておきます。


環境構築編はこちら
SDL_imageとOpenGLというライブラリを使うので、それの環境構築がまだな人は以下の記事から構築をしてください。
kurora-shumpei.hatenablog.com

練習問題1はこちら
kurora-shumpei.hatenablog.com

練習問題2はこちら
kurora-shumpei.hatenablog.com

練習問題3はこちら
www.kuroshum.com

練習問題4はこちら
www.kuroshum.com

練習問題5はこちら
www.kuroshum.com

あと、前回からシェーダーを弄りはじめましたが、VisualStudioくんはGLSLに関してはシンタックスハイライトを行ってくれません。その場合はMarketPlaceから追加のツールをダウンロードしてくる必要があります。以下のサイトにいいってDownloadできるので、追加することをおすすめします。
marketplace.visualstudio.com





練習問題の内容

第6章ではシェーダやメッシュなどを用いた3Dグラフィックスが主題となっています。

練習問題では,

  1. メッシュごとに別々のシェーダを描画する
  2. シーン上に最大4個の点光源を配置できるようにする

となっています。

ついに、3Dグラフィックスを存分に活用する段階になってきましたね。クォータニオンや座標の変換、メッシュなど、数学の要素が濃くなっていますが、頑張っていきましょう。

練習問題の解答

解答のコードは以下のGitHubリポジトリに置いています。
Star押してくれたらありがたいです。
github.com


ただ、解答のコードはC/C++初心者の私が作成したもので、これが正解だ!というわけではありませんし、他にもっと簡単かつ上手い実装方法があるということを念頭に置いた上で上記の解答コードを参考にしてくれたらありがたいです。

f:id:kurora-shumpei:20210421012330j:plain
リザルト画面

そういえば今まで肝心の練習問題の解説をしていなかった気がするので、今回からは頑張って解説しようかと思います。

練習問題6.1

この練習問題でやることは二つあります。

  1. メッシュごとに別々のシェーダで描画する
  2. 描画する際にシェーダごとに描画する

6章では単一のシェーダで全てのメッシュを描画していました。それを複数のシェーダを用いて描画を行うようにするのが1.です。また、シェーダを切り替えながら描画を行うのは効率が悪いので、メッシュごとにシェーダをグループに分けて、あるシェーダを使うメッシュを全て描画した後に別のシェーダを使うメッシュを描画を行うようにするのが2.です。

Renderer.h

まずは、複数のシェーダを扱えるようにするために

  • mMeshShaderのデータ型をshader* -> unordered_map

メッシュとシェーダを対応づけるために

  • mMeshCompsのデータ型をvector->map

に変更します。

std::map<std::string, class MeshComponent*> mMeshComps;
std::unordered_map<std::string, class Shader*> mMeshShaders;
Renderer::GetMesh

Renderer.cppのGetMesh関数も変更します。mMeshCompsのkeyに読み込むmeshファイル名を格納するのですが、ファイル名をそのままkeyに設定すると同じmeshファイル名を複数登録できなくなります。なので、meshファイル名の後ろに番号を付けて、同じmeshを複数mapに格納できるようにします。

ただ、同じmeshファイルとmeshcomponentをたくさん格納することになって、冗長な書き方な気がするので改善の余地ありです。あと、文字列を連結するやり方も気持ち悪いなあと思いながら書きました。C++でのスマートな文字列連結の書き方があればだれか教えてください。

Mesh* Renderer::GetMesh(const std::string & fileName, MeshComponent* mc)
{
	Mesh* m = nullptr;
	
	// ===========================================
	// 課題6.1

	// ファイル名に番号を付け加える
	//  mapでシェーダやメッシュコンポーネントを管理するためには、
	//   ファイル名を異なるものにする必要があるから
	std::string under_bar("_");
	std::string mMeshes_size(std::to_string(mMeshes.size()));
	std::string fileName_cnt = fileName.c_str() + under_bar + mMeshes_size;
	//std::string fileName_cnt = fileName + mMeshes_size;
	// ===========================================
	
	auto iter = mMeshes.find(fileName_cnt);
	if (iter != mMeshes.end())
	{
		m = iter->second;
	}
	else
	{
		m = new Mesh();
		if (m->Load(fileName_cnt, this, mc))
		{
			mMeshes.emplace(fileName_cnt, m);
		}
		else
		{
			delete m;
			m = nullptr;
		}
	}
	return m;
}
Mesh::Load

Renderer::GetMeshで呼んでいるMesh::Loadも変更します。Renderer::GetMeshでは番号付きのmeshファイル名を引数で渡していましたが、番号ついたままではファイル読み込みができません。なので、番号を外してあげます。

std::string fileName = renderer->split(fileName_cnt, '_', 1);

meshを読み込む際には様々な要素を読み込みます。その中でどのシェーダでmeshを描画するかの情報が存在するので、meshを読み込むついでにshaderも読み込みます。shaderを読み込む際にまた文字列連結を行っています。これは読み込むshaderをmMeshShadersに格納するときに、shaderとmeshの情報があると嬉しいからです。

std::string under_bar("_");
std::string shader_and_fileName_cnt = mShaderName + under_bar + fileName_cnt.c_str();

renderer->LoadMeshShaders(shader_and_fileName_cnt, mc);
Renderer::LoadMeshShaders

この関数は、LoadShaders関数にあったmeshshaderを読み込む部分をほとんど移植しただけですね。meshのロードをRenderer::GetMeshShadersに移植したのと、最後にMechCompsにmeshcomponentを追加するようにしています。

bool Renderer::LoadMeshShaders(std::string& shader_and_fileName_cnt, MeshComponent* mc)
{
	if (!GetMeshShaders(shader_and_fileName_cnt, mc))
	{
		return false;
	}

	// Set the view-projection matrix
	mView = Matrix4::CreateLookAt(Vector3::Zero, Vector3::UnitX, Vector3::UnitZ);
	mProjection = Matrix4::CreatePerspectiveFOV(Math::ToRadians(70.0f),
		mScreenWidth, mScreenHeight, 25.0f, 10000.0f);
	GetMeshShaders(shader_and_fileName_cnt, mc)->SetMatrixUniform("uViewProj", mView * mProjection);
	AddMeshComp(shader_and_fileName_cnt, mc);

	return true;
}
Renderer::GetMeshShader

この関数では、shader名の抽出と頂点シェーダとフラグメントシェーダーのパス設定を行い、シェーダの読み込みとmMeshShadersにシェーダの追加を行います。

Shader* Renderer::GetMeshShaders(const std::string& shader_and_fileName_cnt, MeshComponent* mc)
{
	Shader* s = nullptr;

 	std::string shaderName = split(shader_and_fileName_cnt, '_', 1);

	std::string shader_path("Shaders/");
	std::string vert_extension(".vert");
	std::string frag_extension(".frag");

	std::string vertName = shader_path + shaderName + vert_extension;
	std::string fragName = shader_path + shaderName + frag_extension;

	auto iter = mMeshShaders.find(shader_and_fileName_cnt);
	if (iter != mMeshShaders.end())
	{
		s = iter->second;
	}
	else
	{
		s = new Shader();
		if (s->Load(vertName, fragName))
		{
			s->SetActive();
			mMeshShaders.emplace(shader_and_fileName_cnt, s);
		}
		else
		{
			delete s;
			s = nullptr;
			
		}
	}

	return s;
}
Renderer::Draw

今まで設定したmMeshCompsとmMeshShadersを用いてメッシュを描画していきます。

Shader* s;
for (auto mc : mMeshComps)
{
	s = mMeshShaders.at(mc.first);
	// Set the mesh shader active
	s->SetActive();
	// Update view-projection matrix
	s->SetMatrixUniform("uViewProj", mView * mProjection);
	// Update lighting uniforms
	SetLightUniforms(s);

	mc.second->Draw(s);
}

練習問題6.2

この練習問題ではシーン上に最大4個の点光源を配置できるようにします。こちらではcppファイルとfragファイルを変更していきます。

Renderer.h

環境光源と同じように点光源も構造体を設定して、配列も用意します。

struct PointLight
{
	// 光源の位置
	Vector3 mPosition;
	// 拡散反射色
	Vector3  mDiffuseColor;
	// 鏡面反射色
	Vector3 mSpecColor;
	// 鏡面反射指数
	float mSpecPower;
	// 影響半径
	float mRadius;
};

PointLight* mPointLight;
Game::LoadData

この関数では様々なデータを読み込みます。その中で点光源に関する設定(点光源の数や構造体メンバーの数値)を行います。

// 点光源の数を指定する
mRenderer->SetPointLightNum(4);
// 点光源の変数を取得
PointLight* point = mRenderer->GetPointLight();
// 点光源の設定を行う
for (int i = 0; i < 4; i++)
{
	point[i].mPosition = Vector3(200.0f, 0.0f + 200 * i, 50.0f);
	point[i].mDiffuseColor = Vector3(0.78f, 0.88f, 1.0f);
	point[i].mSpecColor = Vector3(0.8f, 0.8f, 0.8f);
	point[i].mSpecPower = 10.0f;
	point[i].mRadius = 200.0f;
}
Renderer::SetLightUniforms

用意した点光源それぞれに設定した値をフラグメントシェーダーに渡します。シェーダーに値を渡す際には、シェーダー側の変数名の文字列を指定しないといけないのが面倒ですね。しかも、今回はシェーダー側の変数も配列になっているので、インデックスを含めた文字列を作成しないといけない。上手いことchar*変数のみで連結しようかと思ったのですが、結局stringに変換するなどをして対応しました。この辺りも上手い方法があれば教えてほしいですね。

void Renderer::SetLightUniforms(Shader* shader)
{
	// Camera position is from inverted view
	Matrix4 invView = mView;
	invView.Invert();
	shader->SetVectorUniform("uCameraPos", invView.GetTranslation());
	// Ambient light
	shader->SetVectorUniform("uAmbientLight", mAmbientLight);
	// Directional light
	shader->SetVectorUniform("uDirLight.mDirection",
		mDirLight.mDirection);
	shader->SetVectorUniform("uDirLight.mDiffuseColor",
		mDirLight.mDiffuseColor);
	shader->SetVectorUniform("uDirLight.mSpecColor",
		mDirLight.mSpecColor);

	// ===========================================
	// 課題6.2

	std::string tmp_uniform_name;
	for (int i = 0; i < 4; i++)
	{
		tmp_uniform_name = format("uPointLight[", "].mPosition", std::to_string(i));
		shader->SetVectorUniform(tmp_uniform_name.c_str(), mPointLight[i].mPosition);
		
		tmp_uniform_name = format("uPointLight[", "].mDiffuseColor", std::to_string(i));
		shader->SetVectorUniform(tmp_uniform_name.c_str(), mPointLight[i].mDiffuseColor);
		
		tmp_uniform_name = format("uPointLight[", "].mSpecColor", std::to_string(i));
		shader->SetVectorUniform(tmp_uniform_name.c_str(), mPointLight[i].mSpecColor);
		
		tmp_uniform_name = format("uPointLight[", "].mSpecPower", std::to_string(i));
		shader->SetFloatUniform(tmp_uniform_name.c_str(), mPointLight[i].mSpecPower);

		tmp_uniform_name = format("uPointLight[", "].mRadius", std::to_string(i));
		shader->SetFloatUniform(tmp_uniform_name.c_str(), mPointLight[i].mRadius);
	}
	// ===========================================

}
Renderer::format

この関数は先ほどお話したRenderer::SetLightUniformsでシェーダー側に値を渡す際に行った文字列連結の部分ですね。この書き方で良いのか疑問ですが、私にはこれが限界でした。

std::string Renderer::format(const char* chars1, const char* chars2, std::string str3)
{
	std::string str1(chars1);
	std::string str2(chars2);

	std::string str11 = str1 + str3 + str2;

	return str11;

	/* char配列の結合を試したけどエラー出て諦めた
	const char* str33 = str3.c_str();

	//int len = strlen(str1) + strlen(str2) + strlen(str33);
	int len = std::strlen(str1) + std::strlen(str2) + std::strlen(str33);
	char* buf = new char[len];
	
	strcpy_s(buf, std::strlen(buf)+1, str1);

	
	strcat_s(buf, sizeof(buf), str33);
	strcat_s(buf, sizeof(buf), str2);
	
	return buf;
	*/
}
Phong.frag

Renderer.hと同じように点光源の構造帯と配列を定義します。配列はuniformになっているので、cppファイル側からデータを受け取ることができます。

// 点光源用の構造体
struct PointLight
{
	// 光源の位置
	vec3 mPosition;
	// 拡散反射色
	vec3  mDiffuseColor;
	// 鏡面反射色
	vec3 mSpecColor;
	// 鏡面反射指数
	float mSpecPower;
	// 影響半径
	float mRadius;
};

// 点光源
uniform PointLight uPointLight[4];

こちらは点光源の処理を行っています。基本的には平行光源と同じです。異なるところは二点あります。

  1. 影響半径外に処理を行わないようにした
  2. 光のベクトルの変数を削除した

平行光源で使用していた光のベクトルは全て点光源と物体のベクトルで置き換えました。この実装でもっともらしい結果が得られたので問題ないとは思いますが、間違えていたらご指摘いただけると嬉しいです。

// Surface normal
vec3 N = normalize(fragNormal);
vec3 Phong = uAmbientLight;

for(int i = 0; i < 4; i++)
{
	float distnce = distance(uPointLight[i].mPosition, fragWorldPos);
	// Vector from surface to light
	//vec3 L_point = normalize(-uPointLight.mDirection);
	// Vector from surface to camera
	vec3 V_point = normalize(uPointLight[i].mPosition - fragWorldPos);
	// Reflection of -L about N
	vec3 R_point = normalize(reflect(-V_point, N));

	// Compute phong reflection
	
	float NdotL = dot(N, V_point);
	if (NdotL > 0 && distnce < uPointLight[i].mRadius)
	{
		vec3 Diffuse = uPointLight[i].mDiffuseColor * NdotL;
		vec3 Specular = uPointLight[i].mSpecColor * pow(max(0.0, dot(R_point, V_point)), uSpecPower);
		Phong += Diffuse + Specular;
	}
}



個人的に詰まったとこ

連想配列のキーの型

結局使わなかったけど、連想配列のキーにはvector型を突っ込めるらしい。
vivi.dyndns.org

文字列操作

今回は文字列操作に一番悩まされました。結局stringで押し切りましたが、色々調べたサイトがあるので、載せておきます。
itsakura.com
qiita.com
marycore.jp

std::format

これが使えたら楽だったのに、C++20の機能だったので使えなかった。悲しい。
qiita.com

strlenとstrsizeの違い

この辺りも完全に忘れていた。Pythonに侵されている気がする。
edu.clipper.co.jp

ポインタを返す関数

関数内のローカル変数のポインタを返そうとしておかしなことになっていました。完全にこの仕様を忘れていた。
qiita.com

最後に

今回はChapter6の練習問題を扱いました。

一年ぶりの更新ということで色々頑張りました。全然C++に触ってこなかった弊害が出てきているなあと感じることが多かったですね。絶対もっとスマートに書く方法があるはずだ!と思いながら、でも進めないといけないよな...とモヤモヤしながらコードを書くのは中々キツイものがありますね。

今は割とやる気があるので、頑張って一ヶ月ぐらいで更新できたらなと思います...。

ではまた次回に...。