Android 3.x RenderScript (5) 任意の 3D モデルを描画する

RenderScript には Android3D_ff というデータフォーマットがあります。
FileA3D を使うと Android3D_ff を読み込んで Mesh を生成できるのですが、
バイナリファイルでコンバート方法もわからなかったのであきらめました。
別の方法を使うことにします。

● IndexBuffer/VertexBuffer を自分で組み立てる

script (*.rs) 側で宣言しなくても IndexBuffer の Allocation を自分で作る
方法がわかりました。

// java
short[] indexdata= { 0, 1, 2, 0, 2, 3 };
Element.Builder builder= new Element.Builder( mRS );
builder.add( Element.I16( mRS ), "index" );
Allocation ibuffer= Allocation.createSized( mRS, builder.create(), indexdata.length, Allocation.USAGE_GRAPHICS_VERTEX );
ibuffer.copyFromUnchecked( indexdata );

配列から直接コピーできるため初期化も簡単です。

VertexBuffer も全く同じように自分で構築することができます。

// java
float[] vertexdata= { 0f, 0f, 0f, ~ };
Element.Builder builder= new Element.Builder( mRS );
builder.add( Element.F32_3( mRS ), "position" );
builder.add( Element.F32_3( mRS ), "normal" );
builder.add( Element.F32_2( mRS ), "texcoord0" );
Element vtype= builder.create();
Allocation vbuffer= Allocation.createSized( mRS, vtype, vertexdata.length/strideSize, Allocation.USAGE_GRAPHICS_VERTEX );
vbuffer.copyFromUnchecked( vertexdata );

ここまで出来ればデータファイル側に頂点構造を記述しておくことができそうです。
任意の頂点形式を持ったモデルデータを読み込めるようになります。

● Shader をもっと簡単に読み込む

以前 シェーダーを読み込むために LoadResource() という関数を使いましたが
無くても読み込めることがわかりました。

vsh_builder.setShader( LoadResource( mContext, R.raw.mesh2dc_vsh ) );

  ↓

vsh_builder.setShader( mRes, R.raw.mesh2dc_vsh );

setShader() に直接リソース名を渡すことができました。
でも今回は、モデルデータを読み込むために再び LoadResource() を使います。

● Model データフォーマットを作る

簡単に読み込めそうだったので JSON を使ってモデルデータを記述しました。

[
 {
  "primitive" : "TRIANGLE",
  "element" : [
    "position" , "F32_3",
    "normal"   , "F32_3",
    "texcoord0", "F32_2"
  ],
  "vertex" : [
    -0.500000, -0.500000,  0.500000,
     0.000000,  0.000000,  1.000000,
     0.375000,  1.000000,
     0.500000, -0.500000,  0.500000,
     0.000000,  0.000000,  1.000000,
     0.625000,  1.000000,
  ~
  ],
  "index" : [
    0,1,2,
    0,2,3,
    4,5,6,
    4,6,7,
    8,9,10,
    8,10,11,
    12,13,14,
    12,14,15,
    16,17,18,
    16,18,19,
    20,21,22,
    20,22,23
  ]
 }
]

こんな感じでテキストとして記述します。

● JSON を読み込む

モデルデータファイルを res/raw/model00.json に置いておきます。

JSONTokener を使ってモデルを読み込み、情報を取り出して Mesh を組み立てます。

// java
// Mesh を作るための準備
mMeshBuilder= new Mesh.AllocationBuilder( mRS );

// リソースから model00.json を読み込む
String data= LoadResource( mContext, R.raw.model00 );

try {
    JSONArray   toparray= (JSONArray)new JSONTokener( data ).nextValue();
    int length= toparray.length();
    for( int i= 0 ; i< length ; i++ ){
        // 読み込んだデータはこれ
        JSONObject  object= toparray.getJSONObject( i );

        // object から element, vertex, index を取り出す
        LoadElement( object );
        LoadVertex( object );
        LoadIndex( object );
    }
}catch( JSONException e ){
}

最初に Element の情報を読み込んで組み立てます。

まずは "F32_3" などの文字列を Element.F32_3() に変換するためのテーブルを
作ります。同時に頂点のサイズ (byte数) も求められるようにしておきます。

// java
// データ形式を Element に対応付けするためのテーブルを準備する
class VertexType {
    public Element  element;
    public int      size;
    public VertexType( Element e, int s ) {
        element= e;
        size= s;
    }
}
~

