■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ライブラリーで提供されている主な機能は下記。
- 膨大な数の演算関数
- ベクターや行列等の基本データ型に変換するルーチン
- ログ取得の為の関数
- グラフィックスのレンダリング関数
- メモリー領域確保の為の関数
- 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.java | GravityアプリのActivityを記述 |
GravityView.java | RenderScript用のSurfaceViewを記述 |
GravityRS.java | RenderScript用のエントリーポイントクラスを記述 |
gravity.rs | RenderScriptのコードを記述 |
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の実機が必要にはなりますがね・・・(^_^;