旧OpenGLのマトリックス操作系APIは自分で作ってしまおう

やあ子供たち。一番寒い季節がやってきたな。元気にやっているかい。年賀状は全部返したか。今日は泣く子も黙るMatrixHelperの紹介だよ。
OpenGL1は機能制限があったが多くのことを僕たちに教えてくれた。
glPushMatrix(), glPopMatrix()といった、3次元空間でのマトリックス演算系APIもその一つだ。
しかしながら、最新のOpenGLではマトリックス演算というものは実際にはGPUに渡すシェーダープログラムの中で行われるべしというふうになっているよ。なので上述のマトリックス演算系APIもDeprecated扱いとなり、悉く駆逐されてしまっています。
古きよき、glPush/PopMatrix()はもういません。私たちは、何かマトリクス演算をしたければ、そのマトリクスをシェーダーのUniform変数として、あるいはUniformBufferObjectとしてGPUの中のシェーダープログラムに転送し、GPUの中で、それを使って計算させる、そういうシェーダーを書く、という手続きを踏むことが前提の設計となっています。
ではGPUに転送するマトリックスはどう管理・保持すればいいのでしょうか。glPush/PopMatrix()を駆使してコードを書きながら3次元形状の階層構造を構築していくあの感覚は非常に重要な体験でした。3次元グラフィックスにおいて、モデル部品間に親子関係、包含関係を持たせることはやはり便利というか、必須です。だからこそ上述のAPIがいたのに、それがもう使えないとあれば、、もう作るしかない!
そう、そこで今回のMatrixHelperの登場となります。以下にMatrixHelperのコードを示します。

template< class Func >
class MatrixHelper
{
public:
	MatrixHelper(Func func)
		:_func(func)
	{
		// スタックのデフォルト基底階層に単位行列をロードしておく
		_matrix_stack.push(glm::mat4(1.0));
	}
	// matrix support
	void LoadIdentity(void) { LoadMatrix(glm::mat4(1.0)); }
	void LoadMatrix(const glm::mat4& mat)
	{
		_matrix_stack.top() = mat;
		// モデルマトリックス送信
		//glUniformMatrix4fv(_uniform_modelmatrix_id, 1, 0,
		//	glm::value_ptr(_matrix_stack.top()));
		_func(_matrix_stack.top());
		return;
	}
	void MultMatrix(const glm::mat4& mat)
	{
		_matrix_stack.top() *= mat;
		// モデルマトリックス送信
		//glUniformMatrix4fv(_uniform_modelmatrix_id, 1, 0,
		//	glm::value_ptr(_matrix_stack.top()));
		_func(_matrix_stack.top());
		return;
	}
	void PushMatrix(void)
	{
		_matrix_stack.push(_matrix_stack.top());
		return;
	}
	void PopMatrix(void)
	{
		_matrix_stack.pop();
		// モデルマトリックス送信
		//glUniformMatrix4fv(_uniform_modelmatrix_id, 1, 0,
		//	glm::value_ptr(_matrix_stack.top()));
		_func(_matrix_stack.top());
	}
	const glm::mat4& GetTopMatrix(void)
	{
		return _matrix_stack.top();
	}
public:
	std::stack< glm::mat4 > _matrix_stack;
	Func _func;
};

はい見てわかるように、これは単なるglm::mat4のスタックを、旧GLAPIライクに操作できるようにしたものです。しかし、そればかりではありません。スタックの最上位にあるマトリックスの内容が変化した場合は、同マトリックスを引数にもつ「処理」を呼び出します。この「処理」には。任意のものを定義可能です。ここでは、スタック最上位のマトリックスを、GPUに転送するような処理を設定してあげます。(コメントにある、glUniformXXX()呼び出しを参考としてください。)
こうすることで、PopMatrix()や、MultMatrix()などの、スタック最上位マトリックスが更新されるような操作を行った際、確実にGPUに新しいスタック最上位マトリックスが転送されるようになります。(Pushで更新用の_funcを呼んでいないのは、最上位マトリックスと同じものが複製されて上に乗っかるのみなので、更新処理は実質必要ないためです。)
こうして、あたかも、旧GLのマトリックスAPIを利用しているかのような、以下のようなコードが書けるようになります。まずはMatrixHelperの定義例から見ていきます。

class LambdaForMatrixHelper
{
public:
	LambdaForMatrixHelper(const int id_ubo, const glm::mat4* prj, const glm::mat4* viw)
		:_id_ubo(id_ubo)
		,_pprj(prj)
		,_pviw(viw)
	{};
	void operator()(glm::mat4& mdl)
	{
		vector< float > buf;
		{
			const auto& prj = *_pprj;
			const auto& viw = *_pviw;
			//
			const float *p = glm::value_ptr(prj);
			buf.insert(buf.end(), p, p + 16);
			const float *r = glm::value_ptr(viw);
			buf.insert(buf.end(), r, r + 16);
			const float *q = glm::value_ptr(mdl);
			buf.insert(buf.end(), q, q + 16);
		}
		GLuint bindingPoint = 1;
		glBindBuffer(GL_UNIFORM_BUFFER, _id_ubo);
		glBufferData(GL_UNIFORM_BUFFER, buf.size() * sizeof(float), buf.data(), GL_DYNAMIC_DRAW);
		glBindBufferBase(GL_UNIFORM_BUFFER, bindingPoint, _id_ubo);
		return;
	};
public:
	int _id_ubo;
	const glm::mat4* _pprj, *_pviw;
};

// ここがテンプレートの実体化
typedef MatrixHelper< LambdaForMatrixHelper > MyMatrixHelper;

はい、いかがだろうか。ここでは、投影行列やビュー行列、モデル行列といった、いわゆる3次元CGにおける3大マトリックス、およびそれらをまとめて管理するUBOのID、といった情報を管理および、glUniformXXX()転送することができる関数オブジェクトを定義し、それを、MatrixHelperの、更新処理として定義しています。とはいえ、operator()の引数を見るとわかるように、このMatrixHelperは、モデル行列のみを、階層構造管理の対象にしています。(ま普通、ビュー行列や投影行列をそれ単体で階層構造管理することはないですので)。
これを使った実装例が、以下になります。これは、階層モデリングをサポートするモデルクラスの描画関数の例です。

void MyModel::DrawModel( MatrixHelper& mh )
{
        // ...
	mh.PushMatrix();
	mh.MultMatrix(this->_matrix);
	mh.PushMatrix();
	mh.MultMatrix(this->_private_matrix);
	{
          // ここでシェーダー有効化、テクスチャ有効化など
      // ここでモデルを描画(VAAHなど使い)
        }
	mh.PopMatrix();
        // 子モデルの描画
	for(auto ich : this->getChildren())
	{
		ich->DrawModel(mh);// ま再帰呼び出しはあまりよくないが
	}// ich
	mh.PopMatrix();

VAAHについては、こちらの過去日記を参照ください。
それでは今日もこの辺で。チャオ!