【Android】OpenGLによる3Dアプリの作り方

「3Dアプリ作ってみたくない?」

こう聞かれたら私なら「作ってみたいです!」と即答しちゃいますね。

3Dって技術的にハードルが高そうですが、アプリを作ってみて案外そうでもないことが分かりました。
ただ、凝った背景作ったり、キャラをより立体的に表現しようとするなら、3Dモデリングっていう別のスキルが必要そうです。。。

OpenGLで作るAndroidSDKゲームプログラミング ←この本がめっちゃ参考になります。オススメです。
はい、こっからがサンプルの説明です。
こんな感じ↓↓のアプリをつくっちゃいましょう。

Screenshot_2014-04-22-18-59-07

3D空間にAndroid君を立たせてあげちゃいましょう。
解説はソースコードをご覧下さいませ。

3つのクラスを作ります。
①MainActivity.java      Activityを継承したクラス
②MyRenderer.java    GLSurfaceView.Rendererをimplementsしたクラス
③GraphicUtil.java     画像のロードや描画を担当する汎用クラス

 

MainActivity.java

import java.util.Timer;
import java.util.TimerTask;

import android.app.Activity;
import android.media.AudioManager;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.os.Handler;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends Activity {

    private MyRenderer myRenderer;
    private GLSurfaceView glSurfaceView;
    // ハンドラを生成
    private Handler handler = new Handler();
    public final static int TIMER_PERIOD = 20;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // フルスクリーン、タイトルバーの非表示
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setVolumeControlStream(AudioManager.STREAM_MUSIC);

        myRenderer = new MyRenderer(this); // MyRendererの生成
        glSurfaceView = new GLSurfaceView(this); // GLSurfaceViewの生成
        glSurfaceView.setRenderer(myRenderer); // GLSurfaceViewにMyRendererを適用

        // requestRender()が呼ばれた時だけMyRendereクラスのonDrawFrameメソッドを実行するようにするように設定変更
        // これなしの場合は、onDrawメソッドが定期的に実行される
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 
        setContentView(glSurfaceView); // ビューにGLSurfaceViewを指定

        // MyRendererのonDrawFrameメソッドを呼ぶためにタイマーを実装
        final Timer timer = new Timer(false);
        // スケジュールを設定
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {

                        // MyRendererのonDrawFrameメソッドを呼ぶ
                        glSurfaceView.requestRender();
                    }
                });
            }
        }, 1000, TIMER_PERIOD); // 初回起動の遅延(1sec)と周期(TIMER_PERIOD)の指定
    }

    @Override
    protected void onResume() {
        super.onResume();

        // GLSurfaceViewをonResumeにする
        glSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();

        // GLSurfaceViewをonPauseにする
        glSurfaceView.onPause();
    }

}

 

MyRenderer.java

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.opengl.GLSurfaceView;

public class MyRenderer implements GLSurfaceView.Renderer {

    private Context context;

    private int width;
    private int height;

    // テクスチャ管理のためのID
    private int androidTex, bottomTex, sideTex, backTex;

    public MyRenderer(Context context) {
        this.context = context;
    }

    public void renderMain(GL10 gl) {

        // 3D描画用に座標系を設定する
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glFrustumf(-0.3f, 0.3f, -0.2f, 0.2f, 0.5f, 20.0f);
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

        // 視点を変更する
        // 全体を回転および移動させる
        gl.glTranslatef(0.0f, 0.0f, -1.0f); // 全体を奥方向に移動させる
        gl.glRotatef(-70.0f, 1.0f, 0.0f, 0.0f); // x軸を中心に反時計回りに全体を70度回転させる

        gl.glEnable(GL10.GL_BLEND);// 背景を透明にするためブレンドを有効化
        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); // ブレンドの種類を設定

        // 地面の描画
        GraphicUtil.drawTexture(gl, 0.0f, 0.0f, 2.0f, 3.0f, bottomTex, 0.0f,
                0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);

        gl.glPushMatrix();
        {
            gl.glTranslatef(0.0f, 1.5f, 0.5f); // y軸方向に1.5f,z軸方向に0.5f移動させる
            gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); // x軸を中心に90度回転

