2011/08/15

RenderScriptでページめくりエフェクト

RenderScriptを使ったアプリで代表的なのがGoogle謹製のBooksとYouTubeアプリらしい。例えば、Booksに関して言うと、恐らくページめくりの部分に使っているんだろうなぁということは容易に想像できますよね。

だが、これを実際に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 件のコメント:

コメントを投稿