Android 3.x RenderScript (3) 独自シェーダーの割り当てとメッシュの描画(2D)

RSSurfaceView に script (*.rs) を登録すると毎フレーム root() 関数が
呼び出され、描画はその中で行います。
描画に使う各種リソースは Java で生成し script に渡します。

script ではこれらのデータやパラメータを組み合わせた描画が可能です。
ジオメトリを計算してアニメーションを行ったり、データを動的に書き換えて
表示内容を更新することもできます。

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

rs_mesh             mesh;           // class Mesh
rs_program_vertex   vsh;            // class ProgramVertex
rs_program_fragment psh;            // class ProgramFragment
rs_program_store    dsbstate;       // class ProgramStore
rs_program_raster   rstate;         // class ProgramRaster
rs_sampler          sampler;        // class Sampler
rs_allocation       texture;        // class Allocation

int root() // 毎フレーム呼ばれる
{
    rsgClearColor( 0.5f, 0.5f, 0.0f, 0.0f );

    rsgBindSampler( psh, 0, sampler );
    rsgBindTexture( psh, 0, texture );
    rsgBindProgramFragment( psh );
    rsgBindProgramVertex( vsh );
    rsgBindProgramStore( dsbstate );
    rsgBindProgramRaster( rstate );

    rsgDrawMesh( mesh );   // ポリゴンを描画する
    return  33;     // interval 33msec
}

コメント部分 ‘// class’ の後ろは対応する Java class 名です。

Mesh            : 頂点とインデックスの集合。描画するポリゴンの集まり。
ProgramVertex   : 頂点シェーダー。座標の変換を記述するプログラム。GLSL
ProgramFragment : ピクセルシェーダー。描画する色を生成するプログラム。GLSL
ProgramStore    : レンダーステート。D3Dの DepthStencilState + BlendState
ProgramRaster   : レンダーステート。D3Dの RasterizerState
Sampler         : テクスチャフィルタ等。D3Dの SamplerState
Allocation      : Buffer, Texture, Uniform 等、script/HW が扱うメモリ。

script の Reflection class が作られるので、Java から script の変数に
容易にアクセスすることができます。

// java
ScriptC_rendermesh  script= new ScriptC_rendermesh( mRS, mRes, R.raw.rendermesh );
    ~
mScript.set_mesh( mesh );
mScript.set_vsh( vsh );
mScript.set_psh( psh );
mScript.set_dsbstate( ProgramStore.BLEND_NONE_DEPTH_NONE( mRS ) );
mScript.set_rstate( ProgramRaster.CULL_NONE( mRS ) );
mScript.set_sampler( Sampler.WRAP_LINEAR( mRS ) );
mScript.set_texture( texture );

mRS.bindRootScript( script );

● Mesh

任意形状の描画には Mesh を使います。
Mesh は 頂点の Allocation と インデックス Allocation の集合です。
TriangleMeshBuilder を使うと Allocation を意識せずに簡単に Mesh を
作ることができます。

// java
Mesh.TriangleMeshBuilder    builder= new Mesh.TriangleMeshBuilder( mRS, 2, Mesh.TriangleMeshBuilder.COLOR );

// 4頂点の定義
builder.setColor( 1f, 0f, 0f, 1f ).addVertex(   0.0f,   0.0f );
builder.setColor( 0f, 1f, 0f, 1f ).addVertex( 200.0f,   0.0f );
builder.setColor( 0f, 0f, 1f, 1f ).addVertex(   0.0f, 200.0f );
builder.setColor( 1f, 1f, 1f, 1f ).addVertex( 200.0f, 200.0f );
// index x 2 の定義 (2ポリゴン)
builder.addTriangle( 0, 2, 1 );
builder.addTriangle( 1, 2, 3 );

Mesh    mesh= builder.create( true );

その代わり TriangleMeshBuilder では頂点形式が固定であまり自由度がありません。
上のサンプルでは頂点要素は「2D頂点座標(xy) + 頂点カラー」ですが頂点カラーが
float4 (F32_4) で格納されてしまいます。
頂点形式の自由度が欲しい場合、また頂点データを script で書き換える場合は
Mesh.AllocationBuilder を使います。(後述)

● シェーダーファイル GLSL

シェーダーは OpenGL ES の GLSL ES でテキストファイルです。
そのまま res/raw 以下のフォルダに入れておきます。
リソース名には拡張子がつかないので、拡張子が無くても区別できるファイル名に
しておきます。

// mesh2dc_vsh.vsh
varying vec4    ocolor;

void main()
{
    gl_Position.xy= ATTRIB_position.xy * UNI_Transform.xy + UNI_Transform.zw;
    gl_Position.zw= vec2( 0.0, 1.0 );
    ocolor.xyzw= ATTRIB_color.xyzw;
}

入力頂点形式 (attribute) や Uniform の宣言は不要です。
renderscript.ProgramVertex が内部の情報を元にヘッダを挿入してくれるからです。
命名ルールは下記のようになっています。Element 名に prefix が付きます。

ATTRIB_<頂点Element>     attribute
UNI_<Constant>           uniform  
UNI_<Texture>            texture