// テーブルを作る
HashMap  map= new HashMap();
map.put( "F32",   new VertexType( Element.F32(mRS), 4 ) );
map.put( "F32_2", new VertexType( Element.F32_2(mRS), 8 ) );
map.put( "F32_3", new VertexType( Element.F32_3(mRS), 12 ) );
map.put( "F32_4", new VertexType( Element.F32_4(mRS), 16 ) );
~

JSON の "element" の情報を読み込んで Element を作ります。

// java
Element.Builder bulider= new Element.Builder( mRS );
int strideSize= 0;

// JSON から element の配列を取り出す
JSONArray   element= object.getJSONArray( "element" );
int esize= element.length();
for( int ei= 0 ; ei< esize ;){
    String  name= element.getString( ei++ );     // "position" 等
    String  typename= element.getString( ei++ ); // "F32_3" 等

    // テーブルを使ってデータ形式文字列を Element に変換する
    if( map.containsKey( typename ) ){
        VertexType  vtype= map.get( typename );
        bulider.add( vtype.element, name );
        // 頂点のサイズも計算しておく
        strideSize+= vtype.size;
    }
}

// Element を作る
mElement= bulider.create();
mStirdeSize= strideSize; // 頂点あたりの byte サイズ

Element と頂点サイズが求まったので、この情報を元に頂点バッファを作ります。

// java
private void LoadVertex( JSONObject object ) throws JSONException {
    // JSON からのデータ読み込み
    JSONArray   array= object.getJSONArray( "vertex" );
    int size= array.length();
    float[] vertexdata= new float[size];
    for( int i= 0 ; i< size ; i++ ){
        vertexdata[i]= (float)array.getDouble( i );
    }

    // バッファを作成して転送する
    Allocation vbuffer= Allocation.createSized( mRS, mElement, vertexdata.length / (mStirdeSize/4), Allocation.USAGE_GRAPHICS_VERTEX );
    vbuffer.copyFromUnchecked( vertexdata );

    // Mesh の要素として追加
    mMeshBuilder.addVertexAllocation( vbuffer );
}

同じように Index も読み込んで作ります。
Index が無い場合にも対応。

// java
private void LoadIndex( JSONObject object ) throws JSONException {

    if( !object.has( "index" ) ){
        // index が無い場合
        mMeshBuilder.addIndexSetType( Mesh.Primitive.TRIANGLE );
        return;
    }

    // JSON から読み込む
    JSONArray   array= object.getJSONArray( "index" );
    int size= array.length();
    short[] indexdata= new short[size];
    for( int i= 0 ; i< size ; i++ ){
        indexdata[i]= (short)array.getInt( i );
    }

    // バッファを作成する
    Element.Builder eb= new Element.Builder( mRS );
    eb.add( Element.I16( mRS ), "index" );
    Allocation ibuffer= Allocation.createSized( mRS, eb.create(), indexdata.length, Allocation.USAGE_GRAPHICS_VERTEX );
    ibuffer.copyFromUnchecked( indexdata );

    // Mesh に追加
    mMeshBuilder.addIndexSetAllocation( ibuffer, Mesh.Primitive.TRIANGLE );
}

あとは Mesh を作るだけです。

Mesh  mesh= mMeshBuilder.create();

これでモデルデータを読み込んで Mesh を作ることが出来ました。
(今回は "primitive" は参照していません。)

● Depth Buffer の利用

RenderScriptGL 作成時に Depth の最小値を与えます。

// java
RenderScriptGL.SurfaceConfig    sc= new RenderScriptGL.SurfaceConfig();
sc.setDepth( 16, 24 );  // ← 追加
mRS= createRenderScriptGL( sc );

ProgramStore で depth test を有効にします。
このオブジェクトを script に渡します。

// java
mScript.set_dsbstate( ProgramStore.BLEND_NONE_DEPTH_TEST( mRS ) );

script 側では Depth Buffer のクリアを追加し、受け取った
ProgramStore (DepthStencilState) を設定します。

// RenderScript
rs_program_store    dsbstate;

