Java OpenGL ES を学習する~Androidの開発環境を作る~

OpenGLES 開発環境構築

表題の通りにJava言語でのOpenGL-ESの開発環境を構築したいと思います。参考にする本は以下のものです。

参考書籍

Javaでの実装には役に立たなかったが、OpenGLの処理内容を理解するのには、役立った。

Android版のセットアップ

書籍のほうでは、EclipseにADT(Android開発プラグイン)を入れて実装していました。(参照したのが古い本でした)なので、Android Studioをインストールして実行します。

Windowsでのセットアップですので、インストーラーをダブルクリックしておしまいです。

セットアップはあまり重要でなく、プログラムの実装とOpenGLの仕組みを理解することがメインになります。ですので開発環境が多少違っても
OpenGLのテクノロジーを使用することには変わりないので影響しません。そしてOpenGLはクロスプラットフォームなのでどのOSにも対応しています。
まぁC++でできていればどのOSでもしようできますね。※Java言語も負けてないです。

<インストーラーを起動したときの画像>

Android Studio

上記のインストーラーを早速起動します。そしたら、下の用が画面が出たので、「Send usage statics to Google」をクリックします。

そして、インストールタイプ(Install Type)はスタンダードを指定して、各ライセンス規定に同意する必要があります。
その後、インストールが始まります。IntelliJ IDEAがベースになっているので、AndroidStudioはIntelliJ IDEAと操作方法が似ているはずです。

インストール完了後は下のように表示されした。

ここから先はAndroid仕様で、作成するアクティビティを選択してから実行していきます。
今回は、EmptyComposedActivityを選択してプロジェクトを作成しました。本家のチュートリアルを参照して作成しました。すごーくわかりやすいです。日本語で読めました。

OpenGLのセットアップ

本家のチュートリアルを参考に行いました。

子のチュートリアルは下のような項目を行っていました。

  1. OpenGL ES 環境の構築
    • Android アプリケーションをセットアップして OpenGL グラフィックスを描画できるようにする方法について説明します。
  2. 図形の定義
    • 図形を定義する方法と、面とワインディングについて認識しておく必要がある理由について説明します。
  3. 図形の描画
    • アプリケーションで OpenGL 図形を描画する方法について説明します。
  4. 投影とカメラビューの適用
    • 投影とカメラビューを使用して、描画されたオブジェクトの新しい視点を取得する方法について説明します。
  5. モーションの追加
    • OpenGL を使って描画されたオブジェクトの基本的な動きとアニメーション化を行う方法について説明します。
  6. タップイベントへの応答

このうち
1から3まで実行したところです。
結果として、三角形と四角形の描画があるのですが、とりあえず三角形の描画を行いました。実機にデプロイできるようにエミュレータを使用しました。

補足、Activityは、初めの状態だと「AppCompatActivity」が継承(extends)されているのでそれを「Activity」に変更する必要がありました。

そして、実機でのテストをするのには、やはり本家のサイトを参考にしました。

  1. 自分のAndroid端末とPCのWifiを同じSSIDに接続する
  2. Android StudioでPair Device using Wifiを選択
  3. Android端末でデバックモードをONにする
  4. 表示されたQRコードをAndroid端末で読み込む
  5. AndroidStudioでテストを実行する

<実行結果>

サンプルコードの実装

作成したファイルは以下のファイルです。

  • MainActivity: これはプロジェクトを作成したときに生成されたものです。中身を修正しました。
  • MyGLRender: チュートリアルにあったクラスです。
  • MyGLSurfaceView: チュートリアルにあったクラスです。
  • Square: チュートリアルにあったクラスです。
  • Triangle: チュートリアルにあったクラスです。

それぞれ以下のように作成しました。※ほぼコピペ

MainActivity


import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;

public class MainActivity extends Activity /* AppCompatActivity */ {

    private GLSurfaceView gLView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
//        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        super.onCreate(savedInstanceState);

        gLView = new MyGLSurfaceView(this);
        setContentView(gLView);
    }
}

MyGLRender

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

