Android 3.x RenderScript で 3D モデルのライティング

RenderScript で光源やカメラ位置を使った描画方法を取り上げて欲しいとの
メールをいただいたので試してみました。

renderscript_ap02d.jpg

OpenGL ES 2.0 なのでこれらのパラメータの反映はシェーダー任せです。
演算は RenderScript よりも GLSL の範疇になるかと思いますが、
複数の ConstantBuffer の作製や PixelShader への渡し方などを参考にして
いただければと思います。

●必要なデータ構造の定義と初期化

RenderScript で、必要な情報の構造体を宣言します。
内容は Java, RenderScript, Shader でそれぞれ共有します。

ジオメトリ関連

// RenderScript
typedef struct GeometryConstant {
    rs_matrix4x4    Projection;		// Projection Matrix
    rs_matrix4x4    World;		// World Matrix
    rs_matrix4x4    PView;		// RS → Shader 用
    float4          CameraPosition;	// (x y z -) カメラ位置
    float4          CameraAim;		// (x y z -) 注視点
} GeometryConstant_T;

描画オブジェクトのマテリアル

// RenderScript
typedef struct MaterialConstant {
    float4      AmbientColor;	// (r g b -)
    float4      DiffuseColor;	// (r g b -)
    float4      SpecularColor;	// (r g b p)
} MaterialConstant_T;

光源。Intensity は Color に事前乗算しておく。

// RenderScript
typedef struct LightConstant {
    float4      AmbientLightColor;		// (r g b -)
    float4      DirectionalLightColor;		// (r g b -)
    float4      PointLightColor;		// (r g b -)
    float4      DirectionalLightDirection;	// (x y z -)
    float4      PointLightPosition;		// (x y z -)
} LightConstant_T;

Java 上でインスタンスを作って初期化します。
カメラは座標と注視点を登録しています。

// Java
// Geometry Uniform
ScriptField_GeometryConstant  geometry_const= new ScriptField_GeometryConstant( mRS, 1, Allocation.USAGE_SCRIPT|Allocation.USAGE_GRAPHICS_CONSTANTS );

// Projection Matrix
Matrix4f    projection= new Matrix4f();
projection.loadPerspective( 30.0f, mWidth/mHeight, 0.1f, 100.0f );
geometry_const.set_Projection( 0, projection, true );

// Camera Position
geometry_const.set_CameraPosition( 0, new Float4( 0.0f, 0.0f, 3.0f, 0.0f ), true );
geometry_const.set_CameraAim( 0, new Float4( 0.0f, 0.0f, 0.0f, 0.0f ), true );

// World Matrix
Matrix4f    world= new Matrix4f();
world.loadIdentity();
geometry_const.set_World( 0, world, true );

テクスチャを使わないのでマテリアルカラーを設定。

// Java
// Material Uniform
ScriptField_MaterialConstant  material_const= new ScriptField_MaterialConstant( mRS, 1, Allocation.USAGE_SCRIPT|Allocation.USAGE_GRAPHICS_CONSTANTS );

material_const.set_AmbientColor( 0, new Float4( 0.3f, 0.3f, 0.3f, 1.0f ), true );
material_const.set_DiffuseColor( 0, new Float4( 1.0f, 0.9f, 0.9f, 1.0f ), true );
material_const.set_SpecularColor( 0, new Float4( 1.0f, 1.0f, 0.4f, 30.0f ), true );

光源を設定

// Light Uniform
ScriptField_LightConstant  light_const= new ScriptField_LightConstant( mRS, 1, Allocation.USAGE_SCRIPT|Allocation.USAGE_GRAPHICS_CONSTANTS );

light_const.set_AmbientLightColor( 0, new Float4( 0.3f, 0.3f, 0.3f, 1.0f ), true );
light_const.set_DirectionalLightColor( 0, new Float4( 1.0f, 1.0f, 1.0f, 1.0f ), true );
light_const.set_PointLightColor( 0, new Float4( 0.0f, 1.0f, 1.0f, 1.0f ), true );
light_const.set_DirectionalLightDirection( 0, new Float4( 0.57f, -0.57f, -0.57f, 0.0f ), true );
light_const.set_PointLightPosition( 0, new Float4( 0.0f, 0.2f, 0.0f, 0.0f ), true );

作成した Constant Buffer をシェーダーで読めるようにします。
Builder で必要な数だけ addConstant() して Uniform を予約します。
インスタンスは ProgramVertex の bindConstants() で渡します。

必要な constnat buffer だけ渡しています。
VertexShader は GeometryConstant と LightConstant。

// Java
// VertexShader
ProgramVertex.Builder   vsh_builder= new ProgramVertex.Builder( mRS );
vsh_builder.setShader( mRes, R.raw.mesh3d_vsh );
vsh_builder.addConstant( geometry_const.getType() );
vsh_builder.addConstant( light_const.getType() );
vsh_builder.addInput( model.getElement() );

ProgramVertex   vsh= vsh_builder.create();
vsh.bindConstants( geometry_const.getAllocation(), 0 );
vsh.bindConstants( light_const.getAllocation(), 1 );

PixelShader (FragmentShader) も同様です。
MaterialConstant/LightConstant の 2つです。

// Java
// PixelShader
ProgramFragment.Builder psh_builder= new ProgramFragment.Builder( mRS );
psh_builder.setShader( mRes, R.raw.mesh3d_psh );
psh_builder.addConstant( material_const.getType() );
psh_builder.addConstant( light_const.getType() );

