Androidの描画比較(AIR for Androidと、OpenGL)

きっかけ

以前AIR for Androidで作りかけたアプリがあって、その時は描画がもっさりしていたのと、
常駐できなかったので断念してました。

その後ゲーム作成を通じてFlashでStage3Dを扱う技術を獲得したので、
再度作ってみようかなという気になり、数日前からいじりはじめました。
AIR for Androidはネイティブより遅いというイメージがあったんだけども、
Stage3D使ったらどうなるだろう?という疑問が沸いてきたので、
FPS比較くらいならすぐにできるだろうと思い、試してみました。

やった事

AndroidアプリをJavaで書くのは少しやった程度なので、OpenGLの書き方なんぞ分かりません。
ネットを探し回って意味が良く分からない関数もあるままで、なんとか動かしましたっていう状態です。
一方AIRのほうは慣れたもので、すらすらとなんの障害もなく書けました。

計測方法としては、単純にpng画像を500個置いて左右に動かしてFPS計測するだけです。
Javaの方で変な文字が出てるのは気にしないでね

結果

デバッグビルドではありますが、両方とも同じくらいの速度になりました。
Galaxy SⅢα で 17 FPS 程度。
ただ、コード量としては3倍くらいの差になり、AIRの圧勝でした。
iOS版への対応も楽でコードも短いとなったら、ネイティブコード書く理由はないですね!

あと、EclipseのほうのAndroidエミュレータだと糞重たくて1FPSとかになってたけど、
AIRのほうのエミュレータだと割とさくさく動いてました。
ますますAIRいいね!

あとは常駐できればアプリ完成が見えてくる・・・
ということでAIR Native Extensionを試してきます。

コード(AIR)

FPS.as

package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageQuality;
    import flash.display.StageScaleMode;
    import flash.events.Event;
    
    import starling.core.Starling;
    
    [SWF(width="600", height="1000", backgroundColor="0x000000", frameRate="60")]
    public class FPS extends Sprite{
        private var _starling:Starling;
        
        public function FPS() {
            addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
        }
        
        private function addedToStageHandler(e:Event):void {
            removeEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
            
            stage.align = StageAlign.TOP_LEFT;
            stage.quality = StageQuality.HIGH;
            stage.scaleMode = StageScaleMode.NO_SCALE;
            
            Starling.handleLostContext = true;
            _starling = new Starling(Main, stage);
            _starling.showStats = true;
            _starling.start(); 
            _starling.showStatsAt("right", "bottom", 5);
        }
    }
}

Main.as

package
{
    import starling.ImageEx;
    import starling.core.Starling;
    import starling.display.DisplayObjectContainer;
    import starling.display.Image;
    import starling.display.Sprite;
    import starling.events.Event;
    import starling.textures.Texture;
    
    public class Main extends Sprite
    {
        private var spriteNum:int = 500;
        private var sprites:Array = new Array;
        [Embed(source = "sample.png")] private static var tenshi:Class;
        
        public function Main(){
            addEventListener(starling.events.Event.ADDED_TO_STAGE, addedToStageHandler);
        }
        private function addedToStageHandler(e:starling.events.Event):void{
            removeEventListener(starling.events.Event.ADDED_TO_STAGE, addedToStageHandler);
            
            for(var i:int=0; i<spriteNum; i++){
                sprites[i] = makeImage(this, tenshi, true, Math.random()*500, Math.random()*500);
                move(sprites[i]);
            }
        }
        public function move(img:Image):void{
            Starling.juggler.tween(img, 1, {x:img.x+Math.random()*50-25, onComplete:function(){ move(img) } });
        }
        
        public static function makeImage(parent:DisplayObjectContainer, bmpClass:Class, visible:Boolean, x:Number=0, y:Number=0):Image{
            var image:Image = new Image(Texture.fromBitmap(new bmpClass()));
            image.x = x;
            image.y = y;
            parent.addChild(image);
            image.visible = visible;
            return image;
        }
    }
}

コード(Java)

MainActivity.java

package com.example.fps;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.widget.TextView;

public class MainActivity extends Activity  {
  SampleGlView glView;
  FpsTextView fps;
  TextView text;
  
  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      // ハンドラセット
      DrawStatic.setHandler(new Handler());
      setContentView(R.layout.activity_main);
      glView = (SampleGlView) findViewById(R.id.sampleGlView1);
      text = (TextView)findViewById(R.id.textView1);
      glView.setTextView(text);
  }
}

SampleGlView.java

package com.example.fps;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.widget.TextView;

public class SampleGlView extends GLSurfaceView {
  SampleRenderer renderer;
  public SampleGlView(Context context) {
    super(context);
    renderer = new SampleRenderer(context);
    setRenderer( renderer );
  }
  public SampleGlView(Context context, AttributeSet attrs) {
    super(context, attrs);
    renderer = new SampleRenderer(context);
    setRenderer( renderer );
  }
  public void setTextView(TextView v){
    renderer.textView = v;
  }
}

SampleRenderer.java

package com.example.fps;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;
import android.widget.TextView;

public class SampleRenderer implements Renderer{
  private Context context;
  public TextView textView;
  private int spriteNum = 500;
  SampleSprite[] tenshi_img = new SampleSprite[spriteNum];
  
  // フレームレート表示用
  private long mFpsCountStartTime = System.currentTimeMillis();
  private int mFramesInSecond = 0;
  private int mFps = 0;
  private Runnable fpsRunnable;
    {
      // フレームレート更新用の Runnable
      fpsRunnable = new Runnable(){
        @Override
        public void run() {
          textView.setText(mFps + "Fps");
        }
      };
    }
    