import android.opengl.GLES20;
import android.opengl.GLSurfaceView;

public class MyGLRender implements GLSurfaceView.Renderer {

    private Triangle mTriangle;
    private Square   mSquare;

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        // initialize a triangle
        mTriangle = new Triangle();
        // initialize a square
        mSquare = new Square();    }

    public void onDrawFrame(GL10 gl) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        mTriangle.draw();
    }

    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }

    public static int loadShader(int type, String shaderCode){

        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
        int shader = GLES20.glCreateShader(type);

        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

        return shader;
    }
}

MyGLSurfaceView

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

public class MyGLSurfaceView extends GLSurfaceView {
    private final MyGLRender render;

    public MyGLSurfaceView(Context context) {
        super(context);
        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2);

        render = new MyGLRender();

        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(render);
        // Render the view only when there is a change in the drawing data
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

    }
}

Square

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

public class Square {

    private FloatBuffer vertexBuffer;
    private ShortBuffer drawListBuffer;

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;
    static float squareCoords[] = {
            -0.5f,  0.5f, 0.0f,   // top left
            -0.5f, -0.5f, 0.0f,   // bottom left
            0.5f, -0.5f, 0.0f,   // bottom right
            0.5f,  0.5f, 0.0f }; // top right

    private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices

    public Square() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 4 bytes per float)
                squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);

        // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 2 bytes per short)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
    }
}

Triangle

import android.opengl.GLES20;

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

public class Triangle {

    private FloatBuffer vertexBuffer;
    private final int mProgram;

    private int positionHandle;
    private int colorHandle;

    private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;
    static float triangleCoords[] = {   // in counterclockwise order:
            0.0f,  0.622008459f, 0.0f, // top
            -0.5f, -0.311004243f, 0.0f, // bottom left
            0.5f, -0.311004243f, 0.0f  // bottom right
    };

    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                    "void main() {" +
                    "  gl_Position = vPosition;" +
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";

    // Set color with red, green, blue and alpha (opacity) values
    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    public Triangle() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (number of coordinate values * 4 bytes per float)
                triangleCoords.length * 4);
        // use the device hardware's native byte order
        bb.order(ByteOrder.nativeOrder());

        // create a floating point buffer from the ByteBuffer
        vertexBuffer = bb.asFloatBuffer();
        // add the coordinates to the FloatBuffer
        vertexBuffer.put(triangleCoords);
        // set the buffer to read the first coordinate
        vertexBuffer.position(0);

        int vertexShader = MyGLRender.loadShader(GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        int fragmentShader = MyGLRender.loadShader(GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram();

        // add the vertex shader to program
        GLES20.glAttachShader(mProgram, vertexShader);

        // add the fragment shader to program
        GLES20.glAttachShader(mProgram, fragmentShader);

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(mProgram);
    }

    public void draw() {
        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram);

        // get handle to vertex shader's vPosition member
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(positionHandle);

        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false,
                vertexStride, vertexBuffer);

        // get handle to fragment shader's vColor member
        colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

        // Set color for drawing the triangle
        GLES20.glUniform4fv(colorHandle, 1, color, 0);

        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(positionHandle);
    }
}

環境構築のまとめ

チュートリアルをそのまま実行すればすぐに環境構築ができました。KotlinでもJavaでも実装ができるので良いと思いました。
次は、2次元描画を行いたいところです。そして、この2次元描画を少し丹念に学習したいと思っております。

コード解析

下記の5クラスを実装しました。

  1. MainActivity
  2. MyGLRender
  3. MyGLSurfaceView
  4. Square
  5. Triangle

これらの関係性を表すのに、UMLを書きました。

こんな感じです。

処理概要

下のような感じです。

  • MainActivityでAndroidの画面を作成します。
  • MyGLSurfaceViewでOpenGLの描画を行うパレットのようなものを作成します。
  • MyGLRenderで実際に描画する三角形とか四角形を描画

処理内容: MainActivity

