ラベル RenderScript の投稿を表示しています。 すべての投稿を表示
ラベル RenderScript の投稿を表示しています。 すべての投稿を表示

2011/08/21

RenderScriptでカルーセルエフェクト

前回のエントリーでRenderScriptを使ってページめくりの効果を実装してみました。となれば、次はもちろんYouTubeアプリのカルーセル表示ですよね。

既に、3D空間にテクスチャーを貼りつけた矩形を表示する方法はわかりました。なので、これも比較的簡単に実装できそうですね。

■カルーセル表示の実装
今回は、円周上に10面の矩形を並べてグルグル回して遊んでみましょう。前回のエントリーと基本的なところは変わらないので、RenderScriptの中身だけ見ていきます。

1. 矩形の座標を求める(carousel.rs)
10面の矩形を円周上に並べるということは、円に内接する正十角形の座標を求めれば良いですね。ということで、公式通り下図のように求めます。


 コードは以下の部分が該当しますが、これは演算済みのものを初期値として持たせておいても構わないですね。まぁ、このコードも初期化時の1回しか呼ばれないので、大した負荷にはならないとは思いますが。
static void initBitmaps() {
    // ビットマップのアドレスを格納
    Bitmaps_t *b = bitmap;
    b->data = gTex_00; b++;
    b->data = gTex_01; b++;
    b->data = gTex_02; b++;
    b->data = gTex_03; b++;
    b->data = gTex_04; b++;
    b->data = gTex_05; b++;
    b->data = gTex_06; b++;
    b->data = gTex_07; b++;
    b->data = gTex_08; b++;
    b->data = gTex_09; b++;

    // 正十角形の一辺の長さを得る
    len = RADIUS * 2 * sin(M_PI/NUM_ITEMS);

    // 矩形の座標を求める
    float angle;
    for (int i = 0; i < NUM_ITEMS; i++) {
        angle = i * 360 / NUM_ITEMS;
        vertices[i*3] = sin(angle * M_PI / 180) * RADIUS;
        vertices[i*3 + 1] = 0;
        vertices[i*3 + 2] = -cos(angle * M_PI / 180) * RADIUS;
    }
}
2. カメラを配置(carousel.rs) 矩形を空間に配置したら、後はカメラを配置すれば良いです。これはモデルビューの座標を設定するところでやればいいですね。
static void displayCarousel() {    
 
    ※コードは該当部分だけ抜粋してます

    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f); // カメラ位置
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f);
    rsgProgramVertexLoadModelMatrix(&matrix);

}
この辺りの値は、並べる矩形の数や円周の半径の大きさにもよるので、トライ&エラーで合わせ込む他は無いんでしょうかね?本当は演算で求められたらすっきりするのでしょうが。

3. 矩形の回転(carousel.rs)
矩形の回転は矩形自体を円周に沿って移動させるのでは無くて、単純にカメラをY軸方向に回転させれば同じ効果を得られます。

ので、こちらもモデルビューの座標を設定する部分でカメラの向きを指定するようにします。
static void displayCarousel() {    

    ※コードは該当部分だけ抜粋してます
    
    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f);
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f); // カメラを回転
    rsgProgramVertexLoadModelMatrix(&matrix);

}
ユーザーがフリックした場合、はじめは勢い良く回って徐々に減速して止めたいので、描画毎に回転する角度を徐々に小さくするような演算を行ってます。
static void displayCarousel() {    

    ※コードは該当部分だけ抜粋してます

    // カメラの回転角度を演算
    // gVxがフリックの強さ
    if (gVx != 0) {
        rot = rot + gVx;
        gVx = gVx * 0.95;
        if (fabs(gVx) < 0.1) {
            gVx = 0;
        }
    }
    
    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f); // カメラ位置
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f); // カメラを回転
    rsgProgramVertexLoadModelMatrix(&matrix);

}
また、カメラは正面を向いた状態では矩形の方を向いていませんので、下図のように18°(18 = 360÷10÷2)だけ回転させたものを初期値として持たせます。