varying (In/Out) は自分で宣言します。

// mesh2dc_psh.psh
precision mediump   float;
varying vec4 ocolor;

void main()
{
    gl_FragColor.xyzw= ocolor.xyzw;
}

Java, RenderScript(C言語), GLSL と複数の言語を併用することになるので
慣れないと混乱しがちです。
例として vector や matrix の表現をあげると下記の通り。

RenderScript     GLSL       Java          Element
-----------------------------------------------------
float            float      float         F32
float2           vec2       Float2        F32_2
float3           vec3       Float3        F32_3
float4           vec4       Float4        F32_4
rs_matrix3x3     mat3       Matrix3f      MATRIX_3X3
rs_matrix4x4     mat4       Matrix4f      MATRIX_4X4

C言語ベースの RenderScript, hlsl, Cg, OpenCL 等は比較的似ています。
この中では GLSL が特殊です。いろいろと。

● Uniform の宣言、データ構造

シェーダーで用いる Constant Buffer (Uniform) は好きなように作ることができます。
Element を組み立てるよりも RenderScript の reflection を使った方が簡単です。

(1) RenderScript/GLSL が参照するリソースの構造宣言は RenderScript (*.rs) で行う。
(2) RenderScript で宣言された構造は Reflection class として Java で参照できる。ScriptFiled_ が付く。
(3) リソースの生成は Java で行う。
(4) 生成したリソースの書き込みは下記の通り。
 ・Java 生成 → script で参照
 ・Java 生成 → WH で参照
 ・Java 生成 → script/WH 両方で参照
 ・Java 生成 → scriptで書き換え → WH で参照

今回の例では 2D のトランスフォーム用に float4 を渡したいので、
RenderScript で下記の宣言を行います。
これで ScriptField_ConstantType2D が作られます。

// renderscript
typedef struct ConstantType2D {
    float4  Transform;
} ConstantType2D_t;

ConstantType2D_t*   ConstantType2D_n; // unuse

ConstantType2D_n はダミーです。
script から参照していないと Reflection class ができないためです。

Java では ScriptField_ConstantType2D をインスタンス化するだけで
ハードウエアに設定可能なメモリ領域 (Allocation) が作られます。
データの書き込みも ScriptField_ConstantType2D を通じて行うことができます。

// java

// メモリ領域作成
ScriptField_ConstantType2D  vconst2d= new ScriptField_ConstantType2D( mRS, 1, Allocation.USAGE_GRAPHICS_CONSTANTS );

// 値の書き込み (HW に転送可能)
vconst2d.set_Transform( 0, new Float4( 2.0f/mWidth, -2.0f/mHeight, -1.0f, 1.0f ), true );

script 側でも参照したり書き換えたい場合は、Allocation.USAGE_SCRIPT を追加し
script 内で受け取るポインタを宣言しておきます。
次に vconst2d.getAllocation() をそのポインタに bind_ します。

● ProgramVertex / ProgramFragment

リソースとして配置したシェーダーを読み込むコード。

// java
public static String LoadResource( Context context, int resid ) {
    InputStream is= context.getResources().openRawResource( resid );
    byte[]  buf= null;
    try {
        int size= is.available();
        buf= new byte[size];
        is.read( buf );
        is.close();
    }catch( IOException e ){
    }
    return  new String( buf );
}

シェーダーを読み込んで ProgramVertex/ProgramFragment を作ります。

// java
// mesh から頂点フォーマット (Element) を取り出す
Element vtype= mesh.getVertexAllocation( 0 ).getType().getElement();

// VertexShader
ProgramVertex.Builder   vsh_builder= new ProgramVertex.Builder( mRS );

// シェーダーファイル読み込み
vsh_builder.setShader( LoadResource( mContext, R.raw.mesh2dc_vsh ) );
vsh_builder.addConstant( vconst2d.getType() ); // Uniform 登録
vsh_builder.addInput( vtype ); // Attribute 登録
ProgramVertex   vsh= vsh_builder.create();
vsh.bindConstants( vconst2d.getAllocation(), 0 ); // Uniform のメモリ領域を登録

頂点シェーダーの入力頂点形式 (attribute) は Mesh と一致させておく必要が
あります。上の例のように Mesh から vtype を取り出すことができます。

なお TriangleMeshBuilder を使った場合の Element 名は固定です。
順番と名称は次の通り: ‘position’, ‘color’, ‘texture’, ‘normal’

// java
// PixelShader
ProgramFragment.Builder psh_builder= new ProgramFragment.Builder( mRS );

// シェーダーファイル読み込み
psh_builder.setShader( LoadResource( mContext, R.raw.mesh2dc_psh ) );
ProgramFragment psh= psh_builder.create();

Texture を用いる場合は psh_builder で AddTexture() します。
VertexShader と同じように uniform ヘッダが挿入されます。

● ProgramStore / ProgramRaster / Sampler

よく使う Object が予め用意されているので、既存のものを渡すだけで十分です。
今回の例では特に設定せず、デフォルトのまま使用しています。

●ソースコード

テクスチャを使わない 2D mesh 描画のコードはこちら。

flatlib_ap02a.zip

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

