だが、これを実際にRenderScriptで実装するのはどうやるんだろう?簡単に思いつくのは、Mesh.TriangleMeshBuilder()でテクスチャーを貼り付けるMeshを作成して、rsgDrawMesh()で描画する方法だ。
こいつのやり方はサンプルコード(RsRenderStates)にもあるが、以下のような手順になる。
1. Meshデータの作成(RsRenderStatesRS.java)
private Mesh getMbyNMesh(float width, float height, int wResolution, int hResolution) { Mesh.TriangleMeshBuilder tmb = new Mesh.TriangleMeshBuilder( mRS, 2, Mesh.TriangleMeshBuilder.TEXTURE_0 ); for (int y = 0; y <= hResolution; y++) { final float normalizedY = (float)y / hResolution; final float yOffset = (normalizedY - 0.5f) * height; for (int x = 0; x <= wResolution; x++) { float normalizedX = (float)x / wResolution; float xOffset = (normalizedX - 0.5f) * width; tmb.setTexture(normalizedX, normalizedY); tmb.addVertex(xOffset, yOffset); } } for (int y = 0; y < hResolution; y++) { final int curY = y * (wResolution + 1); final int belowY = (y + 1) * (wResolution + 1); for (int x = 0; x < wResolution; x++) { int curV = curY + x; int belowV = belowY + x; tmb.addTriangle(curV, belowV, curV + 1); tmb.addTriangle(belowV, belowV + 1, curV + 1); } } return tmb.create(true); }Mesh.TriangleMeshBuilder()を用いて、width x heightの領域を
wResolution x hResolutionに分割するMeshデータ(三角形)を生成します。
はじめにテクスチャーの分割設定をsetTexture()で行ない、次に分割したそれぞれの座標に対してaddVertex()で座標を追加してます。
座標は分割したテクスチャーの頂点座標を指定しますが、テクスチャーの中心が(0, 0)に来るような設定にします。
例えば、512x512のテクスチャーを4x4の領域に均等に分割したとすると、テクスチャーと座標の関係は下図のようになります。
次にaddTriangle()でMeshを構成する三角形を追加しますが、三角形の各頂点は座標を指定するのでは無くて、先程設定した頂点座標のインデックスを指定します。その際にインデックスの並びは下図のように、なぜか反時計回りになって居ないとダメなようです。
2. テクスチャーとなるBitmapの読み込み(RsRenderStatesRS.java)
private Allocation loadTextureRGB(int id) { return Allocation.createFromBitmapResource( mRS, mRes, id, Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE, Allocation.USAGE_GRAPHICS_TEXTURE ); } private void loadImages() { mTexOpaque = loadTextureRGB(R.drawable.data); mScript.set_gTexOpaque(mTexOpaque); }Allocation.createFromBitmapResource()を用いて、ResourceからBitmapデータのAllocationを作成し、RenderScript側のrs_allocationにバインドします。
3. 描画(renderstates.rs)
static void bindProgramVertexOrtho() { // Default vertex shader rsgBindProgramVertex(gProgVertex); // Vertex Programをバインド // Setup the projection matrix rs_matrix4x4 proj; // 平行投影領域の設定 rsMatrixLoadOrtho( &proj, 0, rsgGetWidth(), rsgGetHeight(), 0, -500, 500 ); // 投影領域をロード rsgProgramVertexLoadProjectionMatrix(&proj); } static void displayMeshSamples() { bindProgramVertexOrtho(); rs_matrix4x4 matrix; rsMatrixLoadTranslate(&matrix, 128, 128, 0); // 表示位置の設定 rsgProgramVertexLoadModelMatrix(&matrix); // Fragment shader with texture rsgBindProgramStore(gProgStoreBlendNone); rsgBindProgramFragment(gProgFragmentTexture); rsgBindSampler(gProgFragmentTexture, 0, gLinearClamp); rsgBindTexture(gProgFragmentTexture, 0, gTexOpaque); rsgDrawMesh(gMbyNMesh); // Meshの描画 rsgFontColor(1.0f, 1.0f, 1.0f, 1.0f); rsgBindFont(gFontMono); rsgDrawText("User gen 10 by 10 grid mesh", 10, 250); }ポイントとなるところだけ抜粋して解説すると、bindProgramVertexOrtho()で透視変換の設定を行ってますが、ここでは平行投影に設定してます。従って、Z軸の座標位置に限らず、オブジェクトはオブジェクトの大きさで描画されます。
後は、rsgDrawMesh()でテクスチャーをバインドしたMeshを描画しているのですが、例によってRenderScript関連の関数を以下にまとめておきます。
rsgBindProgramVertex() | Vertexシェーダーをバインド |
rsMatrixLoadOrtho() | 平行投影領域の設定 |
rsgProgramVertexLoadProjectionMatrix() | 座標行列を透視変換行列としてロード |
rsMatrixLoadTranslate() | 移動行列をロード |
rsgProgramVertexLoadModelMatrix() | 座標行列をモデルビューの行列としてロード |
rsgBindProgramStore() | ProgramStore(ハードウェアがフレームバッファにどのように描画を行うかを司る)をバインド |
rsgBindProgramFragment() | Fragmentシェーダーをバインド |
rsgBindSampler() | Sampler(データをどのようにテクスチャーバッファーに展開するかを司る)をバインド |
rsgBindTexture() | テクスチャーをバインド |
rsgFontColor() | 文字の色を設定 |
rsgBindFont() | フォントをバインド |
rsgDrawText() | 文字列を描画 |
ただ、このやり方の問題点は、Meshの頂点をRenderScript側からいじるためのAPIが(多分)無いので、RenderScript側でページめくりの座標演算が出来ないことだ。Meshのデータ構造がわかればRenderScriptからいじれるのかも知れないが、そんな情報はググッてもどこにも出てきやしないのさ。
だからと言って、頂点座標の演算をJavaコード側で行うのは何か違う気がする。なので、この方法は却下する。
■ページめくりエフェクトの実装
じゃー、他にやりようは無いのだろうか?実は、rsgDrawQuadTexCoords()という関数を使えばRenderScriptから頂点座標をいじれてテクスチャーを貼り付けられる矩形を描画することができる。ヘッダーファイルのコメント中には、パフォーマンスが低いから大量の描画には向かない旨が書かれてあるが、現状ではこれしか使えそうなAPIが無いので、今回はこれを使って実装してみた。
原理的にはすごく単純で、ページの画像Bitmapデータを複数の矩形に分割し、それぞれの矩形座標に対して、ページめくりの動きをつけて、分割したBitmapをテクスチャーとして矩形に貼りつけるだけだ。
1. Bitmapの分割(PageCurlRS.java)
private void loadImages(int id) { // もとのbitmapデータを作成 Bitmap b = BitmapFactory.decodeResource(mRes, id, mOptionsARGB); // bitmapを5つの領域に分割 mScript.set_gTex_00(Allocation.createFromBitmap(mRS, Bitmap.createBitmap(b, 0, 0, 198, 512), Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE, Allocation.USAGE_GRAPHICS_TEXTURE )); mScript.set_gTex_01(Allocation.createFromBitmap(mRS, Bitmap.createBitmap(b, 198, 0, 116, 512), Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE, Allocation.USAGE_GRAPHICS_TEXTURE )); mScript.set_gTex_02(Allocation.createFromBitmap(mRS, Bitmap.createBitmap(b, 314, 0, 82, 512), Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE, Allocation.USAGE_GRAPHICS_TEXTURE )); mScript.set_gTex_03(Allocation.createFromBitmap(mRS, Bitmap.createBitmap(b, 396, 0, 64, 512), Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE, Allocation.USAGE_GRAPHICS_TEXTURE )); mScript.set_gTex_04(Allocation.createFromBitmap(mRS, Bitmap.createBitmap(b, 460, 0, 52, 512), Allocation.MipmapControl.MIPMAP_ON_SYNC_TO_TEXTURE, Allocation.USAGE_GRAPHICS_TEXTURE )); // 分割したbitmapのAllocationを管理する為のstructure配列の領域作成 ScriptField_Bitmaps bitmap = new ScriptField_Bitmaps(mRS, 5); mScript.bind_bitmap(bitmap); }ページはめくる時に端っこの方が折れ具合が大きくなるので、均等に分割するんじゃなくて、端に行くほど細かく分割してやる(下図)。
2. 座標の演算(pagecurl.rs)
static void calcVertices(float degRot) { if (degRot<0) degRot=0; if (degRot>MAX_ROTATION) degRot=MAX_ROTATION; float r = -degRot * DEGREE; float rMod; // Calculate rotation amounts for each rectangle // and store in vStripRots // [A] Applies to all degrees rMod = boundary1Mod; // [B] Applies to all degrees > boundary1 if (degRot > boundary1) { // range: 0 to MAX_ROTATION - B1 float a = degRot - boundary1; // range: 0 to B2MOD a = a / (boundary2 - boundary1) * boundary2Mod; // range: B1MOD to B1MOD-B2MOD rMod -= a; } float vStripRots[SEGMENT_W + 1]; // Recursively multiply vStripRots elements by rMod for (int i = 0; i < (SEGMENT_W + 1); i++) { vStripRots[i] = r; r *= rMod; } // [C] Applies to degrees > boundary2. // Grow vStripRots proportionally to MAX_ROT. // (Note the 'additive' nature of these 3 steps). if (degRot >= boundary2) { for (int j = 0; j < (SEGMENT_W + 1); j++) { float diff = MAX_ROTATION*DEGREE - fabs(vStripRots[j]); // range: 0 to 30 float rotMult = degRot - boundary2; // range: 0 to 1 rotMult = rotMult / (MAX_ROTATION - boundary2); // range: __ to MAX_ROTATION vStripRots[j] -= diff * rotMult; } } // [2] Create myVertices[] for (int k = 0; k < (SEGMENT_W + 1) * (SEGMENT_H + 1) * 3; k = k + 3) { int idx = floor((float)((k/3) / (SEGMENT_H + 1))) - 1; myVertices[k] = (idx >= 0) ? vStripWidths[idx] : base_vertices[k]; myVertices[k + 1] = base_vertices[k + 1]; myVertices[k + 2] = base_vertices[k + 2]; } // [3] Apply rotation to myVerts[] for (int l = (SEGMENT_H + 1) * 3; l < (SEGMENT_W + 1) * (SEGMENT_H + 1) * 3; l = l + 3) { int idx2 = floor((float)((l/3) / (SEGMENT_H + 1))) - 1; myVertices[l] = cos(vStripRots[idx2]) * myVertices[l] - sin(vStripRots[idx2]) * myVertices[l + 2]; myVertices[l + 1] = myVertices[l + 1]; myVertices[l + 2] = cos(vStripRots[idx2]) * myVertices[l + 2] + sin(vStripRots[idx2]) * myVertices[l]; } // [4] 'connect' the rectangles for (int m = (SEGMENT_H + 1) * 2 * 3; m < (SEGMENT_W + 1) * (SEGMENT_H + 1) * 3; m = m + 3) { // (first 2 edges are fine) myVertices[m] += myVertices[m - (SEGMENT_H + 1) * 3]; myVertices[m + 2] += myVertices[m - (SEGMENT_H + 1) * 3 + 2]; } int i = 0; for (int x = 0; x < (SEGMENT_W + 1); x++) { vertices[x * 3 ] = myVertices[i++]; vertices[x * 3 + 1] = myVertices[i++]; vertices[x * 3 + 2] = myVertices[i++]; int pos = (SEGMENT_W + 1 + x) * 3; vertices[pos] = myVertices[i++]; vertices[pos + 1] = myVertices[i++]; vertices[pos + 2] = myVertices[i++]; } }ページめくりの座標演算に関しては、ここのblogにあるコードを参考にした。これはPapervision3Dでページめくりを実装した例だが、Flashにはこのような参考になるコードが山ほどあるので、非常にありがたいですよね。
3. 画面への描画(pagecurl.rs)
static void displayPageCurl() { // Vertexシェーダーの設定 rsgBindProgramVertex(gProgVertex); // 透視変換の設定(透視投影) rs_matrix4x4 proj; float aspect = (float)rsgGetWidth() / (float)rsgGetHeight(); rsMatrixLoadPerspective(&proj, 30.0f, aspect, 0.1f, 1500.0f); rsgProgramVertexLoadProjectionMatrix(&proj); // Fragmentシェーダーの設定 rsgBindProgramStore(gProgStoreBlendNone); rsgBindProgramFragment(gProgFragmentTexture); rsgBindProgramRaster(gCullNone); rsgBindSampler(gProgFragmentTexture, 0, gLinearClamp); // モデルビューの座標行列の設定 rs_matrix4x4 matrix; rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -1480.0f); rsMatrixRotate(&matrix, 0, 1.0f, 0.0f, 0.0f); // 回転しない場合でも // 指定しないとダメ rsgProgramVertexLoadModelMatrix(&matrix); // ページめくりの座標演算 calcVertices(gVx); // 分割した矩形の描画 Bitmaps_t *b = bitmap; for (int j = 0; j < SEGMENT_H; j++) { for (int i = 0; i < SEGMENT_W; i++) { rsgBindTexture(gProgFragmentTexture, 0, b->data); b++; rsgDrawQuadTexCoords( offset_x + vertices[i*3+j*(SEGMENT_W+1)*3], offset_y + vertices[i*3+j*(SEGMENT_W+1)*3+1], fabs(vertices[i*3+j*(SEGMENT_W+1)*3+2]), 0, 1, // テクスチャー左下 offset_x + vertices[i*3+(j+1)*(SEGMENT_W+1)*3], offset_y + vertices[i*3+(j+1)*(SEGMENT_W+1)*3+1], fabs(vertices[i*3+(j+1)*(SEGMENT_W+1)*3+2]), 0, 0, // テクスチャー左上 offset_x + vertices[(i+1)*3+(j+1)*(SEGMENT_W+1)*3], offset_y + vertices[(i+1)*3+(j+1)*(SEGMENT_W+1)*3+1], fabs(vertices[(i+1)*3+(j+1)*(SEGMENT_W+1)*3+2]), 1, 0, // テクスチャー右上 offset_x + vertices[(i+1)*3+j*(SEGMENT_W+1)*3], offset_y + vertices[(i+1)*3+j*(SEGMENT_W+1)*3+1], fabs(vertices[(i+1)*3+j*(SEGMENT_W+1)*3+2]), 1, 1 // テクスチャー右下 ); } } }ページめくりの効果を立体的に見せたいので、先程の例と違い、今回は透視変換は透視投影に設定しています。ここでの注意点は、モデルを回転させない場合でも、rsMatrixRotate()で回転の指定をしないとモデルが画面に描画されないことです(実際には回転させないので、角度0で設定します)。
始め、このことに気がつかなかったので、二週間くらい無駄にしましたよ・・・
矩形を描画するrsgDrawQuadTexCoords()の引数と実際の矩形、テクスチャーの関係は下図になります。
透視変換が平行投影と透視投影ではテクスチャーのマッピング方法が違う(透視投影の場合はテクスチャーの上下がひっくり返る)ような気がするが、気のせいかも知れない。あと、座標の指定する順番も何か描画に影響があるような気も?
■まとめ
と、まぁ、1つのページを複数の矩形に分割するという、かなりブルートフォースなやり方で無理やりRenderScriptでページめくりを実装してみたが、やはり王道は1つのMeshにページをテクスチャーとして貼りつけ、RenderScriptの中でページめくりの座標演算を行う方法だと思われる。
もし、このようなやり方、あるいは他にもっと良いやり方をご存知の方がいらっしゃれば、是非ご教授くださいまし。例によって、サンプルコードはここに置いてます。
最後に、動作させた時の動画以下に貼り付けておきます。
0 件のコメント:
コメントを投稿