  public SampleRenderer(Context _context){
    context = _context;
    for(int i=0; i<spriteNum; i++){
      tenshi_img[i] = new SampleSprite();
    }
  }
  
  @Override
  public void onDrawFrame(GL10 gl) {
    // 描画用バッファをクリア
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    for(int i=0; i<spriteNum; i++){
      tenshi_img[i].draw(gl);
        tenshi_img[i].pos_x += Math.random()*5 - 2.5;
        if(tenshi_img[i].pos_x > 400) tenshi_img[i].pos_x = 0;
    }
    upFps();
  }
  
  @Override
  public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);
    gl.glMatrixMode(GL10.GL_PROJECTION);//プロジェクションモードに設定
    GLU.gluOrtho2D(gl, 0.0f, width, 0.0f, height);//平行投影用のパラメータをセット
  }
  
  @Override
  public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    //背景色をクリア
    gl.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    //ディザを無効化
    gl.glDisable(GL10.GL_DITHER);
    //深度テストを有効化
    gl.glEnable(GL10.GL_DEPTH_TEST);
    //テクスチャ機能ON
    gl.glEnable(GL10.GL_TEXTURE_2D);
    //透明可能に
    gl.glEnable(GL10.GL_ALPHA_TEST);
    //ブレンド可能に
    gl.glEnable(GL10.GL_BLEND);
    gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
    for(int i=0; i<spriteNum; i++){
      tenshi_img[i].setTexture(gl,context.getResources(), R.drawable.sample_img);
      tenshi_img[i].pos_x = (float)Math.random() * 500;
      tenshi_img[i].pos_y = (float)Math.random() * 500;
    }
  }
  private void upFps(){
    long nowTime = System.currentTimeMillis();// 現在時間を取得
    long difference = nowTime - mFpsCountStartTime;// 現在時間との差分を計算
    if (difference >= 1000) {// 1秒経過していた場合は、フレーム数のカウント終了
      mFps = mFramesInSecond;
      mFramesInSecond = 0;
      mFpsCountStartTime = nowTime;
      DrawStatic.addRunnable(fpsRunnable);
    }
    mFramesInSecond++;// フレーム数をカウント
  }
}

SampleSprite.java

package com.example.fps;
import java.io.IOException;
import java.io.InputStream;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11Ext;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
 
public class SampleSprite {
  //テクスチャNo
  public int textureNo;
    //表示位置
  float  pos_x;
  float  pos_y;
  float  pos_z;
  //テクスチャ(画像)の位置とサイズ
  int    texX;
  int    texY;
  int    texWidth;
  int    texHeight;
  //配置する時の幅と高さ
  float  width;
  float  height;
 
  public void setTexture( GL10 gl, Resources res, int id ){
    InputStream is = res.openRawResource(id);
    Bitmap bitmap;
    try{
      bitmap = BitmapFactory.decodeStream(is);
    }
    finally{
      try{
        is.close();
      }
      catch(IOException e){ }
    }
    gl.glEnable(GL10.GL_ALPHA_TEST);
    gl.glEnable(GL10.GL_BLEND);
    gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
    gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE);
    //テクスチャIDを割り当てる
    int[] textureID = new int[1];
    gl.glGenTextures(1, textureID, 0);
    textureNo = textureID[0];
 
    //テクスチャIDのバインド
    gl.glBindTexture(GL10.GL_TEXTURE_2D, textureNo);
    //OpenGL ES用のメモリ領域に画像データを渡す。上でバインドされたテクスチャIDと結び付けられる。
    GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
    //テクスチャ座標が1.0fを超えたときの、テクスチャを繰り返す設定
    gl.glTexParameterx(GL10.GL_TEXTURE_2D,GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT );
    gl.glTexParameterx(GL10.GL_TEXTURE_2D,GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT );
    //テクスチャを元のサイズから拡大、縮小して使用したときの色の使い方を設定
    gl.glTexParameterx(GL10.GL_TEXTURE_2D,GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR );
    gl.glTexParameterx(GL10.GL_TEXTURE_2D,GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR );
 
    texX      = 0;
    texY      = bitmap.getHeight();
    texWidth  = bitmap.getWidth();
    texHeight = -bitmap.getHeight();
    pos_x     = 0;
    pos_y     = 0; 
    pos_z     = 0;
    width     = bitmap.getWidth();
    height    = bitmap.getHeight();
  }
 
  public void draw( GL10 gl){
    gl.glDisable(GL10.GL_DEPTH_TEST);
    //背景色を白色で塗りつぶし
    gl.glColor4x(0x10000, 0x10000, 0x10000, 0x10000);
    //テクスチャ0番をアクティブにする
    gl.glActiveTexture(GL10.GL_TEXTURE0);
    //テクスチャIDに対応するテクスチャをバインドする
    gl.glBindTexture(GL10.GL_TEXTURE_2D, textureNo);
    //テクスチャの座標と幅と高さを指定
    int rect[] = { texX,  texY,  texWidth, texHeight};
    //テクスチャ画像のどの部分を使うかを指定
    ((GL11) gl).glTexParameteriv(GL10.GL_TEXTURE_2D,GL11Ext.GL_TEXTURE_CROP_RECT_OES, rect, 0);
    //描画
    ((GL11Ext) gl).glDrawTexfOES( pos_x, pos_y, pos_z, width, height);
    gl.glEnable(GL10.GL_DEPTH_TEST);
  }
}

コメント