これはAndroidの画面を作成するクラスです。アンドロイドのAPIで定義されているActivittyクラスを継承して実装します。

  • 「extends AppCompatActivity」になっていたところを「Activity」に変更しています。
  • 「onCreate」メソッドをオーバーライドしています。
    • 親クラスで呼び出しているonCreateを上書きするので、こちらのメソッドが動きます。
  • フィールド変数にOpenGLのGLSurfaceViewを定義しています。これに、自作のMyGLSurfaceViewをセットします。
public class MainActivity extends Activity /* AppCompatActivity */ {

    private GLSurfaceView gLView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
//        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        super.onCreate(savedInstanceState);

        gLView = new MyGLSurfaceView(this);
        setContentView(gLView);
    }
}

処理内容: MyGLRender

このクラスは、「MyGLRender」クラスをGLSurfaceView#setRendererにセットしています。
その他、細かい設定をしています。

public class MyGLSurfaceView extends GLSurfaceView {
    private final MyGLRender render;

    public MyGLSurfaceView(Context context) {
        super(context);
        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2);

        render = new MyGLRender();

        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(render);
        // Render the view only when there is a change in the drawing data
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

    }
}

処理内容:

「GLSurfaceView.Renderer」をimeplementsすることで、描画処理を行います。
それぞれ「\@Override」アノテーションがついているメソッドがインタフェースでオーバーライドを強制するメソッドになります。

コメントに何をしているか書いています。

  • 「Set the background frame color」=背景枠の色を設定する
  • 「initialize a triangle」=三角形初期化
  • 「initialize a square」=四角形初期化
  • 「Redraw background color」=背景の再描画
public class MyGLRender implements GLSurfaceView.Renderer {

    private Triangle mTriangle;
    private Square   mSquare;

    @Override
    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        // initialize a triangle
        mTriangle = new Triangle();
        // initialize a square
        mSquare = new Square();
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        mTriangle.draw();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }

    public static int loadShader(int type, String shaderCode){

        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
        int shader = GLES20.glCreateShader(type);

        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

        return shader;
    }
}

処理内容:Triangle

これは、長くなってしまうので、文言のみにしますが、

  1. コンストラクタ(Triangle())で描画の準備
  2. draw()で描画処理

上記を行っています。

処理内容:Square

こちらのクラスの描画処理は見当たらなかったので、改めて実装しました。
結論から言うと、三角形の描画メソッドとほぼ同じでした。違うのは、描画のためのデータ、以下のものです。

  • squareCoords: 四角形の各頂点定義
  • 描画モードの設定
    • GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount); // 三角形の描画
    • GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount); // 四角形の描画
package jp.zenryoku.firstapp;

import android.opengl.GLES20;

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

public class Square {

    private FloatBuffer vertexBuffer;
    private ShortBuffer drawListBuffer;
    private final int mProgram;

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;

    private int positionHandle;

    static float squareCoords[] = {
            -0.5f,  0.5f, 0.0f,   // top left
            -0.5f, -0.5f, 0.0f,   // bottom left
            0.5f, -0.5f, 0.0f,   // bottom right
            0.5f,  0.5f, 0.0f }; // top right

    private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices

    private int colorHandle;

    private final int vertexCount = squareCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
    // Set color with red, green, blue and alpha (opacity) values
    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                    "void main() {" +
                    "  gl_Position = vPosition;" +
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";

    public Square() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 4 bytes per float)
                squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);

        // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 2 bytes per short)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram();

        int vertexShader = MyGLRender.loadShader(GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        int fragmentShader = MyGLRender.loadShader(GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);
        // add the vertex shader to program
        GLES20.glAttachShader(mProgram, vertexShader);

        // add the fragment shader to program
        GLES20.glAttachShader(mProgram, fragmentShader);

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(mProgram);
    }

    public void draw() {
        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram);

        // get handle to vertex shader's vPosition member
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(positionHandle);

        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false,
                vertexStride, vertexBuffer);

        // get handle to fragment shader's vColor member
        colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

        // Set color for drawing the triangle
        GLES20.glUniform4fv(colorHandle, 1, color, 0);

        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount);

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(positionHandle);
    }
}

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。

コメントを残す