ProgramFragment psh= psh_builder.create();
psh.bindConstants( material_const.getAllocation(), 0 );
psh.bindConstants( light_const.getAllocation(), 1 );

Shader と同じように、RenderScript にもアクセスするデータを渡します。
GeometryConstant/LightConstant の 2つ。

// Java
mScript.bind_vconst( geometry_const );
mScript.bind_light( light_const );

●RenderScript

ViewMatrix (WorldSpace To CameraSpace) の作成と ProjectionMatrix
との前計算を行なっています。

CameraPosition/CameraAim, ProjectionMatrix を元に、
PView (ProjectionMatrix * ViewMatrix) を求めます。

// RenderScript
rs_matrix4x4    viewMatrix;
LookAt( &viewMatrix, vconst->CameraPosition.xyz, vconst->CameraAim.xyz );
rsMatrixLoadMultiply( &vconst->PView, &vconst->Projection, &viewMatrix );

RenderScript は C言語ながら、上記のようにシェーダーのような vector 記法
が使えます。例えば float4 で宣言した変数も .xyz をつけることで float3
の部分アクセスができます。

float4 vec;
float3 pos= vec.xyz;

swizzle もできました。使いやすいです。

float4 d= vec.wzyx;
float4 e= vec.yyyy;
float4 f;
f.zw= vec.xy;

RenderScritp に LookAt 関数があるかと思ったらなかったので作ります。

// RenderScript
static void LookAt( rs_matrix4x4* view, float3 pos, float3 aim )
{
    rsMatrixLoadIdentity( view );
    float3  up= { 0.0f, 1.0f, 0.0f };
    float3  pz= normalize( aim - pos );
    float3  px= normalize( cross( pz, up ) );
    float3  py= normalize( cross( px, pz ) );
    view->m[0]= px.x;
    view->m[4]= px.y;
    view->m[8]= px.z;
    view->m[1]= py.x;
    view->m[5]= py.y;
    view->m[9]= py.z;
    view->m[2]= -pz.x;
    view->m[6]= -pz.y;
    view->m[10]= -pz.z;
    rsMatrixTranslate( view, -pos.x, -pos.y, -pos.z );
}

これ以外に、サンプルの script では効果がわかりやすいように
カメラ位置と点光源のアニメーションを行なっています。

●Shader

シェーダーでは与えられたパラメータを元に座標と色の計算を行なっています。
ScreenSpace だけでなく WorldSpace での座標と法線の算出。

// GLSL
varying vec3    onormal;
varying vec3    oeye;
varying vec3    opos;

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;
    oeye.xyz= UNI_CameraPosition.xyz - wpos.xyz;
    opos.xyz= wpos.xyz;
}

MaterialConstant と LightConstant を用いてライティング。

// GLSL
precision mediump   float;

varying vec3 onormal;
varying vec3 oeye;
varying vec3 opos;

void main()
{
    vec3    normal= normalize( onormal.xyz );

    // AmbientLight
    lowp vec3    diffuse= UNI_AmbientLightColor.xyz * UNI_AmbientColor.xyz;

    // DirectinalLight
    float   dd= clamp( dot( normal.xyz, -UNI_DirectionalLightDirection.xyz ), 0.0, 1.0 );
    diffuse.xyz+= UNI_DirectionalLightColor.xyz * UNI_DiffuseColor.xyz * dd;

    float   ds= pow( clamp( dot( normal.xyz, normalize( oeye.xyz - UNI_DirectionalLightDirection.xyz ) ), 0.0, 1.0 ), UNI_SpecularColor.w );
    lowp vec3    specular= UNI_SpecularColor.xyz * ds;

    // PointLight
    vec3    lightvec= UNI_PointLightPosition.xyz - opos.xyz;
    vec3    lightdir= normalize( lightvec.xyz );
    float   dl= dot( lightvec.xyz, lightvec.xyz ) * 10.0;
    float   pd= clamp( dot( normal.xyz, lightdir.xyz ), 0.0, 1.0 ) / dl;
    diffuse.xyz+= UNI_PointLightColor.xyz * UNI_DiffuseColor.xyz * pd;

    gl_FragColor.xyz= diffuse.xyz + specular.xyz;
    gl_FragColor.w= 1.0;
}

●ソースコード

モデルデータの読み込みは以前のエントリと同じ物を使っています。

flatlib_ap02d.zip ソースコード

●最適化

無駄が多いので最適化の余地があります。

ConstantBuffer は更新するデータ量が少なくなるように分割できます。
今回は Java, RenderScript, Shader でバッファを共有していますが不要な
パラメータがあります。RenderScript で書き換えるものと Shader だけが
参照するものをわけると転送量を減らせます。

それでも ConstantBuffer (Uniform) は頂点書き換えと異なり PushBuffer
を経由するため lock が発生しないため効率は良いです。

PixelShader (FragmentShader) での演算は負荷が重いので、出来る限り
シェーダーの外に出した方が良いです。
MaterialColor * LightColor はプリミティブ単位で求まるので前計算可能です。
頂点単位で十分なものは VertexShader に分散させることができます。

関連エントリ
Android 3.x RenderScript (7) RenderScript Compute の速度
Android 3.x RenderScript (6) Graphics まとめ
Android 3.x RenderScript (5) 任意の 3D モデルを描画する
Android 3.x RenderScript (4) script で頂点を書き換える
Android 3.x RenderScript (3) 独自シェーダーの割り当てとメッシュの描画(2D)
Android 3.x RenderScript (2) 描画と Allocation
Android 3.x RenderScript (1)