typedef struct ConstantType2D {
    float4  Transform;
} ConstantType2D_t;
ConstantType2D_t*   ConstantType2D_n; // unuse

rs_program_vertex   vsh;
rs_program_fragment psh;
rs_mesh             mesh;

int root()
{
    rsgClearColor( 0.5f, 0.5f, 0.0f, 0.0f );

    rsgBindProgramFragment( psh );
    rsgBindProgramVertex( vsh );

    rsgDrawMesh( mesh );
    return  33;
}

// SimpleView.java
package jp.flatlib.ap02a;

import android.content.Context;
import android.content.res.Resources;
import android.renderscript.RSSurfaceView;
import android.renderscript.RenderScriptGL;
import android.renderscript.Float4;
import android.renderscript.Allocation;
import android.renderscript.ProgramVertex;
import android.renderscript.ProgramFragment;
import android.renderscript.Mesh;
import android.view.SurfaceHolder;
import java.io.InputStream;
import java.io.IOException;


public class SimpleView extends RSSurfaceView {

    private RenderScriptGL      mRS;
    private Resources           mRes;
    private Context             mContext;

    private float       mWidth;
    private float       mHeight;

    private ScriptField_ConstantType2D  a_vconst2d;


    public SimpleView( Context context ) {
        super( context );
        createRS();
    }


    public static String LoadResource( Context context, int resid ) {
        InputStream is= context.getResources().openRawResource( resid );
        byte[]  buf= null;
        try {
            int size= is.available();
            buf= new byte[size];
            is.read( buf );
            is.close();
        }catch( IOException e ){
        }
        return  new String( buf );
    }


    private void createRS() {
        if( mRS != null ){
            return;
        }

        RenderScriptGL.SurfaceConfig    sc= new RenderScriptGL.SurfaceConfig();
        mRS= createRenderScriptGL( sc );

        mContext= getContext();
        mRes= mContext.getResources();
    }


    private void setupRS() {

        createRS();

        ScriptC_rendermesh  mScript= new ScriptC_rendermesh( mRS, mRes, R.raw.rendermesh );

        // Mesh
        Mesh.TriangleMeshBuilder    builder= new Mesh.TriangleMeshBuilder( mRS, 2, Mesh.TriangleMeshBuilder.COLOR );

        builder.setColor( 1f, 0f, 0f, 1f ).addVertex(  10.0f,  10.0f );
        builder.setColor( 0f, 1f, 0f, 1f ).addVertex( 280.0f,  10.0f );
        builder.setColor( 0f, 0f, 1f, 1f ).addVertex(  10.0f, 280.0f );
        builder.setColor( 1f, 1f, 1f, 1f ).addVertex( 280.0f, 280.0f );

        builder.addTriangle( 0, 2, 1 );
        builder.addTriangle( 1, 2, 3 );
        Mesh    mesh= builder.create( true );


        // Uniform
        a_vconst2d= new ScriptField_ConstantType2D( mRS, 1, Allocation.USAGE_GRAPHICS_CONSTANTS );
        a_vconst2d.set_Transform( 0, new Float4( 2.0f/mWidth, -2.0f/mHeight, -1.0f, 1.0f ), true );


        // VertexShader
        ProgramVertex.Builder   vsh_builder= new ProgramVertex.Builder( mRS );
        vsh_builder.setShader( LoadResource( mContext, R.raw.mesh2dc_vsh ) );
        vsh_builder.addConstant( a_vconst2d.getType() );
        vsh_builder.addInput( mesh.getVertexAllocation( 0 ).getType().getElement() );
        ProgramVertex   vsh= vsh_builder.create();
        vsh.bindConstants( a_vconst2d.getAllocation(), 0 );


        // PixelShader
        ProgramFragment.Builder psh_builder= new ProgramFragment.Builder( mRS );
        psh_builder.setShader( LoadResource( mContext, R.raw.mesh2dc_psh ) );
        ProgramFragment psh= psh_builder.create();


        // Global
        mScript.set_vsh( vsh );
        mScript.set_psh( psh );
        mScript.set_mesh( mesh );


        mRS.bindRootScript( mScript );
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        createRS();
    }

    @Override
    public void onDetachedFromWindow() {
        if( mRS != null ){
            mRS= null;
            destroyRenderScriptGL();
        }
    }

    @Override
    public void surfaceChanged( SurfaceHolder holder, int format, int w, int h ) {
        super.surfaceChanged( holder, format, w, h );
        mWidth= w;
        mHeight= h;
        setupRS();
    }
}

最終的にはこれだけです。

● Mesh.AllocationBuilder

Mesh.TriangleMeshBuilder では制限があるので、頂点形式を自由に宣言して
script で書き換えたい場合は Mesh.AllocationBuilder を使います。

Android SDK のサンプルはどれも PointSprite なので、IndexBuffer の使い方が
わからず理解に時間がかかりました。やっと出来るようになりました。

次はそのあたりを動かしてみます。「Android 3.x RenderScript (4) script で頂点を書き換える」

関連エントリ
Android 3.x RenderScript (2) 描画と Allocation
Android 3.x RenderScript (1)