2D描画系(その2)

ひとまず、もっとも需要の多い矩形スプライトの描画について考える。


以前の仕事では、もっと複雑なものも作ったことがあるが、描画に用いられるほとんどのものは矩形であるため、その描画効率を最大化するのは必須だ。この部分をおさえた上で、より複雑なものの描画(たとえば3Dモデル同様、ボーンと頂点ウェイトを持つ2Dモデルなど)と両立させる方法を考えていく。


GLES2*1を使う上で可能な限り回数を減らしたいものは描画の前処理、つまりテクスチャや頂点/インデックスバッファの有効化と、uniform転送、そして描画キック(glDrawElementsの呼び出し)だ。できれば一回の描画キックで多数のオブジェクトを描画してしまいたい。


そこで、こんな方法を考えてみる。

  1. あらかじめ、一回の描画キックで描画される最大数を取り決めておく。
  2. 個々のスプライトについて、使用するマトリクスを別途設けられたマトリクス構造体の1次元配列上に持つ。この配列上には複数のオブジェクトに対応するマトリクスが載っており、各オブジェクトはポインタで自身の使うマトリクス領域を保持する。
  3. 頂点バッファには、一辺が1.0の長さの正方形を原点基準で描画する頂点だけを並べる。また各頂点ごとに、描画に使用されるマトリクスのインデックスを4頂点を組として与える(例: 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2 …)
  4. インデックスバッファには、長方形を描いたら次の長方形描画にかかるようにインデックスを並べる(例: 0,1,2,3,3, 4,5,6,7,7, 8,9,10,11,11, …)*2
  5. Scenegraphの計算時、スケーリングマトリクス(縦、横のサイズをスケーリングする行列)に、ワールドマトリクスをかけたものを 2.で作った配列に格納する。
  6. 描画キック時、2.で作ったマトリクス配列を uniform に送る。各頂点はあらかじめ自分が参照するマトリクスが決め打ちで与えられているので、そのマトリクスを描画する番になれば、1.0x1.0サイズの矩形が、マトリクスで与えたサイズ、位置、回転を持った矩形となって描画される。

つまり、

\begin{equation} \begin{bmatrix} 1 & 1 & 1 \end{bmatrix} \begin{bmatrix} width & 0 & 0 \\ 0 & height & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} width & height & 1 \end{bmatrix} \end{equation}

となるので、これに回転/並行移動をあらわす行列をかけたものを与えてやれば良い、という発想だ。


このようにすれば、同じ配列上にマトリクスを持つ矩形は一回の描画キックでまとめて描画される。重要なのは頂点バッファ上では一辺の長さが1.0であればよ良いため、全く同じ頂点バッファを何度でも使いまわせる、という点だ。200pxの幅が欲しければ、横方向を200倍にスケーリングして描画すれば良いのだ。

行列はスケーリングと位置、回転をまとめてGPUに与えられる大変に便利な道具である。各スプライトの大きさ/位置/回転が行列の配列で与えられるのであれば、頂点バッファは一つだけあれば何回でも流用が効く。テクスチャとそのuvは別の配列uniformで用意して、同じインデックスで参照できるようにしてしまえば良い。


shaderにするとこんな感じだろうか。まずはvertex shader.

attribute highp vec2 a_vert;		// 座標は(0.0, 0.0)-(1.0, 1.0)の矩形として与えられる。
attribute mediump vec4 a_params;	// 何かパラメータがあればここに詰め込む。とりあえず x = 使用行列のindex.

uniform highp mat4 u_projection;	// 画面の座標系を反映するプロジェクションマトリクス。

uniform mat3 u_mat[32];		// 各スプライトのサイズ/位置/回転が最大32個まとめて与えられる。
uniform vec4 u_uv[32];		// uv値の左上と右下を正規化座標で与える。u_mat[] と同じインデックスで対応している。

varying vec2 v_uv;			// fragment shader に渡すuv値

void main(void)
{
	int idx = int(a_params.x);

	vec2 lt = vec2(u_uvsz[idx].x, u_uvsz[idx].y);	// 左上
	vec2 rb = vec2(u_uvsz[idx].z, u_uvsz[idx].w);	// 右下

	v_uv = (rb - lt) * a_vert + lt;
	gl_Position = u_projection * vec4(u_mat[idx] * vec3(a_vert, 1.0), 1.0)
}


次は fragment shader

precision mediump float;

uniform vec4 u_bright;		// 描画物全体に与えるRGBA係数。フェードイン/アウトなどに使用。
uniform vec4 u_rgba;		// 描画物固有のRGBA係数。

uniform sampler2D u_tex;	// 使用テクスチャ

varying vec2 v_uv;			// vertex shader から渡されたuv値

void main(void)
{
	gl_FragColor = texture2D(u_tex, v_uv) * u_rgba * u_blight;
}

…構想レベルではうまくいきそうな気がしている。
uniform で送る一群の配列はすべて同じテクスチャアトラスでなければならないのと、同じテクスチャであってもシェーダが定めた上限を超える分は複数回に分ける必要があるのと。あとは使われなくなり歯抜けになったマトリクスについては、縦横のスケールが0倍になるようにしておけば何も描画されなくなるはず。


細かなことを考え始めるとGLESのテクスチャは左下原点じゃないのか、とか、それも踏まえて座標系を左下原点で作った方がよくないか、とか色々出ては来るが、

*1:OpenGL ES 2.0

*2:GLES2では、GL_TRIANGLE_STRIPを描画する際に同じ頂点が連続して現れるとそこで三角形の連鎖を一旦打ち切り、また最初から描画を開始するという仕様がある。なので、矩形に必要な三角形二つをの描画が終わったら、最後の頂点をもう一度出してやればそこで一旦区切りをつけられる。もちろんインデックスバッファの転送は一回で済ませることができる。