            // 奥背景の描画
            GraphicUtil.drawTexture(gl, 0.0f, 0.0f, 2.0f, 1.0f, backTex, 0.0f,
                    0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
        }
        gl.glPopMatrix();

        gl.glPushMatrix();
        {

            gl.glTranslatef(1.0f, 0.0f, 0.5f); // x軸方向に1.0f移動させ、z軸方向に0.5f移動
            gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f); // y軸中心に90度回転
            gl.glRotatef(90.0f, 0.0f, 0.0f, 1.0f); // z軸中心に90度回転

            // 右横背景の描画
            GraphicUtil.drawTexture(gl, 0.0f, 0.0f, 3.0f, 1.0f, sideTex, 0.0f,
                    0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
        }
        gl.glPopMatrix();

        gl.glPushMatrix();
        {

            gl.glTranslatef(-1.0f, 0.0f, 0.5f); // x軸方向に-1.0f移動させ、z軸方向に0.5f移動
            gl.glRotatef(-90.0f, 0.0f, 1.0f, 0.0f); // y軸中心に-90度回転
            gl.glRotatef(-90.0f, 0.0f, 0.0f, 1.0f); // z軸中心に-90度回転

            // 左横背景の描画
            GraphicUtil.drawTexture(gl, 0.0f, 0.0f, 3.0f, 1.0f, sideTex, 0.0f,
                    0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
        }
        gl.glPopMatrix();

        gl.glPushMatrix();
        {
            gl.glTranslatef(0.0f, 0.0f, 0.15f); // z軸方向に0.15f移動させる
            gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); // x軸を中心に90度回転

            // Android君の描画
            GraphicUtil.drawTexture(gl, 0.0f, 0.0f, 0.3f, 0.3f, androidTex,
                    0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);

        }
        gl.glPopMatrix();

        gl.glDisable(GL10.GL_BLEND); // GL_BLENDを無効にする
    }

    // 描画処理を記述する
    @Override
    public void onDrawFrame(GL10 gl) {

        // ビュー内部で実際に描画する範囲を指定
        gl.glViewport(0, 0, width, height);
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();

        // glOrthof(左端のx座標, 右端のx座標, 下端のy座標, 上端のy座標, 手前のz座標, 奥のz座標)
        gl.glOrthof(-1.0f, 1.0f, -1.5f, 1.5f, 1.0f, -1.0f);
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

        // 画面をクリアする
        gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        renderMain(gl);
    }

    // 画面生成時、画面向き変更時に呼ばれる。初期化処理などを行う。
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

        // SurfaceViewのサイズを取得
        this.width = width;
        this.height = height;

        // テクスチャの画像を読み込む
        // 画像ファイルは2の階乗になっていないといけない。(128px*128px, 256px*256px, 512px*1024px...)
        androidTex = GraphicUtil.loadTexture(gl, context.getResources(),
                R.drawable.android);
        bottomTex = GraphicUtil.loadTexture(gl, context.getResources(),
                R.drawable.bottom);
        sideTex = GraphicUtil.loadTexture(gl, context.getResources(),
                R.drawable.side);
        backTex = GraphicUtil.loadTexture(gl, context.getResources(),
                R.drawable.back);
    }

    // 画面生成時、画面向き変更時に呼ばれる。初期化処理などを行う。
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    }

}

 

GraphicUtil.java

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Bitmap.Config;
import android.opengl.GLUtils;

public class GraphicUtil {