最後に、矩形描画に関する部分のすべてのコードを以下に載せて置きます。
#define NUM_ITEMS 10
static int rot = 360/NUM_ITEMS/2; // カメラの角度の初期値
static void displayCarousel() {
    // 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);

    // カメラの回転角度を演算
    // gVxがフリックの強さ
    if (gVx != 0) {
        rot = rot + gVx;
        gVx = gVx * 0.95;
        if (fabs(gVx) < 0.1) {
            gVx = 0;
        }
    }
    
    // モデルビューの座標行列の設定
    rs_matrix4x4 matrix;
    rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f); // カメラ位置
    rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f); // カメラの回転
    rsgProgramVertexLoadModelMatrix(&matrix);

    // 矩形の描画
    Bitmaps_t *b = bitmap;
    for (int i = 0; i < 10; i++) {
        rsgBindTexture(gProgFragmentTexture, 0, b->data);
        rsgDrawQuadTexCoords(
            vertices[i*3],
            -(len/2),
            vertices[i*3+2],
            0,1,
            vertices[i*3],
            len/2,
            vertices[i*3+2],
            0,0,
            vertices[i == 9 ? 0 : (i+1)*3],
            len/2,
            vertices[i == 9 ? 0 + 2 : (i+1)*3 + 2],
            1,0,
            vertices[i == 9 ? 0 : (i+1)*3],
            -(len/2),
            vertices[i == 9 ? 0 + 2 : (i+1)*3 + 2],
            1,1
        );
        b++;
    }
}
■まとめ
上記の通り、比較的簡単にAndroidタブレットで使われているカルーセル表示を実装することが出来ました。今回、実装するにあたっては、clockmakerさんのPapervision3D本を参考にさせていただいてます。

Flashには昔からこのような3Dのエフェクトを多用したテクニックが確立されてますので、RenderScriptでクールなエフェクトを用いたUIを作る際に参考にできる情報が沢山あると思います。

また、はじめからRenderScriptでゴリゴリ実装して思った動作にならなかった場合は、デバッグが非常にやりずらいので、予めPapervision3Dとかを使って試作するなどして勘所を掴んでおけば、デバッグもスムーズに進むかも知れません。

では、いつものようにコードはここに置いておきます。また、実行時の動画も以下に貼りつけておきます。


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の中でページめくりの座標演算を行う方法だと思われる。

もし、このようなやり方、あるいは他にもっと良いやり方をご存知の方がいらっしゃれば、是非ご教授くださいまし。例によって、サンプルコードはここに置いてます。

 最後に、動作させた時の動画以下に貼り付けておきます。

2011/07/09

renderscript-examples開設

Google Projectにrenderscript-examplesというプロジェクトを開設しました。RenderScriptに関する情報を少しづつまとめて置いて行くつもりです。

まだ、コンテンツはほとんど何も無いのですが、この間blogに書いたwonderflの重力マウスをRenderScriptに移植したもののソースコードも置いてありますので、興味のある方は見てみてください。