int root()
{
    rsgClearColor( 0.0f, 0.2f, 0.5f, 0.0f );
    rsgClearDepth( 1.0f ); // ← 追加
    ~
    rsgBindProgramStore( dsbstate ); // 設定
    ~

● 3D 描画用の ConstantBuffer を作る

今回は Mesh の頂点は書き換えませんが、Matrix を script で書き換えるため
Constant Buffer の構造体を RenderScript で宣言します。

// RenderScript
typedef struct ConstantType {
    rs_matrix4x4    PView;
    rs_matrix4x4    World;
} ConstantType_t;
ConstantType_t* ConstantType_n; // unuse

OpenGL ES 2.0 には 3x4 matrix が無いので残念ながら RenderScript にもありません。
PView は ProjectionMatrix * ViewMatrix です。
初期値は Java から与えてみます。

// java
// Uniform
vconst= new ScriptField_ConstantType( mRS, 1, Allocation.USAGE_SCRIPT|Allocation.USAGE_GRAPHICS_CONSTANTS );
Matrix4f    projection= new Matrix4f();
projection.loadPerspective( 30.0f, mWidth/mHeight, 0.1f, 100.0f );

Matrix4f    view= new Matrix4f();
view.loadTranslate( 0.0f, 0.0f, -10.0f );

Matrix4f    pview= new Matrix4f();
pview.loadMultiply( projection, view ); // ProjectionMatrix * ViewMatrix

Matrix4f    world= new Matrix4f();
world.loadIdentity();

// 書き込み
vconst.set_PView( 0, pview, true );
vconst.set_World( 0, world, true );

シェーダーでは UNI_PView と UNI_World で参照できます。

// GLSL : mesh3d_vsh.vsh
varying vec3    onormal;
varying vec2    otexcoord0;

void main()
{
    vec4    wpos= UNI_World * vec4( ATTRIB_position.xyz, 1.0 );
    gl_Position= UNI_PView * wpos;
    onormal.xyz= mat3(UNI_World) * ATTRIB_normal.xyz;
    otexcoord0.xy= ATTRIB_texcoord0.xy;
}

● RenderScript で Matrix 書き換え

Java や Shader だけでなく script からも ConstantType のデータに
アクセスできます。
あらかじめ mScript.bind_vconst( vconst ) でバッファを登録しておきます。
RenderScript には Matrix 演算のための関数が用意されているため
このような geometry 演算は容易に出来ます。

// RenderScript : rendermesh.rs
#pragma version(1)
#pragma rs java_package_name(jp.flatlib.ap02c)
#include    "rs_graphics.rsh"


typedef struct ConstantType {
    rs_matrix4x4    PView;
    rs_matrix4x4    World;
} ConstantType_t;
ConstantType_t* ConstantType_n; // unuse


rs_program_vertex   vsh;
rs_program_fragment psh;
rs_allocation       texture;
rs_sampler          sampler;
rs_mesh             mesh;
rs_program_store    dsbstate;
rs_program_raster   rstate;

ConstantType_t*     vconst;

static float        rot= 0.0f;


int root()
{
    rsgClearColor( 0.0f, 0.2f, 0.5f, 0.0f );
    rsgClearDepth( 1.0f );

    // 回転させてみる
    rs_matrix4x4    world;
    rsMatrixLoadRotate( &world, rot, 0.0f, 1.0f, 0.0f );
    rot+= 1.0f;
    if( rot >= 360.0f ){
        rot-= 360.0f;
    }

    // WorldMatrix を書き換える
    rsMatrixLoad( &vconst->World, &world );
    rsgAllocationSyncAll( rsGetAllocation( vconst ) );

    // 読み込んだ mesh の描画
    rsgBindSampler( psh, 0, sampler );
    rsgBindTexture( psh, 0, texture );
    rsgBindProgramFragment( psh );
    rsgBindProgramVertex( vsh );
    rsgBindProgramStore( dsbstate );
    rsgBindProgramRaster( rstate );

    rsgDrawMesh( mesh );

    return  33;
}

●ソースリスト

これで JSON に変換した任意のモデルデータを読み込んで RenderScript で
描画できるようになりました。
Material や Geometry、Animation 情報も欲しくなります。

flatlib_ap02c.zip ソースコード

次はまとめなど 「Android 3.x RenderScript (6) Graphics まとめ」

関連エントリ
Android 3.x RenderScript (4) script で頂点を書き換える
Android 3.x RenderScript (3) 独自シェーダーの割り当てとメッシュの描画(2D)
Android 3.x RenderScript (2) 描画と Allocation
Android 3.x RenderScript (1)