    // テクスチャをロードするためのメソッド
    public static final int loadTexture(GL10 gl, Resources resources, int resId) {

        int[] textures = new int[1];

        // Bitmapの作成
        Bitmap bmp = BitmapFactory.decodeResource(resources, resId, options);
        if (bmp == null) {
            return 0;
        }

        // OpenGL用のテクスチャを生成します
        gl.glGenTextures(1, textures, 0);
        gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
        GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bmp, 0);
        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
                GL10.GL_LINEAR);
        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
                GL10.GL_LINEAR);
        gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);

        // OpenGLへの転送が完了したので、VMメモリ上に作成したBitmapを破棄する
        bmp.recycle();

        return textures[0];
    }

    private static final BitmapFactory.Options options = new BitmapFactory.Options();
    static {
        options.inScaled = false;// リソースの自動リサイズをしない
        options.inPreferredConfig = Config.ARGB_8888;// 32bit画像として読み込む
    }

    // テクスチャを描画するためのメソッド
    public static final void drawTexture(GL10 gl, float x, float y, 
            float width, float height, int texture, float u, float v, 
            float tex_w, float tex_h, float r, float g, float b, float a) {

        // ポリゴンの頂点座標
        float[] vertices = {
            -0.5f * width + x, -0.5f * height + y,
             0.5f * width + x, -0.5f * height + y,
            -0.5f * width + x,  0.5f * height + y,
             0.5f * width + x,  0.5f * height + y,
        };

        // ポリゴンの頂点の色
        float[] colors = {
            r, g, b, a,
            r, g, b, a,
            r, g, b, a,
            r, g, b, a,
        };

        // マッピング座標
        float[] coords = {
                    u, v + tex_h,
            u + tex_w, v + tex_h,
                    u,         v,
            u + tex_w,         v,
        };

        // OpenGLではVM上に確保したメモリ領域にアクセスできないため、
        // 作成した配列をシステムメモリに転送する必要がある。
        FloatBuffer polygonVertices = makeFloatBuffer(vertices);
        FloatBuffer polygonColors = makeFloatBuffer(colors);
        FloatBuffer texCoords = makeFloatBuffer(coords);

        gl.glEnable(GL10.GL_TEXTURE_2D); // テクスチャ機能の有効化
        gl.glBindTexture(GL10.GL_TEXTURE_2D, texture); // テクスチャオブジェクトの指定(引数で取得する) 

        // glVertexPointer(1頂点あたりのデータ数, データ型, オフセット, 頂点配列)
        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, polygonVertices); // 確保したメモリをOpenGLに渡す
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // ポリゴン頂点座標のバッファをセットしたことをOpenGLに伝える
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, polygonColors); // 確保したメモリをOpenGLに渡す
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY); // ポリゴン頂点色のバッファをセットしたことをOpenGLに伝える
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texCoords); // 確保したメモリをOpenGLに渡す
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // マッピング座標のバッファをセットしたことをOpenGLに伝える

        // ポリゴンの描画には幾つか種類があり、引数で指定(GL10.GL_TRIANGLE_STRIP)
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4); // ポリゴンの描画

        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // 描画が終わったら、テクスチャマッピング用のバッファをリセット
        gl.glDisable(GL10.GL_TEXTURE_2D); // テクスチャ機能の無効化
    }

    // システム上のメモリ領域を確保するためのメソッド
    // float型の配列を入れるためのデータ領域を用意し、そこにデータを転送している。
    // 配列のサイズは[4 * 配列のサイズ]バイトとなっている。
    public static final FloatBuffer makeFloatBuffer(float[] arr) {
        ByteBuffer bb = ByteBuffer.allocateDirect(arr.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer fb = bb.asFloatBuffer();
        fb.put(arr);
        fb.position(0);
        return fb;
    }   
}

 

これでサンプルは以上となりますが、
MyRendererクラスのrenderMainメソッドについて、補足します。
このクラスでテクスチャを後ろに書いた順に、手前に表示されます。
なので、サンプルコードでは、Android君が最も手前で、次が左の背景となります。

gl.glPushMatrix();
        {
            gl.glTranslatef(0.0f, 0.0f, 0.15f); // z軸方向に0.15f移動させる
            gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); // x軸を中心に90度回転

            // Android君の描画
            GraphicUtil.drawTexture(gl, 0.0f, 0.0f, 0.3f, 0.3f, androidTex,
                    0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);

        }
gl.glPopMatrix();

このコードでAndroid君を描画するポリゴンを変形させています。

gl.glPushMatrix();

上記メソッドでくくった範囲のみ変形が適応されます。

変形が適応される順番としては、後に書いたコードが先に適応されます。
サンプルでいうと、gl.glRotatef()が先に適応されてgl.glTranslatef()が後になります。
ややこしいですね。注意しましょう。

 

今回紹介した技術を使えば、こんなアプリができちゃいます。↓↓

綱渡りで宝探し

コメントを残す

メールアドレスが公開されることはありません。