しかし、無数にあるGoogle ProjectにもRenderScriptに関するプロジェクトはまだ無かったみたいで、もしかして世界初?RenderScriptって誰も触ってない?(汗

プロジェクト名をまんまrenderscriptにしようかとも一瞬思ったのですが、やっぱりびびって後ろにexamplesって付けちゃった、てへ

2011/06/20

Flasherの為のRenderScript入門

■RenderScriptとは?
RenderScriptはAndroid OS3.0から導入された、ハイパフォーマンスな3Dグラフィックスのレンダリングや演算をC言語(C99の構文)で記述できるAPIです。

これまでも、ハイパフォーマンスなレンダリングを行う手段として、OpenGLをNDKで直接叩くという方法がありましたが、RenderScriptはLLVMで一旦中間コードにコンパイルされたものがアプリケーションに付随され、実行時にそれぞれの環境向けに最適化されたマシンコードに更にコンパイルして実行するという仕組みになっております。

その結果、使用しているCPUやGPUの種類によらず、同一のソースコードでそれぞれの環境に置いて効率の良いパフォーマンスを発揮しますが、当然のこととして、特定の環境向けにカリカリにチューニングされたOpenGLのパフォーマンスには及びません。

ただ、iPhoneと違って、様々なプラットフォーム上に展開しているAndroid OSのデバイスとしては、環境依存を吸収する手段として、なかなか面白い仕組みでは無いでしょうか?

■RenderScriptシステムのレイヤー構造
RenderScriptのシステムは、以下の3つのレイヤーから構成されます。

・Native RenderScript layer
このレイヤーはレンダリングや演算の為のRenderScriptそのものです。上に書いたように、C言語の構文で記述しますが、 RenderScriptは汎用的なCPUのみならず、GPU上でも実行する場合もあり得るので、標準的なCのライブラリーがすべて使えるわけではありません。

Native RenderScriptライブラリーで提供されている主な機能は下記。
  1.  膨大な数の演算関数
  2. ベクターや行列等の基本データ型に変換するルーチン
  3. ログ取得の為の関数
  4. グラフィックスのレンダリング関数
  5. メモリー領域確保の為の関数
  6. 2元、3元、4元ベクトル等の、RenderScriptシステムをサポートするデータ型や構造
・Reflected layer
Reflected layerは、AndroidVMからnative RenderScriptコードにアクセスする為の レイヤーで、Androidのビルドツールが自動生成します。その為、Reflected layerは、Android frameworkからRenderScript内の関数や変数にアクセスする為のエントリーポイントを提供します。

・Android framework layer
Android framework layerは、RenderScript APIや通常のAndroid framework APIから構成され、あなたのアプリケーションのActivityのライフサイクルやメモリーの管理などをします。また、このレイヤーで、Reflected layerを介して、ユーザーの操作をnative RenderScriptのコードに関連づけたりもします。

と、かなり前置きが長くなりましたが、詳しくはここのドキュメントを読んでみてください。Flasher的にはC/C++で記述し、同様にLLVMでActionScriptに変換するAlchemyを使うのと、ノリは似ているかも知れませんね。

今回、RenderScriptを使ってFlashでよく使われるParticleのデモを実装してみたいと思いますが、題材としてWonderflの重力マウス(さらに軽量化してみた)を使用したいと思います。

■重力マウスをRenderScriptに移植
重力マウスを移植するにあたって、以下の4つのファイルを記述します。
Gravity.javaGravityアプリのActivityを記述
GravityView.javaRenderScript用のSurfaceViewを記述
GravityRS.javaRenderScript用のエントリーポイントクラスを記述
gravity.rsRenderScriptのコードを記述

Gravity.java
package com.android.sample;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class Gravity extends Activity {
    private GravityView mView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);        
        mView = new GravityView(this); // RSSurfaceViewの作成
        setContentView(mView);
    }
}
特に何も難しいことはしていません。Activityの中で、アプリケーションで使用するViewを設定していますが、今回はRSSurfaceViewを継承したGravityView(後述)を作成してます。

GravityView.java
package com.android.sample;

import android.renderscript.RSSurfaceView;
import android.renderscript.RenderScript;
import android.renderscript.RenderScriptGL;

import android.content.Context;
import android.view.SurfaceHolder;
import android.view.MotionEvent;

public class GravityView extends RSSurfaceView {

    public GravityView(Context context) {
        super(context);
    }

    private RenderScriptGL mRS; // グラフィックス出力の為にスクリプト等
                                // を付与するコンテクスト

    private GravityRS mRender;  // エントリーポイントクラス

