2011/08/25

Thoughts on Flash vs HTML5

世の中、何事も対立軸で見た方がわかりやすいし、面白いですよね。最近度々起こるFlash vs HTML5論争もそんな感じなのですが、この論争のきっかけ(のひとつ)を作った敬愛するSteve JobsがAppleのCEOの座を辞職することになったので、これを機会に彼のThought on Flashを真似て、Thoughts on Flash vs HTML5として、自分の考えをまとめてみました。

■FlashとHTML5はそもそも競合する技術なのか?
一部のHTML5支持層はHTML5によって今後Flashは不要になると言う。Flashの多くの支持層は、FlashとHTML5はお互いを排他する存在ではなくて、両方をうまく使ってよりよいものを作って行ければ良いと言う。双方にそれぞれの言い分があるようだ。

じゃー、私はどう思っているのか?と言いますと、私はFlashとHTML5は基本的に競合する技術だと思う。確かに両方をうまく組み合わせて使うことを否定はしないけど、HTML5がFlashのポジションを狙っているのは明らかじゃない?

いまはまだリトルリーグと大リーグくらいの力の差はあるのかも知れないけど、HTML5少年もいつかは大リーガーになることを夢見てこれからも練習に励んでいくんだろう。Flashはその頃には更に先に進んでいるんだろうけど、HTML5で多くの人が必要十分だと思う機能はいずれ実現されると思う。

但し、用語に混乱があるような気がする。HTML5と言っても何がHTML5なのか私にはよくわからない。なので、この手の話はFlash vs その他のWeb技術と捉えるようにしている。もっとぶっちゃけると、Flashを無くしたいと思っている人達と、無くしたく無いと思っている人達の論争なんだろうな、と。

■何でそんなにFlashを無くしたいの?
Flashを無くしたい人達は何でそこまでFlashを嫌うんだろうか?幾つかケースを考えてみた。

1) iOSで見れないから
これはわかる。これは他に選択肢が無いもんね。でも、みんながみんなiOSを使っているわけじゃないんだし、実はFlashを見られない人よりもHTML5を見られない人の方が多いらしいよ?いまんとこね。

2) オープンソースじゃないから
Flash Playerがオープンソースじゃないというのは、いまの時代には嫌うに充分な理由になると思う。自分は、Adobeもそのことは充分承知しているが、オープンにすることで互換性の無いPlayerが広まることを恐れているのでは無いかと勝手に想像している。

だから、Adobeはオープンにしない代わりにみんなの為にいろんなプラットフォームやブラウザ間での互換性を保つ為に血の滲むような多大な努力を払っているんだなと思うようにしている。

まぁ、オープンにしてほしいけどね。

3) プラグインを入れるのが嫌だから
自分もプラグインを入れると何となく不安定になるんじゃないかというイメージは持っているので、なるべくプラグインは入れたく無いという気持ちはよくわかりますし、実際、必要だと思う最低限のものしか入れないようにしてます。でも、常用のブラウザ(Chrome)にはデフォルトでFlash Playerのプラグインが入るようになったんですよ・・・

4) 過去に何か嫌な思いをしたから
過去に糞重いFlashサイトを開いてしまって、すごく迷惑だった?あるいはいきなり大音量が出て恥ずかしい思いをしたとか?何でもそうですけど、一度失った信用を取り戻すというのは大変なことですよね。これに関しては、私から言えることは何もありません。

ただ、Flashが流行り始めたあの頃と違って、いまはそんなにひどいサイトはあまり見かけないと思うんですけどねー。

5) ちょっとした出来心
HTML5によって以前よりもインタラクティブなサイトがFlashを使わなくても作れるようになったというのは歴然たる事実だと思う。だから、Flashイラネとか言ってみたらちょっと受けるかな?とか、ちょっとした出来心で言ってしまったってのが本当のところじゃないのかな?

■FlasherはFlashが無くなったら困るの?
じゃー仮にFlashの無い世界がやって来たとして、Flasherは困るのだろうか?実はそんなに困らないんじゃないのかなぁ?と思ってます。むしろFlashを毛嫌いして、インタラクティブなUIやコンテンツをあまり作ってこなかった人達の方が、HTML5の時代になって、そういうコンテンツを求められた時に困るのでは無かろうか?

AdobeもHTML5対応のツールを今後積極的に出していくと思うし、Edgeを見てみても、それは現在のFlash IDEの使い勝手をある程度踏襲したものになるだろう。であれば、極論すれば、Flasherに取っては出力フォーマットがFlashかHTML5の違いでしか無いのだ。

だからHTML5の時代が来ても、インタラクティブなUIや演出に長けたFlasherの仕事は減ることは無く、むしろ増える方向に行くのでは無いだろうか?HTML5にはまた違ったKnowHowが当然求められるだろうが、Flashで一回乗り越えた人達の方が、いろいろと鼻は効くような気がする。

それに、最終的にAdobeはFlash IDEでFlashコンテンツを作っても、プラグインが無いとHTML5にフォールバックするようなコンテンツを作れるようにするでしょ?それは絶対間違いないでしょ?だからHTML5派の人もFlashを使えるようになってても損はしないと思うけどな。

■まとめ
個人的にはHTML5がFlashを駆逐すると言うにはまだまだ時期尚早だと思うし、現実解としてのFlashは今後もしばらく、というかずっと使われることになると思う。だからと言って安泰というわけでも無く、ある領域に置いてはHTML5にその座を譲ることになるのであろう。

だけど、そんなことはどうでもいいことじゃないだろうか?正直、HTML5とFlashのどちらが明日生き残ろうが、大した問題では無い。そんなことより、大好きなあの子に今日フラれるかも知れないことの方がよっぽど重要な問題だよね。

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

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

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