    // Surfaceの構成が変わると呼ばれるハンドラー
    public void surfaceChanged(SurfaceHolder holder, int format,
                               int w, int h) {
        super.surfaceChanged(holder, format, w, h);
        if (mRS == null) {
            // グラフィックスのピクセルフォーマットを指定する為のクラス
            RenderScriptGL.SurfaceConfig sc
                = new RenderScriptGL.SurfaceConfig();
            mRS = createRenderScriptGL(sc);
            mRS.setSurface(holder, w, h); //osのsurfaceをバインド

            // エントリーポイントクラスを作成
            mRender = new GravityRS();
            mRender.init(mRS, getResources(), w, h); // 初期化
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        // 後始末
        if (mRS != null) {
            mRS = null;
            destroyRenderScriptGL();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev)
    {
        // エントリーポイントクラスにタッチした座標、指圧と
        // IDの情報を渡す
        // IDはマルチタッチがサポートされている場合に、
        // タッチした指を識別する為のID
        mRender.newTouchPosition(ev.getX(0), ev.getY(0),
            ev.getPressure(0), ev.getPointerId(0));        
        return true;
    }
}
RenderScriptでレンダリングした内容を表示する為のSurfaceViewクラスです。

surfaceChanged()メソッド内でRenderScriptのコンテキストを生成し、Surfaceをバインドしています。スクリプトのバインディングは、後述するエントリーポイントクラス(GravityRs)の方で行なっています。

また、タッチイベントが発生した際も、エントリーポイントクラスに座標等のタッチに関連する情報を渡します。このように、ユーザーの操作によって発生したイベントは基本的にエントリーポイントクラスに情報を渡して、エントリーポイントクラス側で処理をしてもらいます。

GravityRS.java
package com.android.sample;

import android.content.res.Resources;
import android.renderscript.*;

public class GravityRS {
    public static final int PART_COUNT = 50000; // 総パーティクル数

    public GravityRS() {
    }

    private Resources mRes;
    private RenderScriptGL mRS;      // レンダリングのコンテクスト
    private ScriptC_gravity mScript; // スクリプトファイルへの
                                     // エントリークラス

    // 初期化関数
    public void init(RenderScriptGL rs, Resources res,
                     int width, int height) {
        mRS = rs;
        mRes = res;

        // GLSLコードを用いないシンプルなフラグメントシェーダーを作成
        ProgramFragmentFixedFunction.Builder pfb
            = new ProgramFragmentFixedFunction.Builder(rs);

    // バーテックスプログラムから指定された色を使用する
        pfb.setVaryingColor(true);

        // フラグメントシェーダーをレンダリングコンテクストにバインド
        rs.bindProgramFragment(pfb.create());
        
        // スクリプト内のPoint構造体×パーティクル数の分だけ
        // メモリー領域を確保する
        ScriptField_Point points = new ScriptField_Point(mRS,
                                                         PART_COUNT);
        
        // RenderScriptで表示する幾何学データのコンテナを作成する
        // オブジェクト
        Mesh.AllocationBuilder smb = new Mesh.AllocationBuilder(mRS);

        // pointsを頂点データとしてオブジェクトに追加
        smb.addVertexAllocation(points.getAllocation());

        // データの種類を点座標に設定
        smb.addIndexSetType(Mesh.Primitive.POINT);

        // コンテナを作成
        Mesh sm = smb.create();

        // スクリプトファイルのエントリークラスを生成
        mScript = new ScriptC_gravity(mRS, mRes, R.raw.gravity);

        // スクリプト内のpartMeshにMeshをセット
        mScript.set_partMesh(sm);

        // pointsとスクリプト内のpointをバインド
        mScript.bind_point(points);

        // レンダリングコンテクストにスクリプトをバインド
        mRS.bindRootScript(mScript); 
    }

    // タッチ情報のアップデート
    public void newTouchPosition(float x, float y, float pressure,
                                 int id) {
        mScript.set_gTouchX(x); // スクリプト内のgTouchXにX座標をセット
        mScript.set_gTouchY(y); // スクリプト内のgTouchYにY座標をセット
    }
}
一見、複雑なことをやっているようですが、まずフラグメントシェーダーの設定し、それから座標データを格納するメモリー領域の確保をしてます。その後、ビルドツールで自動生成されるエントリーポイントのクラスをインスタンス化して、スクリプト内の変数やポインターと確保したメモリー領域の関連付けを行ってます。

そして最後に、レンダリングコンテクストにスクリプトをバインドして実行、という感じでしょうか?

gravity.rs
#pragma version(1)
#pragma rs java_package_name(com.android.sample)
#pragma stateFragment(parent)

#include "rs_graphics.rsh"

// staticな変数はエントリーポイントが生成されない
static int initialized = 0;

rs_mesh partMesh; // 描画するデータのコンテナ

// タッチ座標
float gTouchX = 0.f; // X座標
float gTouchY = 0.f; // Y座標

// パーティクルの構造体
typedef struct __attribute__((packed, aligned(4))) Point {
    float2 delta;
    float2 position;
    uchar4 color;
} Point_t;
Point_t *point;

// パーティクルの初期化
void initParticles()
{
    // データサイズを取得
    int size = rsAllocationGetDimX(rsGetAllocation(point));

    float width = rsgGetWidth();   // サーフェイスの幅
    float height = rsgGetHeight(); // サーフェイスの高さ

    uchar4 c = rsPackColorTo8888(0.9f, 0.0f, 0.9f); // 色

    Point_t *p = point;
    for (int i = 0; i < size; i++) {
        p->position.x = rsRand(width);  // 乱数でX座標をセット
        p->position.y = rsRand(height); // 乱数でY座標をセット
        p->delta.x = 0;
        p->delta.y = 0;
        p->color = c;
        p++;
    }
    initialized = 1; // 初期化完了
}

/*
 * root()は描画する毎に呼ばれる
 */
int root() {
    // 初回実行時に初期化
    if (!initialized) {
        initParticles();
    }

    float width = rsgGetWidth(); 
    float height = rsgGetHeight();

    // 指定色でレンダリングサーフェイスをクリア
    rsgClearColor(0.f, 0.f, 0.f, 1.f);
    
    int size = rsAllocationGetDimX(rsGetAllocation(point));

    Point_t *p = point;
    for (int i = 0; i < size; i++) {
        float diff_x = gTouchX - p->position.x;
        float diff_y = gTouchY - p->position.y;
        float acc = 50.f/(diff_x * diff_x + diff_y * diff_y);
        float acc_x = acc * diff_x;
        float acc_y = acc * diff_y;
        p->delta.x += acc_x;
        p->delta.y += acc_y;
        p->position.x += p->delta.x;
        p->position.y += p->delta.y;
        p->delta.x *= 0.96;
        p->delta.y *= 0.96;
        if (p->position.x > width) {
            p->position.x = 0;
        } else if (p->position.x < 0) {
            p->position.x = width;
        }
        if (p->position.y > height) {
            p->position.y = 0;
        } else if (p->position.y < 0) {
            p->position.y = height;
        }
        p++;
    }    
 
    rsgDrawMesh(partMesh); // 座標データ列の描画
    
    return 1; // おおよそ1ms後に再描画
}
最後にいよいよRenderScriptの本体です。

まず、Point構造体がパーティクルのデータ構造を構成しており、初回実行時だけ、initParticles()関数内で、パーティクルの座標や色のデータの初期化を行います。

そして、root()関数が描画毎に呼ばれる関数で、Flasher的にはenterFrameのイベントハンドラーのようなものと思えば、理解がしやすいでしょうか?この中のfor文で各パーティクルの座標データをアップデートしております。ここのコードは重力マウスのコードをほぼ、そのまま丸パ◯リ(笑)

ActionScriptでは、動作スピードを上げる為に、リンクリストを形成しておりましたが、ここではリンクリストは使用しないで、単純に構造体のポインターをfor文で要素の数だけ回すという処理になってます。

パーティクルの座標データの更新が終わったら、rsgDrawMesh()という関数で、コンテナ内のデータを元に描画を行います。

最後に、このファイル中に出てきたRenderScript関連の関数をまとめておきます。
rsGetAllocation()VM側で確保したメモリー領域を返す
rsAllocationGetDimX()メモリー領域のX方向の大きさを返す。今回、データは1次元の配列なので、要素数を返すことと同じか?
rsgGetWidth()サーフェースの幅を返す
rsgGetHeight()サーフェースの高さを返す
rsPackColorTo8888()R, G, B値から色コード(8888形式)を生成
rsRand()乱数値を生成
rsgDrawMesh()コンテナのデータを元に描画

■RenderScriptのパフォーマンス
RenderScriptを使うことで、どれくらいのパフォーマンスが出たのでしょうか?PC上では重力マウスは、楽に60fps出ていますが、手持ちのXOOMのブラウザ経由では、20fpsほどしか出ません。

今回、RenderScriptにはfpsを計測する機能を実装していないので、厳密にはどれくらいのフレームレートが出ているのかはわかりませんが、 フルスクリーン(解像度で4倍以上)で、尚且つパーティクルの数を50000に増やしても、目視で30fpsはゆうに出ているように感じました。

なので、少なく見積もっても5倍以上の性能差はありそうです。モバイル版のFlash Playerに早くStage3Dが搭載されることが待たれますね。

実際のアプリケーションの動作の動画を以下に貼りつけておきます。


■まとめ
難しいことは全て理解しなくとも(私も半分くらいしか理解できてない)、上記ファイルをテンプレートに、RenderScriptファイル中のfor文の中身(パーティクルの動作をつけている部分)を適当に書き換えれば、簡単に遊べると思いますので、腕に覚えのあるFlasherの方に是非お試しいただければと思います。

まぁ、遊ぶにはAndroid 3.xの実機が必要にはなりますがね・・・(^_^;