AndroidタブレットにOpenGLで3D空間を作って立体を並べるサンプル集
(Android Studio + JAVA でOpenGL ES2を使用する)
Nov 2017 coskx

1.はじめに


Android Studioの開発環境でOpenGL ES2が使いやすくなっています。
しかし,座標変換およびOpenGL特有の表現などわかりにくいところがあり, チュートリアル解説を行ないます。

チュートリアルですが,最初から舞台裏の様々な設定が見えてしまっています。説明しているところだけ理解してください。
説明してない部分は悩まずに先に進んでください。

ただし,Javaのクラス,インスタンス,コンストラクタ,メソッド,staticメソッドなどは,事前に学習しているものとします。

内容
補足
ここに載せているサンプルプログラムは次の機種で動作を確認している
    Android Studio Emulator -Nexus_5_API_22
    Nexus7 (Asus MeMO ME370T) Android version 4.4.2
    Nexus7 (Asus MeMO ME370T) Android version 5.5.1
    ASUS MeMO Pad 7 ME176-BK16 Android version 4.4.2
  Nexus 7 ME571-16G Android version 5.1.1
Android Studio 1.4 1.5.1 付近 にてlayoutが表示されなくなった時の対処(おそらく一時的なバグ)
    content_main.xmlにて
        xmlns:tools="http://schemas.android.com/tools"
        tools:context="com.example.tommy.sunearthmoonsystem.MainActivity"
        tools:showIn="@layout/activity_main"
    を削除 注意 最後の > を消してはいけない





2.3次元空間のxyz軸を描いた舞台・Android Studioでサンプルプログラム


2.1 サンプルプログラム
最初は,ダウンロードしたjavaソースファイルをandroid studioのプロジェクトに貼り付けて,実行します。
Android Studioを立ち上げて,新規プロジェクトでGLES2Sample01を作成します。
(プロジェクトの名前だけ「
GLES2Sample01」にして,後は BlankActivity Basic Activity デフォルト設定のまま作成します。)

まだ何もしていない段階のプロジェクトが図2.1の様に立ち上がってきます。
AVD(Android Virtual Device)が設定されていなかったら,設定してください。
現時点ではNexus5API22がAVDのデフォルトと思いますが,それを使うで良いと思います。
画面上の「layout」の構成(activity_main.xml)がこれと異なっていても大丈夫です。



次のファイルをダウンロードして,作業に使います。
gles2sample01.zip 


図2.1
                 図2.1 プロジェクト開始直後

ダウンロードして解凍した6個のファイルをすべてコピーします。
図2.1の左側の,Project -> Android 中の java -> com.xxxx.xxx を右クリックして,ペーストします。
(数回確認があるのですべてOK,Overwrite)
各ソースファイルに書いてあるpackage名は自動的に更新されます。
図2.2のようにクラスが増えます。
RunメニューからAppRunを選び,AVDとして既定のものを選ぶと,エミュレータ上で,動作します。

もし Empty Activity でプロジェクトを作ってしまったら,コンパイル時に,エラーが起きますが,
MainActivity.java中の

public boolean onCreateOptionsMenu(Menu menu)
public boolean onOptionsItemSelected(MenuItem item)
の2つのメソッドを削除すればうまくいきます。

PCによってはエミュレータの起動に時間がかかる場合があります。(充電中の表示が出たらスワイプ)
AndroidタブレットやスマートフォンをUSBケーブルで接続しておくと,(開発者モードでUSBデバッグができるようにしておく。そのやり方は一般のアプリ開発と同じなので他のサイトで確認してください。)それらの上で図2.3のように動作します。
配布するための実行ファイルの作り方メモ

図2.3画面上でスワイプすると,座標軸が回転します。
図2.3に見えている座標系は後に出てくる「ワールド座標系」です。


ソースファイル 軸の表示
図2.2 ソースファイル(クラス)が
取り込まれたところ

図2.3 画面表示
(xyz軸が表示される。画面スワイプで軸が回転)



●layoutが気になったら読んでください。
「画面上の「layout」の構成(activity_main.xml)がこれと異なっていても大丈夫です。」と書きましたが,デフォルトのlayoutの設定 はandroid studioのバージョンに依存します。これを無視するために,MainActivity.javaを修正して,デフォルトのlayout設定を無視するようにしています。

MainActivity.java のprotected void onCreate(Bundle savedInstanceState) 中の
setContentView(R.layout.activity_main);
をコメントアウトして
setContentView(glView);
で画面表示を設定しています。
コメントアウトされたsetContentView(R.layout.activity_main); はプロジェクト中のres\layout \activity_main.xmlに定義されている画面設定を使って画面を構成するという意味ですが,OpenGLの描画に専念するため,ここでは使 いません。
res\layout\activity_main.xmlを使用した例は付録1で紹介します。




2.2 サンプルプログラムの簡単な説明

いきなり6個のファイルが出てきましたが,次のような内容です。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
               この部分は当面見ないようにします。シェーダの解説まで
               楽しみにとっておきましょう。
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
               軸表示はここでしている。
    Axes.java                3軸表示用プリミティブ
               コンストラクタで,座標軸を表す点列を定義し,
               メソッドdrawで表示できる。
    BufferUtil.java          OpenGL_ESで用いるバッファ(舞台裏で使う)

これら全部を今は見ないようにして,要点だけ見てみましょう。

(1)GLRenderer.java
GLRenderer.javaの一部だけ見ましょう。

public void onDrawFrame(GL10 glUnused)
の最後のところの,

        //座標軸の描画
        //モデル変換行列mMatrixを単位行列にする。
        GLES.disableShading(); //シェーディング機能は使わない
        Matrix.setIdentityM(mMatrix, 0);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        //座標軸の描画本体
        //引数 r, g, b, a, shininess(1以上の値 大きな値ほど鋭くなる), linewidth
        //shininessは使用していない
        MyAxes.draw(1f, 1f, 1f, 1f, 10.f, 2f);//座標軸の描画本体
        GLES.enableShading(); //シェーディング機能を使う設定に戻す

ここで座標軸を表示しています。
シェーディング機能すなわち光源による効果は使わないで描きます。
(シェーダについては後述しますので,今は悩まないでください。)

メソッドMyAxes.draw呼び出し部の最初の3つの引数がred,green,blueを表しています。
1fと言うのは1.0f(float型の1.0)のことです。(OpenGLでは実数型にdouble型は使わないでfloat型を使います。)
色指定の各値は0fから1fの範囲で値を与えられます。適当に値を変えると軸の色が変わります。
引数の最後の値2fは線の幅です。

public void onSurfaceCreated(GL10 gl10,EGLConfig eglConfig)
中には,
        //背景色の設定
        GLES20.glClearColor(0f, 0f, 0.2f, 1.0f);

というのが有りますが,ここも3つの引数がred,green,blueを表しています。
適当に変えると背景色が変化します。

ここに出てきたonDrawFrame()は自動的に繰り返し呼びだされ,描画しています。描画内容を変更することでアニメーションのように振る舞います。ですから,描きたいものはonDrawFrame()の中にかけば良いことになります。


(2)Axes.java
Axes.java内のAxesのコンストラクタでは,軸や文字を折れ線を表す点列をvertexsに作り,描く点列の順番をindexsに作っていま す。配列vertexsは,次のような値で構成されています。点の数は座標軸:14個,文字X:5個,文字Y:5個,文字Z:4個です。
(vertexは頂点の意味です。)
(本来は点を表すx,y,zの3つの値を点の概念のデータとして,そのデータの配列をつくればよいのにの考えるが,構造を単純化するために次のようなデータ構造をとっているようだ。)
0番目の点のx座標,0番目の点のy座標,0番目の点のz座標,1番目の点のx座標,1番目の点のy座標,1番目の点のz座標,
2番目の点のx座標,2番目の点のy座標,2番目の点のz座標,3番目の点のx座標,3番目の点のy座標,3番目の点のz座標,
   :
((点の数×3)個の配列要素数になる)
そして,メソッドMyAxes.drawでは,描画をしますが,実際の描画作業は後で説明するシェーダが行います。

        //頂点点列
        GLES20.glVertexAttribPointer(GLES.positionHandle, 3,
                GLES20.GL_FLOAT, false, 0, vertexBuffer);

で点列をシェーダに送り,(ただし点列配列のままではなく,vertexBufferというバッファ形式に変換してから送っています。)
各引数の意味は大体次のように読みます。
GLES.positionHandle:送り先
3:データ1つの大きさは3個(x座標,y座標,z座標の3個で1データ)
GLES20.GL_FLOAT:float型
false:正規化作業不要
0:データのオフセット
vertexBuffer:バッファ形式に格納された点列データ
 
        //P0から14個の連続点で,正方向を示す矢と共に座標軸を一気に描く(都合上,同じ線上を2度通るところもある)
        indexBuffer.position(0);
        GLES20.glDrawElements(GLES20.GL_LINE_STRIP,
                14, GLES20.GL_UNSIGNED_BYTE, indexBuffer);

これで,シェーダに座標軸の点列の表示順を与えて,描画させています。
「GL_LINE_STRIP」は与えた点列で連続折れ線を描画するモードです。14は与えている点の数です。

それに引き続いて,文字X,Y,Zを描画しています。


(3)OpenGLの描画のまとめ

ここでOpenGLの描画についてまとめておきましょう。
OpenGLではGLRenderer.java中の関数onDrawFrame()が繰り返し呼びだされます。
onDrawFrame()中では,
1)表示のための変換行列mMatrixをGLES.updateMatrix(mMatrix);で与えて,
2)MyAxes.draw(1f, 1f, 1f, 1f, 10.f, 2f);にて予め作成してあった座標軸用の点列(Axesのコンストラクタが作成していた)を,表示させています。
MyAxes.draw()中では関数GLES20.glDrawElements()がシェーダに描画させています。


(4)その他
スワイプ(指を画面上で動かす動作)は MyGLSurfaceView.java の

public boolean onScroll(MotionEvent event1, MotionEvent event2, float distx, float disty)
で受け取って,GLRenderer.java の

public void setScrollValue(float[] Scroll)

に送り,カメラビュー変換でalphとbetaとして使われています。
スワイプによって得られている座標変換に関しては,後の章で説明しますが,このように俯瞰する角度から図形を見られるようにする変換はティルティング(tilting)変換と呼ばれます。


3.3次元空間のxyz軸を描いた舞台に立方体を置き,シェーディングする。


3.1 サンプルプログラム

2.で作成した舞台(3次元空間)の原点に立方体を描きます。立方体は新たに取り入れたCube.javaのクラスで定義されています。

次のファイルをダウンロードして,作業に使います。
gles2sample02.zip 

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
               コンストラクタで,座標軸を表す点列を定義し,
               メソッドdrawで表示できる。
    BufferUtil.java          OpenGL_ESで用いるバッファ(舞台裏で使う)

2.と同じように新規プロジェクトGLES2Sample02をBasic Activity デフォルト設定のまま作成します。
作業すると,シェーディングと言って,光の来る方向により面の明るさが変化する様子を再現し,図3.1のように表示されます。
舞台装置のからくりに関しては後で説明します。

3.2 GLRenderer.java
この舞台上では,デフォルトでシェーディングを施すのがデフォルトになっていて,
GLRenderer.java内の
    private  float[] LightPos={0f,1.5f,3f,1f};//x,y,z,1
で光源の位置を設定しています。そのため,立方体を下から見上げるようにすると図3.1のように鏡面反射するのが時々見られます。

シェーディング機能のデフォルトの設定は,public void onDrawFrame(GL10 glUnused)の最初に
    GLES.enableShading();   //シェーディング機能を有効にする。(デフォルト)
で設定しています。

この後に続くプログラムで,図形によってシェーディングを利用して描画するかしないかを指示することは重要なポイントになります。
    GLES.enableShading();    //シェーディング機能を有効にする。
    GLES.disableShading();   //シェーディング機能を無効にする。
の指示に注意してください。

立方体はGLRenderer.java内の
    private Cube MyCube = new Cube(); //原点に,外接球半径1の立方体オブジェクトを作成
で作られます。

public void onDrawFrame(GL10 glUnused)
の最後のところで,座標軸を描く作業の次に,立方体を描く作業をしています。

        //MyCubeの描画
        Matrix.setIdentityM(mMatrix, 0);  //ここではすでに設定されているので省略可
        Matrix.rotateM(mMatrix, 0, angle * 2, 0, 1, 0);
        Matrix.scaleM(mMatrix, 0, 0.6f, 0.6f, 0.6f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        //MyCubeの描画本体
        // r, g, b, a, shininess(1以上の値 大きな値ほど鋭くなる)
        MyCube.draw(0f, 1f, 0f, 1f, 20.f);


立方体が,回転しているのは,変数angleのためで,onDrawFrameが呼び出されるたびに1度ずつ増加して,
    Matrix.rotateM(mMatrix, 0, angle * 2, 0, 1, 0);
で立方体を回転しています。(回転角度の単位は[deg]です。)詳細は4.4参照してください。

    Matrix.scaleM(mMatrix, 0, 0.6f, 0.6f, 0.6f);
は立方体をx,y,z各方向の倍率を設定しています。ここではすべて0.6倍ですが,異なる値を設定すると直方体を描くことが出来ます。
詳細は4.4参照してください。
 

cube1
cube2
図3.1 ななめ上から見た立方体
図3.2 ななめ下から見た立方体
鏡面反射の明るい面が見える



座標変換およびシェーディングの表現については後で説明します。

3.3 Cube.java
Cube.javaのコンストラクタで,次の3.4に示す方法で立方体を定義します。

OpenGLは図形としては,点,線,三角形を描くことが出来ます。そこで,1つの正方形を描くのに2つの三角形を使うことにして,三角形を複数描くことで立方体を描けるように,立方体を定義します。
そして,メソッドdrawで表示します。メソッドdrawでは,連続三角形(次の3.4で説明します)を表示する方法で,描いています。

        //描画をシェーダに指示
        indexBuffer.position(0);
        GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP,
                28, GLES20.GL_UNSIGNED_BYTE, indexBuffer);

ここで28は頂点の数であり,「GL_TRIANGLE_STRIP」の指示は,連続三角形を表します。

もう一度描画の流れを振り返っておきましょう。
OpenGLではGLRenderer.java中の関数onDrawFrame()が繰り返し呼びだされます。
onDrawFrame()中では,
1)表示のための変換行列mMatrixをGLES.updateMatrix(mMatrix);で与えて,
2)Cube.draw(1f, 1f, 1f, 1f, 10.f, 2f);にて予め作成してあった立方体の点列(Cubeのコンストラクタが作成していた)を,表示させています。
Cube.draw()中では関数GLES20.glDrawElements()がシェーダに描画させています。


3.4 連続三角形の記述

OpenGLでは,三角形を描きます。立方体を描くためには正方形を描くことが必要ですが,正方形を描くには2つの直角二等辺三角形を描く事になります。
四角形を描くために,三角形を描くことを考えてみます。
複数の三角形を描く方法は2通りあります。

(1)個々の三角形を複数個描く (
GLES20.GL_TRIANGLES

図3.3において,「三角形 P0,P1,P2を描け」,「三角形P2,P1,P3を描け」,「三角形 P2,P3,P4を描け」,・・・,「三角形P9,P10,P11を描け」と8個の指示をすると,この図形が描けます。さて,CGの世界の習慣として,点列の指定にお いては, 表から見て反時計回り(CCW)に指示することでその面が表面であることを示します。逆回りに指示すると裏面であることを示します。立体図形を描く時は, 裏面は描かないオプションを指定します。そのため,三角形の定義は常に反時計回りに点を指定します。図3.3は平面図形ですが,すべての三角形が同一平面 上になくても同じことが言えます。

(2)連続三角形を描く(その1) (
GLES20.GL_TRIANGLE_STRIP

連続三角形を描く方法があります。この場合は「連続三角形P0,P1,P2,P3,P4,P5,P6を描け」,「連続三角形 P7,P8,P9,P10,P11を描け」の2つの指示で図3.3が描けます。連続三角形の場合は最初の三角形の点列の指定のみ反時計回り(CCW)に指示すれば,その後は 反時計回り(CW)と時計回り(CCW)が交互に指示されますが,これで連続三角形の表面側を描くことになります。そして図形全体を描くことが出来ます。

ここで「連続三角形P0,P1,P2,P3,P4,P5,P6を描け」では,次のように三角形が描かれます。
P0,P1,P2(CCW)-P1,P2,P3(CW)-P2,P3,P4(CCW)-P3,P4,P5(CW)-P4,P5,P6(CCW)
また,「連続三角形P7,P8,P9,P10,P11を描け」では,次のように三角形が描かれます。
P7,P8,P9(CCW)-P8,P9,P10(CW)-P9,P10,P11(CCW)

(3)連続三角形を描く(その2) (GLES20.GL_TRIANGLE_STRIP)

さらに,3点を追加し,「連続三角形P0,P1,P2,P3,P4,P5,P6,P6,P6,P7,P7,P8,P9,P10,P11 を描け」という1つの指示で図3.3を描くことが出来ます。
3点とは 「P6,P6,P7」です。(P6,P7,P7の3点でもOK)このとき次のように三角形が描かれます。

P0,P1,P2(CCW)-P1,P2,P3(CW)-P2,P3,P4(CCW)-P3,P4,P5(CW)-P4,P5,P6(CCW)-
P5,P6,P6(CW縮退三角形)-P6,P6,P6(CCW縮退三角形)-P6,P6,P7(CW縮退三角形)-
P6,P7,P7(CCW縮退三角形)-P7,P7,P8(CW縮退三角形)
-P7,P8,P9(CCW)-P8,P9,P10(CW)-P9,P10,P11(CCW)

このように,5つの見えない三角形(縮退三角形)を追加したことになります。この5つの三角形では3つの頂点のうち2つが重なっているため,三角形は描かれ ません。

描画指示は実行時の初期化作業が重いため,描画指示回数をできるだけ減らすほうがよいので,多くの三角形群を描くときはこの方法がよく用いられます。

複数の三角形群を描くときに,縮退三角形を用いるには,先行描画群の終端点と次描画群の先頭点の2つは必ず重ねます。そして次描画群の最初の三角形が反時計回りに始まるように必要ならもう一点くわえます。
原理は面倒ですが,不連続になる部分では,不連続部分の両側の点をいくつか重ねる(上記の例は3点重ねています)と考えればよいでしょう。
いくつ重ねるかは先行描画群の連続三角形を描くために用いた頂点の数によります。

先行描画群の連続三角形を描くために用いた頂点の数が偶数のとき :挿入する点の数は2個
先行描画群の連続三角形を描くために用いた頂点の数が奇数のとき :挿入する点の数は3個(図3.3の例はこちら)


triangle
      図3.3 連続三角形の記述


参考 GL_XXXXXの表現


3.5 立方体の記述

OpenGLは図形としては,点,線,三角形を描くことが出来ます。そこで,1つの正方形を描くのに2つの三角形を使うことにして,三角形を複数描くことで立方体を描けるように,立方体を定義します。
そして,メソッドdrawで表示します。メソッドdrawでは,連続三角形(次の3.4で説明します)を表示する方法で,描いています。

        //描画をシェーダに指示
        indexBuffer.position(0);
        GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP,
                24, GLES20.GL_UNSIGNED_BYTE, indexBuffer);

ここで24は指示する頂点の数であり,「GL_TRIANGLE_STRIP」の指示は,連続三角形を表します。シェーディング(グーローシェーディン グ)のときには,面を構成するすべての頂点における「面の法線ベクトル」(図3.4)が必要になりますが,OpenGLでは,三角形に分割して面を構成する ようになっていて,3つの頂点での法線ベクトルと光源の位置関係で頂点の色を決定し,三角形を 構成するすべての内部の点の色は,3つの頂点の色から内挿して決めるようになっています。(後に述べるフラグメントシェーダの作業内容です)そのため,三角形の3つの頂点における「面の法線ベクトル」(3つあるが,この3つは同じベクトルになる)を必要とします。
normalvector
図3.4 面の法線ベクトル(大きさは1)


立方体は8この頂点を持ちますが,法線ベクトルを点ごとに指定しなければならないことを考慮して,6つの正方形(ただし1つの正方形は2つの直角二等辺三角形で表現する)を描くことになるため,6×4=24個の頂点を定義することによって立方体を表します。
(例えば,P0,P19,P14は同じ座標を持つ点ですが,その点の属する面が異なるため,その点の持つ法線ベクトルが異なるため,別の点として扱わなければなりません。)

Cube.javaのコンストラクタにおいて,頂点番号と,頂点の座標は図3.5のように表すことが出来ます。そして,
点列配列vertexsを作っています。各頂点における「面の法線ベクトル」配列normalsも図3.5を見ながら作ります。
1つの面は1つの法線ベクトルしか持たないが,OpenGLでは,1つの面を構成する頂点(ここでは4頂点)全てに同じ法線ベクトルを定義して,その1つの面の法線ベクトルを指定したことになります。

PrimitiveCube
          図3.5 立方体を構成する頂点の定義

連続三角形描画の考えにより,立方体を表す点番号指示配列indexsを作っています。
図3.5の上面だけなら,「連続三角形P0,P1,P2,P3」 を描けとなります。また左側面だけなら,「連続三角形P4,P5,P6,P7」を描けとなります。これを次けて描くには縮退三角形が入ってしまうことを覚 悟で,「連続三角形P0,P1,P2,P3,P4,P5,P6,P7」を描けとなります。そして立方体内に三角形が表示されてしまうが,外部から見えない のでそのまま放置して,P23まで登録します。(全部で24個)
そうすると,立方体表示のための三角形の連鎖の登録は次のようになります。
(外側から見て反時計回り(CCW)で開始して登録)

P0,P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12,P13,P14,P15,P16,P17,P18,P19,P20,P21,P22,P23

この登録により,次のように前の方から3点ずつで三角形が描かれます。
(CCWとCWの登録が交互に現れ,縮退して描かれない三角形,および立方体の内部に入ってしまい外部から見えない三角形もある。)

P0,P1,P2(CCW)-P1,P2,P3(CW)-
P2,P3,P4(CCW縮退三角形)-P3,P4,P5(CW縮退三角形)-
P4,P5,P6(CCW)-P5,P6,P7(CW)-
P6,P7,P8(CCW縮退三角形)-P7,P8,P9(CW縮退三角形)-
P8,P9,P10(CCW)-P9,P10,P11(CW)-
P10,P11,P12(CCW縮退三角形)-P11,P12,P13(CW縮退三角形)-
P12,P13,P14(CCW)-P13,P14,P15(CW)-
P14,P15,P16(CCW外部から見えない三角形)-P15,P16,P17(CW外部から見えない三形)-
P16,P17,P18(CCW)-P17,P18,P19(CW)-
P18,P19,P20(CCW外部から見えない三角形)-P19,P20,P21(CW外部から見えない三角形)-
P20,P21,P22(CCW)-P21,P22,P23(CW)

練習問題
正四面体,正八面体のクラスを作って,表示するアプリを作成してみてください。正八面体の方が容易です。
また,この2つは三角形で出来ているため,連続三角形を用いずに三角形の羅列で描いた方が容易です。
        GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP,
                numIndexs, GLES20.GL_UNSIGNED_BYTE, indexBuffer);
        GLES20.glDrawElements(GLES20.GL_TRIANGLES,
                numIndexs, GLES20.GL_UNSIGNED_BYTE, indexBuffer);

GL_TRIANGLE_STRIP:連続三角形を描く (3.4参照)
GL_TRIANGLES :  孤立した三角形を連続して描く (3点ずつで三角形を連続して描く)

参考 
正四面体の作り方正八面体の作り方 ,正十二面体の作り方正二十面体の作り方
参考 GL_XXXXXの表現



4.座標変換の復習。


4.1 座標変換と右手座標系

3次元空間における座標変換(広義の座標変換)は,図形を固定したまま,座標軸を動かす「座標変換(狭義の座標変換)」と,座標軸を固定したまま図形を動 かす「幾何学的変換」がありますが,この2つは逆方向の変換となります。例えば,図形を固定したまま座標軸を左にr移動するのと,座標軸を固定して図形を右にr移 動するのとは,同じ結果になります。そのため,ここでは「幾何学的変換」を扱います。その際,右手座標系(図4.1)を用います。
coordinate3D.gif
  図4.1 右手座標系


4.2 主な座標変換と変換行列

主な座標変換には
の3つがあります。
3次元座標をベクトルで表し,変換を行列で表すと,変換行列とベクトルの積として変換を表すことができます。3つの変換を統一的に表すには,x,y,zに もう1つの要素wを加えた,同次座標が使用されます。同次座標系では1点は4つの要素を持つ列ベクトルで表され,変換は16個の要素を持つ変換行列(正方行列)で表されます。


ここで述べる変換は1つの点を移動させるものですが,三角形を構成する3つの頂点をそれぞれ同じ変換で移動させて,移動させた点で三角形を作ると,元の三 角形を移動させたことになります。同様に立方体の8つの頂点をそれぞれ同じ変換で移動させて,移動させた点で立方体を作ると,元の立方体を移動させたことになります。

(1) 各座標軸について平行移動
expTrans3D.jpg
(2) 各座標軸周りの回転
回転はx軸周り,y軸周り,z軸周りの3つ があります。各軸周りの回転では,軸の正方向にねじを置き,ねじが軸の正方向に進むように回転させる向きを正の回転方向とします。例えば,z軸の正方向に ねじを置き,ねじがz軸の正方向に進むように回転させる向き(xy平面で反時計回り)を正の回転方向とします。(図4.2参照)
rotetingDirection.gif
  図4.2 回転の向き

(2.1) z軸回りの回転
expRotZ3D.jpg
(2.2) x軸回りの回転
expRotX3D.jpg
(2.3) y軸回りの回転
expRotY3D.jpg

(3) スケーリング(拡大縮小,座標軸方向ごとに倍率を設定できます)
expScale3D.jpg
ここに述べた変換行列は,点を移動するための変換です。ある図形をこれらの変換で移動するということは,図形を構成する個々の点に対してこれらの変換を施すことになります。そうすることで,図形を移動することが可能になります。




4.3 連続変換と変換行列

座標変換でもう一つの大事な内容は,連続変換です。
例えば,ある図形をy軸周りに60度回転して,その後x軸方向に2移動したとします。
これは元の点を表すベクトルPに「y軸周りに60度回転させる」変換行列M1をかけて,P'を求め,P'に「x軸方向に2移動」する変換行列M2をかけてP"を求めることになります。


P' = M1 P"
P" = M2 P'

ですので
P" = M2 ( M1 P )
なのですが,掛け算の順番は変更できるので,
P" = ( M2 M1 ) P
とも表せます。ここで行列M1 と行列M2 の積は合成された変換行列になります。気をつけなければならない点は,
先に施す変換を表す変換行列が後ろになるということです。例えば,
P' = ( M4 M3 M2 M1 ) P
ならば変換M1から順にM2,M3,M4が行われることになります。
また,図形の変換には多くの点の変換が行われるので,演算量を低減するために,連続変換を表す行列Msを求めておき,各点の変換を行うことで計算の手間を省く事ができます。

Ms = M4 M3 M2 M1
P' = Ms P

連続変換では,変換の順番に気をつけなければなりません。
立方体が最初は原点にあるとして,
「立方体をy軸周りに60度回転して,その後x軸方向に2移動する」のと,
「立方体をx軸方向に2移動し,その後,y軸周りに60度回転する」のとでは,結果が異なります。
前者では,x軸上の2の位置で,図形が30度自転した状態で,立方体が描かれます。
後者では,xz平面上で,半径2の円をつくり,この円とx軸の交点から,y軸周りに60度公転し,しかも60度の自転もしている状態で,立方体が描かれます。
図4.3はこの状態を示しています。


          図4.3 立方体の連続変換 その1
原点の立方体:変換前の状態
右下の立方体:y軸周りに60度回転して,その後x軸方向に2移動
右上の立方体:x軸方向に2移動し,その後,y軸周りに60度回転

もし,「立方体をy軸周りに-60度回転し,x軸方向に2移動し,その後,y軸周りに60度回転する」という変換を加えると,xz平面上で,半径2の円をつくり,この円とx軸の交点から,y軸周りに60度公転し,自転していない状態で,立方体が描かれます。
図4.4はこの状態を示しています。

       図4.4 立方体の連続変換 その2
原点の立方体:変換前の状態
右上の立方体:y軸周りに-60度回転し,x軸方向に2移動し,その後,y軸周りに60度回転




4.4 ベクトルと変換行列に関する関数
Matrix.xxxxxx()というクラスMatrixの静的関数が使われている。主なものを見ておこう。
行列はクラスMatrixのインスタンスではなく,floatの大きさ16の配列で表現されている。
float[] MyMatrix=new float[16];
のように定義される。配列の内部は第0要素から順に,第0列から第3列に向けて,縦に4つずつ値を並べています。
行列の要素を直接取り出すことはないと思うので,このことは気にする必要はありません。
またベクトルも要素数4の配列で表されます。

(1)Matrix.setIdentityM(MyMatrix, 0);
 行列MyMatrixを単位行列になるように各要素に値を埋める関数です。
 第2引数の0はオフセットです。行列は第0要素から始まっていることを意味します。
 もし,MyMatrixの配列要素数が32だったら,0の代わりに16を指定すると第16要素から始まる2つ目の行列を単位行列にします。

(2)Matrix.scaleM(mMatrix, 0, 0.6f, 0.6f, 0.6f);
 行列MyMatrixに,スケーリングを行う行列を後ろから掛けて,行列MyMatrixを更新する関数です。
  MyMatrix = MyMatrix × [スケーリング変換行列]
 第2引数の0はオフセットです。行列は第0要素から始まっていることを意味します。
 3つのスケーリング係数は,前から順にx,y,z方向の成分です。

(3)Matrix.rotateM(MyMatrix, 0, theta, 0, 1, 0);
 行列MyMatrixに,y軸周りにtheta回転する変換行列を後ろから掛けて,行列MyMatrixを更新する関数です。
  MyMatrix = MyMatrix × [回転変換行列]
 第2引数の0はオフセットです。行列は第0要素から始まっていることを意味します。
 同様にMatrix.rotateM(MyMatrix, 0, theta, 0, 0, 1);ならz軸周りにtheta回転する変換行列を後ろから掛ける意味となります。
 同様にMatrix.rotateM(MyMatrix, 0, theta, 1, 0, 0);ならx軸周りにtheta回転する変換行列を後ろから掛ける意味となります。

(4)Matrix.translateM(mMatrix, 0, 0f, 0f, 1.2f);
 行列MyMatrixに,z軸方向に1.2平行移動する変換行列を後ろから掛けて,行列MyMatrixを更新する関数です。
    MyMatrix = MyMatrix × [平行移動変換行列]
 第2引数の0はオフセットです。行列は第0要素から始まっていることを意味します。
 同様にMatrix.translateM(mMatrix, 0, 0f, 1.2f, 0f);ならy軸方向に1.2平行移動する変換行列を後ろから掛ける意味となります。
 同様にMatrix.translateM(mMatrix, 0, 1.2f, 0f, 0f);ならx軸方向に1.2平行移動する変換行列を後ろから掛ける意味となります。

(5)Matrix.multiplyMM(resultMatrix, 0, leftMatrix, 0, rightMatrix, 0);
 行列同士を掛け算する関数です。
 resultMatrix, leftMatrix, rightMatrix はすべて4×4の行列で,resultMatrix = leftMatrix × rightMatrix を計算します。
 3つの0はそれぞれ直前の行列を表す配列のオフセットです。

(6)Matrix.multiplyMV(resultMatrix, 0, MyMatrix, 0, MyVector, 0);
 行列とベクトルの掛け算をする関数です。
 resultMatrix, MyMatrix は4×4の行列,MyVectorは要素4の列ベクトルで,resultMatrix = MyMatrix × MyVector を計算します。
 3つの0はそれぞれ直前の行列またはベクトルを表す配列のオフセットです。

(7)Matrix.invertM(resultMatrix, 0, MyMatrix, 0);
 行列の逆行列を求める関数です。
 resultMatrix, MyMatrix は4×4の行列で,resultMatrix = inv(MyMatrix) を計算します。
 2つの0はそれぞれ直前の行列表す配列のオフセットです。



5.OpenGLでの座標変換と透視図の仕組み。


5.1 モデル変換

OpenGLにおける座標変換では,4で説明した変換行列は,プログラム上には出てきません。
3で取り上げた,gles2sanple02 の GLRenderer.java onDrawFrame()における立方体表示部分をみると,

        //MyCubeの描画
        Matrix.setIdentityM(mMatrix, 0);  //ここではすでに設定されているので省略可
        Matrix.rotateM(mMatrix, 0, angle * 2, 0, 1, 0);
        Matrix.scaleM(mMatrix, 0, 0.6f, 0.6f, 0.6f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        //MyCubeの描画本体
        // r, g, b, a, shininess(1以上の値 大きな値ほど鋭くなる)
        MyCube.draw(0f, 1f, 0f, 1f, 20.f);

となっています。

Matrix.setIdentityM(mMatrix, 0);
は,4×4行列(実際には大きさ16のfloat配列)mMatrixを単位行列に設定します。
第2引数の0は配列mMatrixの第0要素から,4×4行列が使用していることを示しています。

Matrix.rotateM(mMatrix, 0, angle * 2, 0, 1, 0);
は,y軸周りの回転変換です。
mMatrix = mMatrix ×「y軸周りに(angle * 2)[deg]回転させる変換行列」
の意味で,後方からかけています。第2引数は配列mMatrixの有効範囲は第0要素から始まっていることを示し,第3引数は回転角度[deg]を与えます。
第4,5,6引数は回転軸を表す方向ベクトルになります。(0,1,0)なので,y軸を表します。
第2引数の0は配列mMatrixの第0要素から,4×4行列が使用していることを示しています。

Matrix.scaleM(mMatrix, 0, 0.6f, 0.6f, 0.6f);
は,スケーリング変換で,3軸すべての方向に0.6倍します。
mMatrix = mMatrix ×「各軸に関して0.6倍する変換行列」
の意味で,後方からかけています。第2引数は配列mMatrixの有効範囲は第0要素から始まっていることを示し,第3,4,5引数はx,y,z方向の倍率を表します。
第2引数の0は配列mMatrixの第0要素から,4×4行列が使用していることを示しています。

この時点で,mMatrixは
 「y軸周りに(angle * 2)度回転させる変換行列」×「各軸に関して0.6倍する変換行列」
を表す連続変換行列になっています。そして
GLES.updateMatrix(mMatrix);
でシェーダにmMatrixを渡します。(実は他にも渡しているものがあります。)

変換行列が確定したので,この後,
MyCube.draw(0f, 1f, 0f, 1f, 20.f);
で,立方体を原点に置きます。その後OpenGLバーテックスシェーダはmMatrixで変換しながら,舞台上に回転する立方体を描きます。

立方体を描く前に,座標軸も同様に描いていますが,次のように
mMatrixは単位行列のままシェーダに送られているのが読み取れると思います。

        //座標軸の描画
        //モデル変換行列mMatrixを単位行列にする。
        Matrix.setIdentityM(mMatrix, 0);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        //座標軸の描画本体
        //引数 r, g, b, a, shininess(1以上の値 大きな値ほど鋭くなる), linewidth
        //shininessは使用していない
        MyAxes.draw(1f, 1f, 1f, 1f, 10.f, 2f);//座標軸の描画本体

ここに出てきた変換行列mMatrixは「モデル変換行列」と呼ばれます。そして,舞台に見えているxyz軸で作られている座標系は「ワールド座標系」と呼ばれます。ワールド座標系は概念であって,まだこのままでは画面上に表示できません。

5.2 カメラビュー変換(視野変換)

ワールド座標系そのままでは視点が定まっていないため表示できません。視点を定めて,ワールド座標系を眺めて表示するための変換はカメラビュー変換(視野変換)と呼ばれます。なにも指示しない場合,視点はワールド座標系の原点にあっ て,z軸の負の方向を向いています。視点の位置に我々の目があるとしたら,「目は原点にあって,z軸の負の方向を向いています。」ということになり,「顔を左右に傾けない。(上方向はy軸の正方向)」となります。
これまでのサンプルプログラムでは,視点はワールド座標系の原点にはなかったのですが,視野の中心(カメラ(眼)の光軸の中心)はワールド座標系の原点を向いていました。
視点をどう動かすかを指示するには,
の3つが必要になります。この3つを与えられたとして,先に述べた,平行移動と回転移動を組み合わせることで,視点の移動が変換行列として表わすことが出来ます。ただし,座標軸のほうが移動するので,図形を移動する場合と逆の作業を行います。

cameraview.jpg
図5.1 カメラビュー変換(カメラの位置,視野中心方向の代表点,カメラの傾き)

例えば,「視点を原点からz軸の正の方向に5移動させる」は「視点を原点に固定したまま,図形をz軸の負の方向に5移動させる」になります。実はOpenGLは,3つの条件を与えて,カメラビュー変換を行なう変換行列を作るメソッドを持っています。(図5.1)
GLRenderer.java onDrawFrame() の

        //カメラビュー変換(視野変換)-----------------------------------
        //カメラ視点が原点になるような変換
        Matrix.setLookAtM(cMatrix, 0,
                (float) (viewlength * Math.sin(beta) * Math.cos(alph)),  //カメラの視点x座標 (図5.1のx0)
                (float) (viewlength * Math.sin(alph)),                   //カメラの視点y座標 (図5.1のy0)
                (float) (viewlength * Math.cos(beta) * Math.cos(alph)),  //カメラの視点z座標 (図5.1のz0)
                0.0f, 0.0f, 0.0f, //カメラの視線方向の代表点x,y,z  (図5.1のx1,y1,z1)
                0.0f, 1.0f, 0.0f);//カメラの上方向ベクトル
        //カメラビュー変換はこれで終わり。
        GLES.setCMatrix(cMatrix);

でcMatrixにカメラビュー変換行列を作成して登録しています。
(カメラの上方向ベクトルは通常(0,1,0)にしておきます。人間で言うと首の付根から頭頂部に抜ける方向のベクトルです。(1,1,0)にすると人間 で言うと首を右側に45度傾けたのと同じになります。これは日常も体験することです。(0,1,1)にすると,首を前に45度傾けた状態になります。上目 使いで対象物を見ることになり,不自然な動きとなります。z成分は0に保っておくの良いと思います。)

これにより,視点(カメラの位置)が上下左右に動くため,舞台を俯瞰したり,見上げたりできるようになっています。
viewlengthは固定値,alph,betaはスワイプ動作によって変化する変数です。これは,チルト変換と言って,座標原点を斜め上から俯瞰する ときに使われる変換です。原点からz軸の不方向を向いて,カメラ画面の上方向がy軸+方向になっている状態が初期状態です。そこから,z軸-方向に viewlengthだけ,後退し,x軸回り+方向にalph回転させ,その後y軸回り+方向にbeta回転させた状態を作っています。この状態でカメラ の視野中心は原点を向いています。
第2引数の0は配列mMatrixの第0要素から,4×4行列が使用していることを示しています。

後で,回転する図形と一緒に回るカメラから見る例が出てきますが,それはcMatrixに別な変換行列を掛けることにより実現します。

さて,これで,すべての変換が終わったように見えますがまだだめです。現在の図形はまだ3次元の座標を持っています。
表示するのは,2次元のディスプレイなのでまだ変換が必要です。

補足
alphとbetaはスワイプで変化すると書きましたが,スワイプなどの画面タッチの受付は,
MyGLSurfaceView.javaが行なっています。
この中のメンバ
 public boolean onScroll(MotionEvent event1, MotionEvent event2, float distx, float disty)
が,x方向y方向のスワイプ量を受け取り,変域を調整した後,
renderer.setScrollValue(Scroll);
でGLRenderer.javaに伝えています。


5.3 プロジェクション変換(射影変換)

2次元のディスプレイに表示するため,3次元座標系の点を2次元にするには,正射影といって,z=0にするだけでもできます。
(この機能もOpenGLは持っていますが,今は使いません。)
しかし,2,3で表示した座標軸や立方体は,遠くにいるときほど小さく見えています。
そのような仕掛けが図5.2に示す透視変換です。この説明は一般的なものです。
P(x,y,z)をP'(x',y',z')に三角形の相似を利用して変換します。
透視変換も変換行列になりますが,w"で割り算をする過程が加わります。
この変換の後z'=0にすれば,平面に投影されます。
Perspective.jpg
                  図5.2 透視変換
OpenGLでは,前後左右上下のクリップをして,正規化まで行い,zバッファ法という前後関係を判断して,前方のものが後方のものを隠す表現を使うため,z方向の値も正規化して残すため,もう少し複雑な射影変換をしています。また視点などの位置関係も変わります。
 この内容は,複雑なため,別ページで説明します。 OpenGLの射影変換へ

この変換をメソッドMatrix.frustumMで与えていますので,
さらにラップしたメソッド.gluPerspectiveでこの射影変換行列pMatrixを作成しています。
GLRenderer.java onDrawFrame() の

        GLES.gluPerspective(pMatrix,
                45.0f,  //Y方向の画角
                aspect, //アスペクト比
                1.0f,   //ニアクリップ   z=-1から
                100.0f);//ファークリップ  z=-100までの範囲を表示することになる
        GLES.setPMatrix(pMatrix);

で作業しています。
ただし,図5.2とは異なり,視点は原点に有り,カメラはz軸不方向を向いていると考えます。
これでようやく,図形は2次元図形にまで変換できるようになり論理平面座標系で表示されるようになります。

5.4 OpenGLにおける変換のまとめ
立方体を原点に描いて図3.1に表示していましたが,もう一度作業を細かく見てみましょう。
まず,クラスCubeのコンストラクタで,原点を中心とした外接球半径1の立方体を定義しました。この座標系はモデル座標系です。
そして,mMatrixでワールド座標系に変換します。ワールド座標系は図3.1に見えている,想像上の座標系です。またモデル座標系とワールド座標系で は3つの座標軸まったく重なりますが,モデルを作るために用いた座標系としてモデル座標系と,出来上がりを想像しているワールド座標系とは区別していま す。
アプリケーション側ではmMatrixを変換の合成として確定し,クラスCubeのメソッドdrawでモデル座標の立方体を描くと,ワールド座標系に置か れた,イメージが作成されたことになります。そしてアプリケーションはGLESのメソッドで,モデル変換行列mMatrixとカメラビュー変換行列 cMatrixとの積モデルビュー変換行列mvMatrixと,プロジェクション変換行列をシェーダpMatrixに与えています。そうするとシェーダは 図形を構成する各頂点座標をpMatrix × mvMatrix の合成変換行列にて,画面に表示しています。

この変換に関してまとめると,図形は

theMatrix = pMatrix × cMatrix × mMatrix
で出来た変換行列により,変換されて,(実際にはWの値で割る演算の後)表示されていることになります。
(実際には最後にもう一度ビューポート変換がなされます。)

逆に言うと,この3つの変換行列を定めた後に,原点に図形を置くと,ワールド座標系の所望の場所に,図形が置かれ,OpenGLが定められた視点から見える図形を表示することになります。
ここでmMatrixは,原点に置かれた図形(モデル座標系での記述)をワールド座標系の所望の場所に移動するモデル変換行列です。
cMatrixはワールド座標系に配置された図形を,視点を変更してカメラビュー座標系に変換するカメラビュー変換行列です。
pMatrixはカメラ座標系に置かれた図形を,透視変換して正規化座標系に変換するプロジェクション(射影)変換行列です。

cMatrix × mMatrix はモデルビュー変換行列mvMatrixとして扱われ,後に説明するシェーダ中では,MMatrixとして使用されます。

各座標系と,変換は次のような関係になります。

transform.jpg
            図5.3 座標系と変換のまとめ

6.描かれた立方体を回転(自転+公転)させる。



6.1 サンプルプログラム
自転しながら公転する立方体を描きます。また,公転軌道を円で描きます。
立方体はCube.javaのクラスで定義されています。
円はCircle.javaのクラスで定義されています。

次のファイルをダウンロードして,作業に使います。
gles2sample03.zip 

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ(舞台裏で使う)

新規プロジェクトGLES2Sample03をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図6.1のように表示されます。

rotatingCube.jpg
     図6.1 自転しながら公転する立方体

GLRenderer.java onDrawFrame() で

        //MyCubeの描画
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.rotateM(mMatrix, 0, angle, 0, 1, 0);
        Matrix.translateM(mMatrix, 0, 0f, 0f, 1.2f);
        Matrix.rotateM(mMatrix, 0, angle * 2, 0, 1, 0);
        Matrix.scaleM(mMatrix, 0, 0.4f, 0.4f, 0.4f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        //MyCubeの描画本体
        // r, g, b, a, shininess(1以上の値 大きな値ほど鋭くなる)
        MyCube.draw(0f, 1f, 0f, 1f, 20.f);

このように変換行列mMatrixが作成されている。自転しながら公転する仕組みがわかると思います。
プログラミングの際は変換の関数を呼ぶ順番は表示のための変換の順番の逆になります。すなわちこの例では次のような順で変換されることになります。
1.全体の大きさを0.4倍する       ・・・Matrix.scaleM(mMatrix, 0, 0.4f, 0.4f, 0.4f);
2.y軸の周りに(angle * 2)度回転させる  ・・・Matrix.rotateM(mMatrix, 0, angle * 2, 0, 1, 0);
3.z軸方向に1.2移動させる        ・・・Matrix.translateM(mMatrix, 0, 0f, 0f, 1.2f);
4.y軸の周りにangle度回転させる     ・・・Matrix.rotateM(mMatrix, 0, angle, 0, 1, 0);

さて,GLRenderer.java onDrawFrame() で同様に円が描かれます。

        //円の描画
        GLES.disableShading(); //シェーディング機能は使わない
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.scaleM(mMatrix, 0, 1.2f, 1.2f, 1.2f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        //円の描画本体
        // r, g, b, a, shininess(1以上の値 大きな値ほど鋭くなる), linewidth
        //shininessは使用していない
        MyCircle.draw(1f, 1f, 0.1f, 1f, 10.f, 1f);
        GLES.enableShading(); //シェーディング機能を使う設定に戻す

このように,Matrix.scaleM(mMatrix, 0, 1.2f, 1.2f, 1.2f)によって半径を1.2として円を描いています。

また,Circle.javaが追加され,コンストラクタにおいて折れ線近似でzx平面内の半径1の円が構成され,
メソッドdrawで円を描くところが読み取れると思います。
クラスCircleのメソッドdraw中で,「GL_LINE_LOOP」は与えた点列で連続折れ線を描画し,最終点と先頭点を結ぶモードです。
Axesのメソッドdraw中では「GL_LINE_STRIP」が使われていました。


参考 GL_XXXXXの表現


7.球を描き,回転(自転+公転)させる。


7.1 サンプルプログラム

自転しながら公転する立方体と球を描きます。また,公転軌道を円で描きます。
立方体はCube.javaのクラスで定義されています。
円はCircle.javaのクラスで定義されています。
球はSphere.javaのクラスで定義されています。

次のファイルをダウンロードして,作業に使います。
gles2sample04.zip 

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ(舞台裏で使う)

新規プロジェクトGLES2Sample04をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図7.1のように表示されます。

rotatingCubeSphere.jpg
     図7.1 自転しながら公転する立方体と球
公転軌道を表す円を2つ描いています。円のインスタンスは1つですが,GLRenderer.java onDrawFrame() で表示の仕方(ここでは同心円なので,拡大率が変更になっているだけ)を変えて,2回表示させています。

球もGLRenderer.javaに
    private Sphere MySphere=new Sphere(1f,40,20);
    //原点に,半径1の球体オブジェクト(40スライス,20スタック)を作成
で作成して,メソッドonDrawFrame()中で3回描いています。その内の1つ(青い球)は,次のようになっています。

        //MySphereの描画
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.rotateM(mMatrix, 0, 1.5f*angle, 0, 1, 0);
        Matrix.translateM(mMatrix, 0, 0.8f, 0f, 0f);
        Matrix.scaleM(mMatrix, 0, 0.2f, 0.2f, 0.2f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        //MySphereの描画本体
        // r, g, b, shininess(1以上の値 大きな値ほど鋭くなる)
        MySphere.draw(0f, 1f, 1f, 1f, 5.f);

で表示しています。
紫色の球はよく見ると半透明です。これはMySphere.drawの第4引数aに1より小さな値を設定しているからです。
aの値を有効にするにはGLRenderer.java中のonSurfaceCreatedで

        // 背景とのブレンド方法を設定します。
        GLES20.glEnable(GLES20.GL_BLEND);
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);    // 単純なアルファブレンド

が設定されていることが必要です。

OpenGLが球を描くことができるわけではありません。図7.2に示すように,球を表す点列を作り,連続三角形で,面を作ります。
そして,各頂点の法線ベクトルをもとに,三角形の頂点の色を定め,内部も連続的に色が変化しているように内挿し,三角形をグラデーションで塗ると,なめら かな球のように見えているだけです。(スムースシェーディング スムースシェーディングには複数の方法がありグーローシェーディングが使われています。)
spheremodels.jpg
図7.2 球の表示モデル(左から,ワイアフレーム,フラットシェーディング,スムースシェーディング)

7.2 球のモデル

球は,球の多面体近似モデルで作ります。縦切り(xy平面で0<zの半平面をy軸周りに一定角度ずつ回して球をスライスする)分割数と横切り(zx平面と平行な平面で球を切る)分割数を定め,各頂点の座標を求めて作成します。
図7.3に示すように,縦切りをスライス,横切りをスタックとします。

sphereSliceStack.jpg
     図7.3 スライスとスタック

Sphere.javaのコンストラクタでは,スライス数とスタック数を引数として,図7.4に示す頂点番号で多面体近似を行なっています。
spherMesh.jpg

    図7.4 球の多面体近似のときの頂点番号

スライスするのはyz平面から等角度でスライスします。またスタックも地軸を均等に割るのではなく,中心から見て,等角度になるように切ってスタックにします。図7.5に示すように,スライス角度φとスタック角度θが決まれば,ある地点Pのxyz座標が求められます。

spherecoord.jpg
   図7.5 多面体近似のときの頂点座標の求め方

球の多面体近似の各頂点の座標が求められたら,図7.6のように1つのスライスについて北極から南極に向けて点番号を並べて連続三角形とします。同様に他 のスライスも点番号を並べて連続三角形とします。そして,縮退三角形を挟んで,すべてのスライスを表す連続三角形を1つの連続三角形として登録できるようにします。

sphereSlice.jpg
図7.6 球の多面体近似の1スライスを連続三角形で記述


7.3 球のいたずら

遊び心で,1つの球のインスタンスを使って3つの球を描くときに,scale時に異方性を与えると,円形座布団やラグビーボールが出来ます。
これをダウンロードして,作業に使います。
gles2sample05.zip 

  フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ(舞台裏で使う)

新規プロジェクトGLES2Sample05の作成をします。前回と同様に作業すると,図7.7のように表示されます。
GLRenderer.java onDrawFrame() の球の表示箇所を見ると,scaleの引数に異方性があること,ラグビーボール側は回転が加わっているのが読み取れます。

rotatingCubeSphere2.jpg
     図7.7 自転しながら公転する立方体と球



8.OpenGL ES2のシェーダとシェーディング。


8.1 OpenGLでの描画

OpenGLで描画するときの手順は次のようになります。

(1)アプリケーション起動時に,シェーダプログラムをコンパイル&リンクして,シェーダが実行可能になるようにする。
(2)描く図形のモデル座標系での頂点座標,法線ベクトル,点列のインデックスを作成しておく。
(3)描画作業を繰り返し行う。

繰り返しの描画作業は次のような手順になります。

(1)カメラ位置,カメラ視線,光源など舞台装置属性をシェーダに与える。
(2)描きたい図形の頂点座標,法線ベクトルなどの属性をシェーダに与える。
(3)モデルビュー変換行列,プロジェクション変換行列などをシェーダに与える。
(4)シェーダは受け取ったデータを使って,3D画像を作成し,表示する。

すなわちアプリケーションプログラムでは,点の座標や法線ベクトル,光源や図形の色を設定し,それらをすべてシェーダに送って,描画してもらっているだけです。

8.2 シェーダ

OpenGLのシェーディングは自由度が有り,自分でシェーダー(シェーディングプログラム)を作成し,シェーディングの効果を実現します。シェーディングのプログラムはGLSL(OpenGL Shading Language)で記述します。
これまで,何も説明してこなかったのですが,シェーディングの方法や考え方は非常に多様ですが,ここはグーローシェーディング( Gouraud shading)を使用してきました。グーローシェーディングでは描きたい図形,ここでは断片化された三角形の三頂点の色を光学近似によって求め,三角形 の内部の点の色は三頂点の色から内挿しています。そのため,三角形内部においては三頂点からなめらかに色が変化して表現されます。(球などはこのためにな めらかな表面に見えています。)

シェーダは複数のプログラムから構成されていますが,バーテックスシェーダ(頂点シェーダ vertex shader)とフラグメントシェーダ(画素シェーダ)を自分で作るようにします。
ここではグーローシェーディングのプログラムを使っていますのでグーローシェーディングでの2つのシェーダの役割を説明します。

バーテックスシェーダは,与えられた図形のすべての頂点に関して,2つの計算をします。
(1)8.4で説明する光の反射の性質を使い,頂点の色を決めています。カメラビュー座標系で考えていて,光の当たり方を考慮して色v_Colorを決めています。
(2)図5.3の正規化座標系での座標gl_Positionを決めています。具体的には,モデル座標系の座標にモデルビュ-変換行列をかけて,プロジェクション変換行列をかけて正規座標系座標を求めています
バーテックスシェーダの関数mainはOpenGLプログラムで与えた頂点の数だけ呼ばれます。

グーローシェーディングのバーテックスシェーダ
(光源による光の変化については8.4で説明します)
uniform int u_EnableShading;     //シェーディング有効:1 無効:0
uniform vec4  u_ObjectColor;     //shadingを使用しない時の色の設定(単色)
//光源
uniform vec4  u_LightAmbient;    //光源の環境光色
uniform vec4  u_LightDiffuse;    //光源の拡散光色
uniform vec4  u_LightSpecular;   //光源の鏡面光色
uniform vec4  u_LightPos;        //光源の位置(カメラビュー座標系)
//マテリアル
uniform vec4  u_MaterialAmbient;     //マテリアルの環境光色
uniform vec4  u_MaterialDiffuse;     //マテリアルの拡散光色
uniform vec4  u_MaterialSpecular;    //マテリアルの鏡面光色
uniform float   u_MaterialShininess; //マテリアルの鏡面指数
//行列
uniform mat4  u_MMatrix;            //モデルビュー変換行列
uniform mat4  u_PMMatrix;            //射影行列×モデルビュー行列
//頂点情報
attribute   vec4    a_Position;     //位置
attribute   vec3    a_Normal;       //法線ベクトル
//出力
varying vec4  v_Color;

void  main(){
    if (u_EnableShading==1) { //シェーディングを適用する場合
        //環境光の計算
        vec4  ambient=u_LightAmbient*u_MaterialAmbient;

        //拡散光の計算(カメラビュー座標系で考えている)
        vec3  P=vec3(u_MMatrix*a_Position);           //モデル座標系の座標にモデルビュ-変換行列を
                                                      //かけてカメラビュー座標系座標を求めている
        vec3  L=normalize(vec3(u_LightPos)-P);        //光源方向単位ベクトル
        vec3  N=normalize(mat3(u_MMatrix)*a_Normal);  //法線単位ベクトル
        float dotLN=max(dot(L,N),0.0);
        vec4  diffuseP=vec4(dotLN);
        vec4  diffuse=diffuseP*u_LightDiffuse*u_MaterialDiffuse;

        //鏡面光の計算(カメラビュー座標系で考えている)
        vec3  V=normalize(-P);  //視点方向単位ベクトル
        float dotNLEffect=ceil(dotLN);
        vec3  R=2.*dotLN*N-L;
        float specularP=pow(max(dot(R,V),0.0),u_MaterialShininess)*dotNLEffect;
        vec4  specular=specularP*u_LightSpecular*u_MaterialSpecular;

        //色の指定(上で計算した3つの和)
        v_Color=ambient+diffuse+specular;
    } else { //シェーディングを適用しない場合
        v_Color=u_ObjectColor;
    }

    //位置の指定
    //モデル座標系の座標にモデルビュ-変換行列をかけて,
    //プロジェクション変換行列をかけて正規座標系座標を求めている
    gl_Position=u_PMMatrix*a_Position;
};



フラグメントシェーダは,図形の各面を構成するすべての点に関して,色を決めています。バーテックスシェーダで決定した色v_Colorは正規化座標系で の座標gl_Positionにおける頂点における色ですが, フラグメントシェーダにおける色v_Colorは頂点における色はなく,図形の各面を構成するすべての点の色のことで,複数の頂点の色から内挿された色の 値が与えられています。そのため,注視点の色はgl_FragColor=v_Colorで決まります。フラグメントシェーダの関数mainは図5.3の 正規化座 標系で表示される図形の面すべての点の数だけ呼び出されます。(バーテックスシェーダのmainよりずっと多い回数)
v_Colorはバーテックスシェーダで求められた,各頂点の色情報のみを表すだけでなく,2 頂点間の線上の各点の色は2頂点の色から内挿された色で,線上の色だけを見るとグラデーションに見えます。描く図形は三角形なので,1つの線分を構成する 各点の色がグラデーションのように定まると,残りの1頂点との間で再度色を内装して決めているので,三角形全体がグラデーションとして色が定まります。

グーローシェーディングのフラグメントシェーダ
precision mediump float;
varying vec4  v_Color;
void  main() {
    gl_FragColor=v_Color;
};



8.3 シェーダのコンパイル&リンクとデータ転送

2つのシェーダプログラムは,GLES.java中に,文字列として記述されています。アプリケーション実行の最初の方で,
GLRenderer.javaのメンバメソッドonSurfaceCreatedのはじめに,GLES.makeProgram()とし て,GLES.javaのpublic static boolean makeProgram()が呼ばれ,2つのシェーダプログラムがコンパイル・リンクされ,シェーダを使う準備ができます。
コンパイル・リンクが失敗したら,それ以上作業しないようになっています。

実行時にはアプリケーションプログラムから,光源に関する情報や,表示される図形の光学的属性,図形の頂点・法線ベクトルなどがシェーダに渡され,3D図形を画面に表示しています。この表示が絶えず繰り返されていることになります。

アプリケーションプログラムから,シェーダにデータを渡す際は,シェーダ側の変数のハンドル(IDのようなもの)をアプリケーションプログラムが予め取得しておいて,このハンドルを手がかりにデータを渡すことが出来ます。(図8.1)

アプリケーションプログラム内でシェーダにデータ送る役目はすべてクラスGLES内の関数を通して行われます。

例 光源の座標
シェーダ側の変数名  uniform vec4 u_LightPos;
           (uniformとは一定値 vec4 は4要素のベクトルx y z 1)
                 (バーテックスシェーダプログラム中)

ハンドル変数名    public static int lightPosHandle;
           (クラスGLESの変数)

ハンドル取得方法   lightPosHandle=GLES20.glGetUniformLocation(myProgram,"u_LightPos");
                 (クラスGLESのメソッドmakeProgram())

アプリ側変数名    float[] CVLightPos= new float[4];
           (同次座標系x,y,z,1)
           (クラスGLESの変数)

データ転送方法    GLES20.glUniform4f(GLES.lightPosHandle, CVLightPos[0], CVLightPos[1], CVLightPos[2], 1.0f);
           (取得されたハンドルはGLESクラスの変数に保存されているためGLRenderer.javaからの作業では
            こうなる)

ハンドル取得時に使用するメソッドや,データ転送時に使用するメソッドは,シェーダ側やアプリ側の変数の形によって異なります。
app2shader.jpg
図8.1 アプリケーションプログラムからシェーダへのデータ転送

8.4 光源の存在下での図形を構成する面の描写

シェーダプログラムでは,光のあたり方で物体の色を決めていますが,考え方を説明します。

現実の世界で物体に色が付いて見えるのは,その色の反射率が高いからで,物体固有の性質である。一方,光源にも色があり,すべての色が均等であれば白色に 見えます。また,現実の世界では,光源は太陽であったり,照明光であったりするが,直接光が当たっていない場所でも,空気や注目していない物体により散乱 光や多重反射光があるため(環境光),暗くて見えなくなることはありません。光があたっているところでは,物体の表面の細かな凹凸によって乱反射が起こり (散乱光),どちらから見ても明るく見えます。また物体の表面が鏡面に近い状態であれば,鏡面反射のように,入射角反射角が等しい方向に多く反射する光 (鏡面反射光)があります。
現実の世界は多くの光がありますが,そのすべてを計算に組み入れることは出来ないので,次の3つの代表的 な光を計算に組み入れることにします。(Phong Model (Diffuse Scattering + Ambient + Specular Reflection))
色は赤緑青の3色を使用し,別々に計算します。

(1)環境光(Ambient):闇夜でない限り,光があたっていてもいなくても見える反射光
L_Ambient.jpg

(2)拡散光(Diffuse):光があたっている部分において,すべての方向に均一に反射する反射光
           ただし,その強さは,面の法線方向と光源方向に依存する
L_DiffuseScatter.jpg
        図8.2 拡散光

(3)鏡面反射光(Specular):入反射の幾何学的状態によって決まる反射光
L_SpecularReflection.jpg
   ただし,cosα<0の時は光が当たっていないので鏡面反射光は無いことにします。

       図8.3 鏡面反射光(Phong Model)

最後にこの3つを合わせて,反射光は次のように表せます。
ただし,cosα<0の時は光が当たっていないので鏡面反射光は無いことにします。
L_PhongModel.jpg

8.5 シェーダでの計算

バーテックスシェーダは,与えられた図形の頂点に関して,カメラビュー座標系で光の 当たり方を考慮して色(varying vec4 v_Color)を決めることと,表示座標系での座標(gl_Position)を決めることをします。またプログラムではシェーディング計算をするかど うか選択できるようになっていて,光源とは無関係に素材の色属性をそのまま使うこともできるようになっています。(GLES.java内の頂点シェーダの コード参照)ここでvaryingというのは,シェーダの出力用変数で,後段のシェーダの入力変数になります。

フラグメントシェーダは,図形の各面を構成するすべての表示座標系での点の色(varying vec4 v_Color)を決めています。ただし,色を内挿する作業はここでする必要はないようです。(GLES.java内のフラグメントシェーダのコード参照)

補足
シェーダで用いているattribute属性の変数には,シェーダ起動の前に必ずデータが入っている必要があります。(入っていないと暴走します)
そのため,GLRenderer.javaではクラスGLRenderer内にDummy変数をおいて,

    //シェーダのattribute属性の変数に値を設定していないと暴走するのでそのための準備
    private static float[] DummyFloat= new float[1];
    private static final FloatBuffer DummyBuffer=BufferUtil.makeFloatBuffer(DummyFloat);

GLRenderer内のonDrawFrameの最初のところで

    //シェーダのattribute属性の変数に値を設定していないと暴走するのでここでセットしておく。この位置でないといけない
    GLES20.glVertexAttribPointer(GLES.positionHandle, 3, GLES20.GL_FLOAT, false, 0, DummyBuffer);
    GLES20.glVertexAttribPointer(GLES.normalHandle, 3, GLES20.GL_FLOAT, false, 0, DummyBuffer);

このように値をセットしています。

後で述べるマルチシェーダを用いると,各描画図形ごとの設定になるので,このようなおまじないは必要なくなります。



9.光源を白い球で描き,描かれた立方体や球の中心を線で結ぶ。


自転しながら公転する立方体と球と光源を表す白い球を描きます。また,公転軌道を円で描きます。
また,各オブジェクトの中心を白い線で結びます。

次のファイルをダウンロードして,作業に使います。
gles2sample06.zip 

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ(Line_PtoP.java用に前のサンプルから変更あり)

新規プロジェクトGLES2Sample06をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図9.1のように表示されます。

rotatingCubeSphere3.jpg
図9.1 光源を表す白球を追加し,各オブジェクトの中心を結ぶ線を表示
これまで立方体や球はシェーダにてシェーディングを施してきました。光源を表す球(光源そのものではありません。光源は点光源です。)はシェーディングを行わずに白で描きます。
もしシェーディングを有効にして描画すると,光源はこの球の内部に光源(点光源)があるので,球の表面には光が当たらないため,真っ黒な球が描かれてしまいます。
そのため,GLRenderer.java onDrawFrame() のこの球を描くところで,

        GLES.disableShading(); //shadingせずに単色で表示

でシェーディング効果を無効にしています。メソッドGLES.disableShading()を追いかけて行くと,シェーダープログラム内で
u_EnableShadingが0になって,与えられた色で塗っていることがわかるでしょう。
この設定は既に,座標軸や円の描画で使っていました。


次に,各オブジェクトを線で結ぶのですが,これまでのプログラムではオブジェクトの中心が,モデル座標系の原点にあることがわかっていても,ワールド座標系でどこにあるのかは追求してきませんでした。
そこで,原点(0,0,0)をモデル変換行列で変換し, ワールド座標系での座標を求めます。
GLRenderer.java onDrawFrame()で,各オブジェクトを描いた直後で

        Matrix.multiplyMV(tmpPos1, 0, mMatrix, 0, origin, 0); //中心の取得
        Matrix.multiplyMV(tmpPos2, 0, mMatrix, 0, origin, 0); //中心の取得

により,オブジェクトのtmpPos1,tmpPos2に座標が求められます。そのためにoriginという原点座標を表す変数が増えています。
そして,

        //物体の中心点を線で結ぶ
        GLES.disableShading(); //シェーディング機能は使わない
        Matrix.setIdentityM(mMatrix, 0);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        line1.setVertexs(tmpPos1, LightPos);
        line1.draw(1f, 1f, 1f, 1f, 0f, 2f);
        line1.setVertexs(tmpPos2, LightPos);
        line1.draw(1f, 1f, 1f, 1f, 0f, 2f);
        line1.setVertexs(tmpPos1, tmpPos2);
        line1.draw(1f, 1f, 1f, 1f, 0f, 2f);
        GLES.enableShading(); //シェーディング機能を使う設定に戻す

のようにクラスLine_PtoPのインスタンスで描きます。クラスLine_PtoPのインスタンスはワールド座標系の座標で描くため,モデル変換行列は単位行列のままを使っています。

なお,クラスLine_PtoPのインスタンスの持つ座標を設定できるように,class BufferUtilにも変更があります。


10.公転中の立方体と一緒に視点を移動させる。



自転しながら公転する立方体と一緒に視点を移動させます。起動時は前のサンプルプログラムと同じ表示ですが,画面をタップすると,公転する立方体と一緒に視点が移動するようになり,もう一度タップすると自転しながら公転する立方体と一緒に視点が移動するようになります。


次のファイルをダウンロードして,作業に使います。
gles2sample07.zip 

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ

新規プロジェクトGLES2Sample07をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図10.1,図10.2のように表示されます。


rotatingCubeSphere4.jpg
rotatingCubeSphere5.jpg
図10.1 立方体の公転と一緒に視点が移動

立方体の公転と一緒に視点が移動しているため,立方体が自
転しているだけのように見えます。また,座標軸や公転軌道
も立方体を中心に回転しているように見えます。

図10.2 立方体の公転+自転と一緒に視点が移動

立方体の公転+自転と一緒に視点が移動しているため,立方体が
静止しているように見えます。また,座標軸や公転軌道も立方体
を中心に回転しているように見えます。


視点を変更するということは,カメラビュー変換行列を変更することになります。
GLRenderer.java onDrawFrame() の最初のところで,カメラビュー変換行列cMatrixを作成していますが,ここでviewmodeによって修正をかけています。
カメラビュー変換行列の修正においても変換行列をかける作業がありますが,カメラをz軸+方向に移動する 場合 は,カメラを固定しておいて舞台装置全体をz軸-方向に動かすことになります。また,カメラをy軸周りの+方向に回転させるには,カメラを固定しておい て,舞台装置全体をy軸周りの-方向に回転させる事になります。連続変換の場合にも,変換順序がモデル変換の時と逆になります。

        Matrix.setLookAtM(cMatrix, 0,
                (float) (viewlength * Math.sin(beta) * Math.cos(alph)),  //カメラの視点 x
                (float) (viewlength * Math.sin(alph)),                    //カメラの視点 y
                (float) (viewlength * Math.cos(beta) * Math.cos(alph)),  //カメラの視点 z
                0.0f, 0.0f, 0.0f, //カメラの視線方向の代表点
                0.0f, 1.0f, 0.0f);//カメラの上方向
        if (viewmode!=0) {
            if (viewmode==2) Matrix.rotateM(cMatrix, 0, -angle * 2, 0, 1, 0);
            Matrix.translateM(cMatrix, 0, 0f, 0f, -1.2f);
            Matrix.rotateM(cMatrix, 0, -angle, 0, 1, 0);
        }


画面をタップしてviewmodeを切り替えていますが,タップなどの画面タッチの受付は,MyGLSurfaceView.javaが行なっています。
この中のメンバ
 public boolean onSingleTapUp(MotionEvent arg0)
が,タップを受け取り,
renderer.toggleViewmode();
でGLRenderer.javaに伝えています。


11.図形に画像や文字をはりつける(テクスチャの組み込み方)



ファイルから得た画像と文字を書いた画像を,四角形に貼り付けて表示します。
ここでのテクスチャに関するクラス定義は,説明用ですので,ここだけで使います。実用の形に関しては次の12章を参考にしてください。

次のファイルをダウンロードして,作業に使います。
gles2sample08.zip 
ただし,圧縮ファイル中の sample.png は res -> drawable にコピーします。(図11.1)

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    Rectangular.java         平面四角形のプリムティブ
    RectangularWithTex.java  テクスチャ付き平面四角形のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ

    sample.png               ドロイド君の画像(透明部のあるpngファイル)

新規プロジェクトGLES2Sample08をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図11.2のように表示されます。

sources2.jpg
texture1.jpg
図11.1 sample.pngの置き場所
図11.2 ドロイド君とHelloを追加表示


新たに増えたものは,四角形を描くためのクラスRectangularとテクスチャ付きの四角形を描くためのクラスRectangularWithTexです。
また,大幅変更のあったのは,クラスGLES,クラスGLRendererです。
Rectangular.javaはこれまでの立方体よりも簡単な四角形表示用になっており,頂点座標,頂点での法線ベクトルが定義されているだけで,簡単み読み取れると思います。


11.1 クラスRectangularWithTex

クラスRectangularWithTexでは,通常の四角形の頂点座標,頂点での法線ベクトルがあり,テクスチャを生成するコンストラクタがあります。
コンストラクタは2つあり,片方はファイルから画像を読み出し,もう片方は文字列で画像を作っています。OpenGLには今のところ文字列を扱う機構がなく,この方法で文字列を表現します。

画像をファイルから読み出すには,画像ファイルを res -> drawable に置きます,画像形式はかなりの形式に対応しているようです。
画像の大きさは,256×256のような2のべき乗の正方形がよいようです。(この形式でしか動作しない端末が多数あるようです。私が試しているNexus7(2012)は,この形式でしか動作しません。Nexus7(2013)ではこの縛りはない。)
BitmapFactory.decodeResource()が,画像ファイルをbitmapに変換してくれます。

文字列を画像化するには,文字列の表示サイズからbitmapサイズを決めてクラスCanvasでbitmapに文字を書きます。この場合もサイズは256×256のような2のべき乗の正方形がよいようです。

bitmapが出来上がったら,テクスチャとして次のようにして登録します。

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, TextureId);
        GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

登録が終わるとテクスチャはTextureIdで管理されます。

次にテクスチャを図形にどのように貼り付けるかの対応を指示する必要があります。
テクスチャ座標は,矩形で(現在は正方形で)左上が原点(0,0),右下が(1,1)になる向きの座標になっています。
図形を定義しているすべての点について,テクスチャ座標のどこが対応するかを示すことになります。
図形が四角形なので,四角形から四角形への対応を示します。
頂点座標

    //頂点座標
    float[] vertexs= {
        -.5f,  .5f, 0f,    //左上 0
         .5f,  .5f, 0f,    //右上 1
        -.5f, -.5f, 0f,    //左下 2
         .5f, -5.f, 0f     //右下 3
    };

の個々の頂点に対応して

    //テクスチャコード
    private final float textcoords[] = {
        0f, 0f,     // 左上 0
        1f, 0f,     // 右上 1
        0f, 1f,     // 左下 2
        1f, 1f      // 右下 3
    };

図11.3を参照しながら,このようにテクスチャコードを設定しました。

PointCorrespondence .jpg
図11.3 テクスチャ座標系をモデル座標系に対応させる


テクスチャコードを間違えて,図11.4のように設定すると,

    //テクスチャコード
    private final float textcoords[] = {
        0f, 1f,
        1f, 1f,
        0f, 0f,
        1f, 0f
    };

図11.5 のように,上下に反転したものが現れる。


PointCorrespondenceWrong.jpg
図11.4 テクスチャ座標系を上下逆にモデル座標系に対応させる

texture2.jpg
Helloもドロイド君も間違えて指示してしまいました
   図11.5 テクスチャコードを間違えた例

また,ドロイド君をはりつける四角形の頂点座標の設定を

    //頂点座標
    float[] vertexs= {
        -.25f,  .5f, 0f,    //左上 0
         .25f,  .5f, 0f,    //右上 1
        -.25f, -.5f, 0f,    //左下 2
         .25f, -5.f, 0f     //右下 3
    };

とすると,図11.6のような細身のドロイド君になります。


texture3.jpg
    図11.6 細身のドロイド君



11.2 クラスGLES

テクスチャを使用することに伴って,シェーダプログラムが,大幅に変更になりました。テクスチャを有効にするしないのスイッチ変数によって,異なる動作になります。
テクスチャを有効にすると,フラグメントシェーダにおいて,点の色はテクスチャが使われるようになります。
同時にシェーダプログラム中のテクスチャのための変数のハンドル設定も増えています。
テクスチャの有効・無効を設定するメソッドは,クラスGLESの最後に書かれています。

シェーダプログラムの入力変数でattribute属性のものは,使わなくても値が設定されていなければならないようです。
すでに最初(GLES2Sample01)からGLRenderer中で

//シェーダのattribute属性の変数に値を設定していないと暴走するのでそのための準備
private static float[] DummyFloat= new float[1];
private static final FloatBuffer DummyBuffer=BufferUtil.makeFloatBuffer(DummyFloat);

が定義され,onDrawFrame()の最初のところで

//シェーダのattribute属性の変数に値を設定していないと暴走するのでここでセットしておく。この位置でないといけない
GLES20.glVertexAttribPointer(GLES.positionHandle, 3, GLES20.GL_FLOAT, false, 0, DummyBuffer);
GLES20.glVertexAttribPointer(GLES.normalHandle, 3, GLES20.GL_FLOAT, false, 0, DummyBuffer);

このように,ダミーの値が設定されていましたが,シェーダプログラムの入力変数でattribute属性のものとして,
a_Texcoordが増えたため,onDrawFrame()の最初のところで

GLES20.glVertexAttribPointer(GLES.texcoordHandle, 2, GLES20.GL_FLOAT, false, 0, DummyBuffer);

が増えています。後述するマルチシェーダを使わないならば,この点に注意が必要です。



11.3 シェーダプログラムについて

テクスチャを利用するようになって,2つのスイッチ変数を使うようになりました。
        uniform highp int u_EnableShading;
        uniform highp int u_EnableTexture;
しかも,この2つのint型変数をバーテックスシェーダとフラグメントシェーダの両方で扱うようになりました。

2つのシェーダで同じ型の同じ変数を宣言すると,共通のグローバル変数として使うことができます。

これまででしたら
        uniform int u_EnableShading;
        uniform int u_EnableTexture;
このように宣言していましたが,Nexus 7 ME571-16G Android version 5.1.1ではフラグメントシェーダが,intの精度を厳しくチェックしていて,
GLSLのリンク時にlogcatにリンクエラー
    --From Vertex Shader:
    Error: Symbol u_EnableShading defined with different precision in vertex and fragment shaders.
    Error: Symbol u_EnableTexture defined with different precision in vertex and fragment shaders.
    --From Fragment Shader:
    Error: Symbol u_EnableShading defined with different precision in vertex and fragment shaders.
    Error: Symbol u_EnableTexture defined with different precision in vertex and fragment shaders.
が表示され,プログラムが動作できない状況になります。
そこで,高精度を意味する「highp」をつけて,両方のシェーダについて精度を一致させると解決できます。

実はバーテックスシェーダでintを宣言すると暗黙に「highp」がついているとみなされます。そのため,フラグメントシェーダ側だけに「highp」をつけるだけでもOKです。


11.4 クラスGLRenderer

メソッドonSurfaceCreated()内で,テクスチャの有効化,シェーダプログラム中のテクスチャのための変数のハンドルへのバッファのセットなどが増えています。
メソッドonDrawFrame()では,先頭部でダミー設定が増えています。

    private RectangularWithTex SampleDroid;
    private RectangularWithTex Hello;

は宣言されていますが,インスタンスの生成は,メソッドonSurfaceCreated()内です。

        SampleDroid = new RectangularWithTex(mContext,R.drawable.sample); //xy平面の原点にオブジェクトを作成
        //                                                    res -> drawable -> sample.png (256×256)が入っている
        Hello = new RectangularWithTex("Hello",20, Color.WHITE, Color.parseColor("#000F00C0")); //xy平面の原点にオブジェクトを作成

R.drawable.sample はフォルダdrawable中にあるsample.xxxを読み込むことになります。sampleの部分はファイルプライマリ名です。
ドロイド君のsample.pngでは予め背景が透明になっています。
Color.parseColor("#000F00C0") は文字列の背景色を透明にしています。



12.図形に画像や文字をはりつける(テクスチャと図形の分離)



ファイルから得た画像と文字を書いた画像を,球や立方体に貼り付けて表示します。

次のファイルをダウンロードして,作業に使います。
gles2sample09.zip 
ただし,圧縮ファイル中の xxxx.png は res -> drawable にコピーします。

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    Rectangular.java         平面四角形のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    Texture.java             画像ファイルから作るテクスチャ
    StringTexture.java       文字列から作るテクスチャ
    TexCube.java             テクスチャを貼り付けられる立方体のプロムティブ(6面全てに同じテクスチャ)
    TexRectangular.java      テクスチャを貼り付けられる平面四角形のプリムティブ
    TexSphere.java           テクスチャを貼り付けられる球のプリムティブ

    sample.png               ドロイド君の画像(透明部のあるpngファイル)
    cherry.jpg               桜の花の画像
    cloud.jpg                夕焼け雲の画像
    earthpicture.jpg         地球の画像
    goldfish.jpg             金魚の画像
    lightsource.jpg          光源球の画像
    rock.jpg                 岩石の画像
    woodenbox.jpg            木箱の画像


新規プロジェクトGLES2Sample09をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図12.1のように表示されます。
ここで画像データの大きさですが,128(ピクセル)×128(ピクセル),256(ピクセル)×256(ピクセル)のように2のべき乗の正方形が無難です。

texture4.jpg

    図12.1 テクスチャ付きの5つの球と2つの立方体

11章で取り上げたテクスチャの組み込み方は,図形とテクスチャが一体となっているため,同じ図形インス タ ンスを使って,別々のテクスチャをはめ込んで複数の図形を描くことが出来ません。そこで,テクスチャのみ単独のクラスにし,テクスチャを受け入れ可能な図 形クラスを作り,図形クラスのインスタンスにテクスチャクラスのインスタンスをつけることによって,これを実現できるようにしました。
なお,11章で使用したクラスRectangularWithTexは使わないようにします。
テクスチャは通常は一度定義した内容を使い続けますが,現在時刻の表示のように,表示内容を逐次変更する事もできます。

テクスチャのみのクラスとしては,次の2つを用意しました。
・ファイルの画像をテクスチャにするクラスTexture
・文字列をテクスチャにするクラスStringTexture

テクスチャを受け入れ可能な図形として,TexRectangular,TexCube,TexSphereを作りました。これらはテクスチャコードも持っています。

・TexRectangularでは四角形(平面図形)にテクスチャを貼り付けます。
・TexCubeでは,正方形テクスチャを繰り返し6回使用して,立方体全面に同じテクスチャを貼り付けます。
・TexSphereでは,正方形テクスチャを,球の分割 n×m にあわせて縦横に分割し,格子点座標でテクスチャコードを作ります。その結果小さな長方形を歪ませながら球に貼り付けることになります。球の両極部分は分割図形が三角形であるため,テクスチャの四角いパーツを三角形2つにして,半分だけを使うようにしています。
実際のクラスSphereでは両極はそれぞれ1点しか割り付けられたいなかった(図7.4)のです が,TexSphereでは,多面体の両極はテクスチャを貼り付けやすいように,複数の点を割りつけています。(図12.2)そして,点0にはテクスチャ 座標の(0,0)を,点numSlices1-1にはテクスチャ座標の(1,0)を,点numStacks*numSlices1にはテクスチャ座標の (0,1)を,点(numstacks+1)*numSlices1-1にはテクスチャ座標の(1,1)を対応するようにしています。

spherMesh2.jpg
         図12.2 球の近似多面体の点の割付


13.文字を舞台装置の最前面に位置固定表示する。



3次元表示されている画面の最前面に文字テキスチャや画像テキスチャを貼り付けます。

次のファイルをダウンロードして,作業に使います。
gles2sample10.zip 
ただし,圧縮ファイル中の xxxx.png は res -> drawable にコピーします。

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    Rectangular.java         平面四角形のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    Texture.java             画像ファイルから作るテクスチャ
    StringTexture.java       文字列から作るテクスチャ
    TexCube.java             テクスチャを貼り付けられる立方体のプロムティブ(6面全てに同じテクスチャ)
    TexRectangular.java      テクスチャを貼り付けられる平面四角形のプリムティブ
    TexSphere.java           テクスチャを貼り付けられる球のプリムティブ

    sample.png               ドロイド君の画像(透明部のあるpngファイル)
    earthpicture.jpg         地球の画像
    lightsource.jpg          光源球の画像


新規プロジェクトGLES2Sample10をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図13.1のように表示されます。
texture5.jpg

    図13.1 テクスチャ付きの5つの球と2つの立方体

GLRenderer.java の onDrawFrame の最後の方で,「//無変換の記述はここ」以降に注目します。

ここでは,プロジェクション変換行列pMatrix,カメラビュー変換行列cMatrix,モデル変換行 列mMatrixのすべてを単位行列にしています。こうすると,図13.2に示すような描画領域は正規座標系(3次元)となり,xyz空間は物理画面に固 定します。またz軸は右手系とは異なり,奥側が正になります。各軸の値の範囲は-1<x,y,z<1になります。タブレット縦置きの場合は縦 のほうが横より長いのですが,それに合わせた-1<x,y,z<1 なので,長さは異方性を持っています。そのため,この空間でxy平面に平行な平面内で正方形を描こうとしても縦長の長方形になってしまいます。そこでス ケーリングの時にfactorx,factoryが必要になっています。

normalizedcoodinate.jpg
図13.2 正規座標系(タブレっト縦置きを想定)
これまでに描いた球などは,モデル座標系において球の中心が原点に有りましたが,この原点がモデル変換・ カメラビュー変換・プロジェクト変換を経ると,正規座標系の座標に変換されます。 そして,この球の位置を正規座標系で表した座標で,z=-1にした座標は,ユーザ視線から見て,球の最前面の位置になります。ここにテクスチャ付きの四角 形を描くと,球の前面にテクスチャが見えます。図13.1のHelloの文字列は球の移動にともなって,いつも大きい地球の前に貼り付いて移動するように なります。

また,座標(-1,1,-1)にドロイド君を貼り付けると,図13.1の画面左上隅にドロイド君の 1/4が見えるようになります。(ドロイド君を貼り付けている四角形の中心が画面の左上隅になるため)画面をスワイプして,見えているxyz軸などを動か しても,左上隅のドロイド君は移動しません。
座標(1-a,1-a,-1)のにドロイド君を貼り付けると,図13.1の画面右上隅にドロイド君の全身が見えるようになります。ただし,aはドロイド君正方形の1辺の半分の長さとします。
座標(0,0,0.99)にドロイド君を貼り付けると,図13.1の画面中央の最背面にドロイド君が見えるようになります。2つの地球はこのドロイド君の前を通過します。(最背面に貼り付けるというのはあまりないと思いますが。)
小さい地球の最前面に貼り付けたドロイド君を合わせると,4つのドロイド君が図13.1に見えています。

        //無変換の記述はここ
        GLES.disableShading(); //シェーディング機能は使わない
        Matrix.setIdentityM(pMatrix, 0);
        GLES.setPMatrix(pMatrix);
        Matrix.setIdentityM(cMatrix, 0);
        GLES.setCMatrix(cMatrix);

        //画面左上の最前面(-1,1,-1)にドロイド君を表示(1/4しか表示されない)
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.translateM(mMatrix, 0, -1f, 1f, -1f);
        Matrix.scaleM(mMatrix, 0, 0.2f * factorx, 0.2f * factory, 0.1f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定2
        SampleDroid.setTexture();
        MyTexRectangular.draw(.1f, .1f, .5f, .2f, 0.f);

        //画面中央下部の最前面(0,-1+a,-1)に現在時刻を表示
        time.setToNow();
        currenttime = time.year + "/" + (time.month+1) + "/" + time.monthDay + " " +
                time.hour + ":" + time.minute + ":" + time.second;
        CurrentTime.makeStringTexture(currenttime, 20, Color.WHITE, Color.parseColor("#000F00C0"));
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.translateM(mMatrix, 0, 0f, -1f + 0.1f * factory, -1f);
        Matrix.scaleM(mMatrix, 0, factorx, factory, 0.1f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定2
        CurrentTime.setTexture();
        MyTexRectangular.draw(.1f, .1f, .5f, .2f, 0.f);

        //画面右上の最前面(1,1,-1)付近にドロイド君を表示(全部表示されるように位置を修正)
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.translateM(mMatrix, 0, 1f - 0.1f * factorx, 1f - 0.1f * factory, -1f);
        Matrix.scaleM(mMatrix, 0, 0.2f * factorx, 0.2f * factory, 0.1f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定2
        SampleDroid.setTexture();
        MyTexRectangular.draw(.1f, .1f, .5f, .2f, 0.f);

        //画面中央の最後面(0,0,.999)にドロイド君を表示
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.translateM(mMatrix, 0, 0f, 0f, .999f);
        Matrix.scaleM(mMatrix, 0, 0.2f * factorx, 0.2f * factory, 0.1f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定2
        SampleDroid.setTexture();
        MyTexRectangular.draw(.1f, .1f, .5f, .2f, 0.f);

        //小さい地球の最前面にドロイド君を表示
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.translateM(mMatrix, 0, tmpPos2v[0], tmpPos2v[1], -.99f);
        Matrix.scaleM(mMatrix, 0, 0.2f * factorx, 0.2f * factory, 0.1f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定2
        SampleDroid.setTexture();
        MyTexRectangular.draw(.1f, .1f, .5f, .2f, 0.f);

        //大きい地球の最前面にHelloを表示
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.translateM(mMatrix, 0, tmpPos1v[0], tmpPos1v[1], -1f);
        Matrix.scaleM(mMatrix, 0, 0.3f * factorx, 0.3f * factory, 0.3f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定2
        Hello.setTexture();
        MyTexRectangular.draw(.1f, .1f, .5f, .2f, 0.f);

        GLES.enableShading(); //シェーディング機能を使う設定に戻す
        GLES.disableTexture();




14.オブジェクトを舞台装置の最前面に位置固定表示する。



3次元表示されている画面の最前面にオブジェクトを貼り付けます。

次のファイルをダウンロードして,作業に使います。
gles2sample10D.zip 
ただし,圧縮ファイル中の xxxx.png は res -> drawable にコピーします。

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    Rectangular.java         平面四角形のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    Texture.java             画像ファイルから作るテクスチャ
    StringTexture.java       文字列から作るテクスチャ
    TexCube.java             テクスチャを貼り付けられる立方体のプロムティブ(6面全てに同じテクスチャ)
    TexRectangular.java      テクスチャを貼り付けられる平面四角形のプリムティブ
    TexSphere.java           テクスチャを貼り付けられる球のプリムティブ

    sample.png               ドロイド君の画像(透明部のあるpngファイル)
    earthpicture.jpg         地球の画像
    lightsource.jpg          光源球の画像


新規プロジェクトGLES2Sample10DをBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図14.1,図14.2のように,小さな地球が最前面に位置固定表示されます。(他のオブジェクトが移動しても,カメラ視点が変化しても,右下の地球は表示位置を変えません。)


 図14.1 右下の地球が最前面に位置固定表示されているところ


 図14.2 右下の地球が最前面に位置固定表示されているところ


GLRenderer.java の onDrawFrame の最後の方で,「//カメラ座標に追随するオブジェクトの記述」以降に注目します。

こ こではカメラビュー変換行列cMatrixを座標原点手前に固定しています。そして描きたいオブジェクト(ここでは地球)を座原点付近に置きます。そうす ると,これまでに描いてきた画像に,全く異なる視点の画像を重ね合わせることが出来ます。そして,動きまわるオブジェクト以外に固定表示されるオブジェクトを同時に表示することができるようになります。

        //カメラ座標に追随するオブジェクトの記述
        Matrix.setLookAtM(cMatrix, 0,
                0f, 0f, 1.4f,  //カメラの視点
                0.0f, 0.0f, 0.0f, //カメラの視線方向の代表点
                0.0f, 1.0f, 0.0f);//カメラの上方向
        GLES.setCMatrix(cMatrix);
        Matrix.setIdentityM(mMatrix, 0);
        Matrix.translateM(mMatrix, 0, 0.3f, -0.4f, 0f);
        Matrix.rotateM(mMatrix, 0, angle * 5, 0, 1, 0);
        Matrix.scaleM(mMatrix, 0, 0.05f, 0.05f, 0.05f);
        GLES.updateMatrix(mMatrix);//現在の変換行列をシェーダに指定
        EarthPicture.setTexture();
        // r, g, b, a, shininess(1以上の値 大きな値ほど鋭くなる)
        MyTexSphere.draw(1f, 1f, 1f, 1f, 5.f);
        GLES.disableTexture();  //テクスチャは使わない。





15.マルチシェーダによる高速化。



複数のシェーダを切り替えながら使うようにプログラムを変更します。

次のファイルをダウンロードして,作業に使います。
gles2sample11.zip 
ただし,圧縮ファイル中の xxxx.png は res -> drawable にコピーします。

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    Rectangular.java         平面四角形のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    Texture.java             画像ファイルから作るテクスチャ
    StringTexture.java       文字列から作るテクスチャ
    TexCube.java             テクスチャを貼り付けられる立方体のプロムティブ(6面全てに同じテクスチャ)
    TexRectangular.java      テクスチャを貼り付けられる平面四角形のプリムティブ
    TexSphere.java           テクスチャを貼り付けられる球のプリムティブ

    sample.png               ドロイド君の画像(透明部のあるpngファイル)
    cherry.jpg               桜の花の画像
    cloud.jpg                夕焼け雲の画像
    earthpicture.jpg         地球の画像
    goldfish.jpg             金魚の画像
    lightsource.jpg          光源球の画像
    rock.jpg                 岩石の画像
    woodenbox.jpg            木箱の画像


新規プロジェクトGLES2Sample11をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,GLES2Sample09と全く同じ図15.1のように表示されます。

texture4.jpg


    図15.1 テクスチャ付きの5つの球と2つの立方体

「14」までは4つのシェーディングモードを使っていましたが,プログラミング上では,シェーダプログラム内でif 文によって,4つの異なる動作をするようになっていました。シェーダプログラムでは,バーテックスシェーダは与えた頂点の数だけ呼び出され,フラグメント シェーダは画面を構成する点の数だけ呼び出されます。そのため,シェーダプログラム内でのif文は負担が大きいようです。そこで,使いたいシェーディング モードを決めた時点でif文を持たない4つのシェーダのうちの一つを使うことによって高速化を図ります。(しかし実際にはシェーダ切替時のオーバーヘッド も大きいようです。)ここでも,表15.1に示す4つのシェーディングモードを使うことにします。

             表15.1 GLES2Sample11における4つのシェーディングモード


シェーディングを使うかどうか
(光源の影響を表現する)
テクスチャを使うかどうか
マルチシェーディングでの
シェーディングモード
1
GLES2Sample11の
座標軸
使わない
使わない
SP_SimpleObject
2
GLES2Sample08の
球や立方体
使う
使わない
SP_ObjectWithLight
3
GLES2Sample11の
光源を表している球
使わない
使う
SP_SimpleTexture
4
GLES2Sample11の
地球や木箱など
使う
使う
SP_TextureWithLight


4つのシェーディングモードはそれぞれバーテックスシェーダとフラグメントシェーダを持ち,GLES.javaに定義され,アプリケーション起動時にコンパイル・リンクされ使えるようになっています。
どのシェーダモードを使うかは,GLRenderer.java中のメソッドonDrawFrameで図形を描く時に

GLES.selectProgram(GLES.SP_SimpleObject);

の様に設定します。


16.フォンシェーディング



これまで,シェーディング効果を有効にすると言ったら,グーローシェーディング(Gouraud shading)を指していました。ここではフォンシェーディングをシェーダに取り込みます。マルチシェーダ構成なので,新しいシェーダを取り込むのは容易になりました。

次のファイルをダウンロードして,作業に使います。
gles2sample12.zip 
ただし,圧縮ファイル中の xxxx.png は res -> drawable にコピーします。

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    Circle.java              円のプリムティブ
    Sphere.java              球のプリムティブ
    Line_PtoP.java           2点間を結ぶ線分プリムティブ
    Rectangular.java         平面四角形のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    Texture.java             画像ファイルから作るテクスチャ
    StringTexture.java       文字列から作るテクスチャ
    TexCube.java             テクスチャを貼り付けられる立方体のプロムティブ(6面全てに同じテクスチャ)
    TexRectangular.java      テクスチャを貼り付けられる平面四角形のプリムティブ
    TexSphere.java           テクスチャを貼り付けられる球のプリムティブ

    lightsource.jpg          光源球の画像


新規プロジェクトGLES2Sample12をBasic Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図16.1,2のように表示されます。

GouraudShading.jpg
図16.1 右側の正方形(グーローシェーディングを適用)に光を当てたところ
    光源反射の光点が正方形中央に出来ずに,頂点部分に不自然な
    光点ができる。
PhongShading.jpg
図16.2 左側の正方形(フォンシェーディングを適用)に光を当てたところ
    光源反射の光点が正方形中央にできる
グーローシェーディングでは,三角形(ここに描いた正方形は三角形2つでできている)の3つの頂点につい て,光源方向ベクトル,面の法線ベクトル,視点方向ベクトルから,色(明るさ)をバーテックスシェーダが計算し,三角形の内部の色は頂点の色から内挿(内 装している部分は見えない)してフラグメントシェーダが決めています。そしてグラデーションを付けて色を滑らかに変化させています。そのため,頂点部分に は強い反射の光点ができますが,正方形の中心辺りには光点は出来ません。
しかし,フォンシェーディングでは,三角形の3つの頂点での面の法線ベクトルから,三角形内部の点における面の法線ベクトルを内挿(内挿しているところは 見えない)し,光源方向ベクトル,視点方向ベクトルを使ってフラグメントシェーダ内でその点の色を決定しています。そのため,正方形の中心に強い反射の光 点ができます。


付録1 layout上にOpenGLのviewを載せ,メニュー・ボタンを使う


1.1 layout上にOpenGLのviewを載せる

これまで本編で扱われているOpenGLの表示は,layoutを使わずにActivity上に直接MyGLSurfaceViewを置いて描画していました。

                 OpenGLの画面(GL_View)
                 (______________________)
     (______________________________)CoordinatorLayout

付録1ではlayout中にMyGLSurfaceViewを置く方法をメモとして,ここに述べます。
工夫すれば,ボタンなどと並んでOpenGLの描画ができるようになるはずです。

                 OpenGLの画面(GL_View)
            (______________________)
                (__________________________)RelativeLayout
     (______________________________)CoordinatorLayout


次のファイルをダウンロードして,作業に使います。
gles2sample01 の MainActivity.java と MyGLSurfaceView.java を修正して作りなおしてあります。
gles2sample01A.zip 

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ


新規プロジェクトGLES2Sample01AをEmpty Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。
前回と同様に作業すると,図A1.1のようなプロジェクトになります。

android studio 2.0以降では,Empty Activity では activity_main.xmlに設定を直接書き込むようになっているので,
CustomViewの設置はactivity_main.xml 内に行うようにします。


最初はactivity_main.xml内に"Hello World"を表示するためのTextViewが設置されているので,
図A1.3のComponent TreeのところにあるTextView(図A1.3では既に消されている。)を消去します。
先にMyGLSurfaceView.javaを作ってから,図A1.1中央のpaletteからCustomViewを選択すると,
図 A1.2のようなCustomViewの選択画面が出てくるので,MyGLSurfaceViewを選び,
図A1.3のように配置します。最初にあったTextViewのところがCustomViewが置き換わります。
そして図 A1.4のようにIDを設定します。(id_myGLviewとしました。)


クラス MainActivity.java の
protected void onCreate(Bundle savedInstanceState)
ではこれまでコメントアウトしていた次の行が復活しています。
setContentView(R.layout.activity_main);
これは,res\layout\activity_main.xml を使って画面を構成することを意味します。

そして先ほどCustomViewのところで設定したIDを取り込みます。
 glView=(MyGLSurfaceView)findViewById(R.id.id_myGLview);
このようにして,glviewをカスタムビューと紐付けします。

一方,クラス MyGLSurfaceView のコンストラクタは

MyGLSurfaceView(Context context, AttributeSet attrs)

このように変更して次の行も
super(context, attrs);
このように変更しています。
その結果,図A1.5のように表示されます。白い枠があるのは,layoutのマージン(16dp)のためで,activity_main.xmlでマージンの設定行を削除すれば,白い枠は消えます。

画面構成は,RelativeLayout上にカスタムビューであるOpenGLの画面が載っているイメージとなっています。


MainActivity.java中で
public class MainActivity extends AppCompatActivity {
このようになっているとアプリケーション名を表示するアプリケーションバーが表示されます。
public class MainActivity extends Activity {
このようになっているとアプリケーションバーは表示されません。
どちらか好ましい方を選びます。ここでは extends Activity にしているので,アプリケーションバーは表示されません。


                 OpenGLの画面(GL_View)
          (__________________________)
              (__________________________)RelativeLayout
     (______________________________)CoordinatorLayout


custumview0.jpg
           図A1.1 プロジェクトの様子(content_main.xmlは無いのでactivity_main.xmlに対して作業する)

custumview1.jpg
       図A1.2 CustomViewの選択

custumview2.jpg
                    図A1.3 CustomViewの追加

custumview3.jpg
      図A1.4 CustomViewへのIDの設定(左側,先ほど置いたCustomViewが選択されている状態で行う

custumview4.jpg
図A1.5 枠付のOpenGLの画面表示



1.2 ツールバーのポップアップメニューを使う

ツールバーメニューのポップアップを使えるようにしました。
このツールバーはアプリケーションバー内に組み込まれています。
図A1.7のようなアプリケーション名(GLES2sample02E)が書いてある幅広の横帯部分がアプリケーションバーです。


次のファイルをダウンロードして,作業に使います。
gles2sample02 の MainActivity.java と MyGLSurfaceView.java,GLRenderer.java を修正して作りなおしてあります。
gles2sample02E.zip 

付録1.1で新しいプロジェクトは生成の際にEmpty Activityを使ったのですが,
toolbarのメニューを使う場合はBasic Activityを使います。

フォルダ内のファイルは次のとおりです。

    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    activity_main.xml        layout設定 (AndroidStudioのバージョンに依存するので,参考にしてください)
    content_main.xml         layout設定 (AndroidStudioのバージョンに依存するので,参考にしてください)

androiod studio 2.以降 のBasicActivityデフォルト設定のレイアウトはactivity_main.xmlで定義されており,
またactivity_main.xmlはcontent_main.xmlをインクルードしています。
content_main.xml内に,RelativeLayoutがあるので,図A1.6のように1.1と同じようにCustomViewを貼り付けます。
そして図 A1.4のようにIDを設定します。(id_myGLviewとしました。)

なお,Active_main.xml中の「fab(CustomView)」は使わないので削除しておきます。

MainActivity.java内にメニューアイテム,メニューID,メニュー文字列を設定し,メニューが選ばれた時の動作を定義します。
メニューが選ばれた時の動作はMyGLSurfaceView.java経由でGLRenderer.javaに伝達され,動作に反映されます。

画面構成は,画面上方にツールバーがあり,下方には広いRelativeLayoutがあります。ツールバー内に,ポップアップメニューが置かれ,RelativeLayout上にカスタムビューであるOpenGLの画面が載っているイメージとなっています。


  ポップアップメニュー(__)
      ツールバー(___)            OpenGLの画面(GL_View)
アプリケーションバー(_____)        (_____________)
   AppBarLayout(_______) (__________________)RelativeLayout
        (______________________________)CoordinatorLayout




        図A1.6 カスタムビュー(GLSurfaceView)の貼り付け




 図A1.7 ツールバーが組み込まれたアプリケーションバー
(右上の白い点をタップすると次図のメニューが現れる)





  図A1.8 メニューを開いたところ

1.3 GL_View上へのボタンの設置


画面上部に動作指示を行うボタンを設置しました。このボタンはどれか1つが押されると,OpenGL画面中の立方体の動作を指示し,ボタンはすべて消えま す。そして,画面のどこかをタップすると3つのボタンが現れます。実用アプリでは,通常はボタンが消えていて,画面が広く使え,必要になったら画面タップ でボタンを呼び出して,作業という使い方を想定しています。なお,ボタンは押すと消えてしまいますが,なにも操作しない時間が3秒続いたらボタンを消すと いった使い方もできると思います。


次のファイルをダウンロードして,作業に使います。
gles2sample02 の MainActivity.java と MyGLSurfaceView.java,GLRenderer.java を修正して作りなおしてあります。
gles2sample02E2.zip 


新規プロジェクトGLES2Sample02E2をここでは再びEmpty Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。

フォルダ内のファイルは次のとおりです。
ただし,圧縮ファイル中の buttondesign.xml は res -> drawable にコピーします。
activity_main.xml はres/layout中にコピーせずに参考にしてください。(Android Studioのバージョンに依存するため)
activity_main.xmlをそのまま使う場合はactivity_main.xml中の次の2つの行
 tools:context="com.example.tommy.gles2sample02e2.MainActivity"
 com.example.tommy.gles2sample02e2.MyGLSurfaceView
のgles2sample02e2をプロジェクトの名前に合わせてください。

    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    activity_main.xml        layout設定 (AndroidStudioのバージョンに依存するので,参考にしてください)
    buttondesign.xml         ボタンのカスタム設定 (これはres/drawableへコピー)

androiod studio 2.以降 のEmpty Activityデフォルト設定のレイアウトはactivity_main.xmlで定義されています。
activity_main.xml 内に,RelativeLayoutがあるので,それを取り払ってからFramelayoutを載せ,図A1.6のように1.1と同じように CustomViewを貼り付けてあります。ただしこの作業はlayoutをtext表示にして手作業で行います。(zipに含まれるactivity_main.xmlを参考にしてください)

画面構成は,FrameLayoutがあり,その上にカスタムビューであるOpenGLの画面を載せ,さらにその上にTableLayoutを載せ,
TableLayout上に複数のボタンを置いています。FrameLayoutは複数のviewを重ねてのせることが出来るため,
このような構成になりました。
ボタンの表示・非表示はTableLayoutの表示・非表示で実現しています。

              ボタン  ボタン  ボタン
             (___)(___)(___)
            (______________________)TableLayout
非表示         (______________________)OpenGLの画面(GL_View)
AppBarLayout(__) (______________________)FrameLayout
     (______________________________)CoordinatorLayout


3つのボタンのIDはb_left,b_stop,b_rightとし,それぞれのbackgroundにはdrawable/buttondesign.xmlを設定しています。
MainActivity.java中のonCreateで3つのボタンのIDからボタンを取り込み,OnClickListener clickedを設定し,
タップされた時の動作を記述しています。
ボタンの表示・非表示はTablelayoutの表示・非表示で行なっています。
ボタンをタップすると,立方体オブジェクトにボタンに応じた動作をさせ,ボタンを非表示にするためにtablelayoutを非表示にします。
ボタンを表示するには画面のどこかをタップすることにしています。そのため,MyGLSurfaceView中でtablelayoutのインスタンスを操作する必要があります。
ボタンへのタップはMainActivityで受け取っているのでそのままtablelayoutを操作できるので すが,画面へのタップはMyGLSurfaceViewで受け取るため,MyGLSurfaceViewにTablelayoutのインスタンスを渡して います。

MainActivity.java中で
public class MainActivity extends AppCompatActivity {
ではなく
public class MainActivity extends Activity {
を使用して,アプリケーションバー(アプリケーション名が書いてある幅広の横帯部分)を消しています。

res\values\styles.xml中の
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

<style name="AppTheme" parent="android:style/Theme.NoTitleBar.Fullscreen">
に変更すると,一番上のステイタスバー(電池残量や現在時刻を表示しているところ)を消すことが出来ます。




ボタンが表示されているところ
どれかのボタンを押すと,指定された動作となり,
ボタンが非表示になります。
ボタンが消えているところ
画面の任意の場所をタップするとボタンが表示され
ます。

            図A1.9 ボタンによる動作指示


1.4 GL_View上へのイメージボタンの設置


1.3 にて画面上部に動作指示を行うボタンを設置しましたが,ここでは文字で表現するボタンの代わりにイメージを貼り付けたイメージボタンを使います。このボタ ンはどれか1つが押されると,OpenGL画面中の立方体の動作を指示し,ボタンはすべて消えます。そして,画面のどこかをタップすると3つのボタンが現 れます。実用アプリでは,通常はボタンが消えていて,画面が広く使え,必要になったら画面タップ でボタンを呼び出して,作業という使い方を想定しています。なお,ボタンは押すと消えてしまいますが,なにも操作しない時間が3秒続いたらボタンを消すと いった使い方もできると思います。


次のファイルをダウンロードして,作業に使います。
gles2sample02 の MainActivity.java と MyGLSurfaceView.java,GLRenderer.java を修正して作りなおしてあります。
gles2sample02E3.zip 


新規プロジェクトGLES2Sample02E3をここでもEmpty Activity デフォルト設定のまま作成し,上記のファイルを取り込ませます。

フォルダ内のファイルは次のとおりです。
activity_main.xml はres/layout中にコピーせずに参考にしてください。(Android Studioのバージョンに依存するため)

    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    Cube.java                立方体のプリムティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    activity_main.xml        layout設定 (AndroidStudioのバージョンに依存するので,参考にしてください)
    gobackward.png           イメージ(左向き矢印) (これはres/drawableへコピー)
    goforward.png            イメージ(右向き矢印) (これはres/drawableへコピー)
    stop.png                 イメージ(ストップ) (これはres/drawableへコピー)

  
androiod studio 2.以降 のEmpty Activityデフォルト設定のレイアウトはactivity_main.xmlで定義されています。
activity_main.xml 内に,RelativeLayoutがあるので,それを取り払ってからFramelayoutを載せ,図A1.6のように1.1と同じように CustomViewを貼り付けてあります。ただしこの作業はlayoutをtext表示にして手作業で行います。(zipに含まれるactivity_main.xmlを参考にしてください)

画面構成は,FrameLayoutがあり,その上にカスタムビューであるOpenGLの画面を載せ,さらにその上にTableLayoutを載せ,
TableLayout上に複数のイメージボタンを置いています。FrameLayoutは複数のviewを重ねてのせることが出来るため,
このような構成になりました。またイメージボタンにはボタンの画像を取り込むようにイメージボタンのプロパティのsrcに画像ファイルを設定します。
ボタンの表示・非表示はTableLayoutの表示・非表示で実現しています。

              イメージ イメージ イメージ
              ボタン  ボタン  ボタン
             (___)(___)(___)
            (______________________)TableLayout
非表示         (______________________)OpenGLの画面(GL_View)
AppBarLayout(__) (______________________)FrameLayout
     (______________________________)CoordinatorLayout


3つのボタンのIDはb_left,b_stop,b_rightとし,それぞれのbackgroundには0を設定して背景を透明にしています。
MainActivity.java中のonCreateで3つのボタンのIDからボタンを取り込み,OnClickListener clickedを設定し,タップされた時の動作を記述しています。
ボタンの表示・非表示はTablelayoutの表示・非表示で行なっています。
ボタンを表示するには画面のどこかをタップすることにしています。そのため,MyGLSurfaceView中でtablelayoutのインスタンスを操作する必要があります。
ボタンをタップすると,立方体オブジェクトにボタンに応じた動作をさせ,ボタンを非表示にするためにtablelayoutを非表示にします。
ボタンへのタップは MainActivityで受け取っているのでそのままtablelayoutを操作できるのですが,画面へのタップはMyGLSurfaceViewで 受け取るため,MyGLSurfaceViewにTablelayoutのインスタンスを渡しています。


MainActivity.java中で
public class MainActivity extends AppCompatActivity {
ではなく
public class MainActivity extends Activity {
を使用して,アプリケーションバー(アプリケーション名が書いてある幅広の横帯部分)を消しています。


res\values\styles.xml中の
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

<style name="AppTheme" parent="android:style/Theme.NoTitleBar.Fullscreen">
に変更すると,一番上のステイタスバー(電池残量や現在時刻を表示しているところ)を消すことが出来ます。


図A1.10 イメージボタンによる動作指示


付録2 スワイプ,ショートタップ,ロングタップ,ピンチ,ローテーション


2.1 最初のサンプルプログラムgles2sample01を改造し,2本指・3本指ジェスチャを有効にする

これまで GestureDetector を使って,スワイプとショートシングルタップを検出していました。これに2本指のピンチイン・ピンチアウトとローテーション,スワイプを検出しようとしても,GestureDetector がこれらの検出の邪魔をしていて,うまく動作しません。
すでに,この課題はchicketen氏によって解決されていまして,ここにMITライセンスのもとに紹介されています。
https://github.com/chicketen/AndroidSample
考え方はこちらにありました。
http://chicketen.blog.jp/archives/1509250.html
これをヒントに1つのジェスチャ検出クラスにすべてのジェスチャをまとめて,さらに2本指,3本指が使えるように拡張しました。
必要があれば4本指,5本指・・・にもすぐに拡張が可能です。(ありえないが一応20本指まで想定している。)

次のファイルをダウンロードして,作業に使います。
gles2sample01 に GestureDetector.java と GestureListener.java が追加され,
gles2sample01 の MainActivity.java と MyGLSurfaceView.java を修正して作りなおしてあります。
gles2sample01B.zip 

フォルダ内のファイルは次のとおりです。
    MainActivity.java        AndroidのMainActivity
    MyGLSurfaceView.java     GLSurfaceViewの派生クラス 画面タッチ関係
    GLES.java                OpenGL_ESの舞台裏(シェーダーはここにある)
    GLRenderer.java          表示部本体
               onDrawFrame()で表示したいものを並べる。
    Axes.java                3軸表示用プリミティブ
    BufferUtil.java          OpenGL_ESで用いるバッファ
    GestureDetector.java     ジェスチャ読み取り
    GestureListener.java     ジェスチャによる関数呼び出しのひな形 classではなくinterfaceとして定義されている。
                             実際のメソッドはMyGLSurfaceView.java内に@Override書く
   

実行すると図A2.1のように実行できます。2本指で回転動作をしたところです。2本指ピンチイン・ピンチアウトも出来,カメラが座標原点から遠ざかったり,近づいたりします。
一本指のスワイプは,座標原点を中心としたティルト回転をさせますが,2本指のスワイプは,カメラ自体を左右上下に回転させるようにしています。
また3本指ジェスチャも使えるようになっていますが,このプログラムでは,3本指スワイプは,2本指スワイプと同じ動作をします。しかし3本指ピンチイン・ピンチアウトはカメラの視野角(画角)を変化させ,カメラのズーム機能(ワイドから望遠)に対応させています。
 
gesture1.jpg
図A2.1 2本指で表示を回転させたところ (Y軸が倒れている)
一本指のスワイプ動作を受け取っているのは,
 MyGLSurfaceView.java

public void onOneFingerMove(GestureDetector detector)
です。

2本指回転動作,2本指スワイプ動作,2本指ピンチイン・ピンチアウト動作を受け取っているのは
public void onTwoFingerMove(GestureDetector detector)
です。そして,
2本指回転動作は
renderer.setRotAngle(Rotation);でGLRenderer.javaに伝えています。
2本指スワイプ動作は
renderer.setScroll2Value(Delta2X, Delta2Y);でGLRenderer.javaに伝えています。
2本指ピンチイン・ピンチアウト動作は
renderer.setViewLength(factor)でGLRenderer.javaに伝えています。

3本指回転動作,3本指スワイプ動作,3本指ピンチイン・ピンチアウト動作を受け取っているのは
public void onTwoFingerMove(GestureDetector detector)
です。そして,
3本指回転動作は
renderer.setRotAngle(Rotation);でGLRenderer.javaに伝えています。(2本指の時と同じ)
3本指スワイプ動作は
renderer.setScroll2Value(Delta2X, Delta2Y);でGLRenderer.javaに伝えています。(2本指の時と同じ)
3本指ピンチイン・ピンチアウト動作は
renderer.setZoomFactor(factor)でGLRenderer.javaに伝えています。


GLRendererのonDrawFrameでのカメラビュー変換のところが次のように修正されています。


        GLES.gluPerspective(pMatrix,
                viewingangle,  //Y方向の画角
                aspect, //アスペクト比
                1.0f,   //ニアクリップ   z=-1から
                100.0f);//ファークリップ  Z=-100までの範囲を表示することになる
        GLES.setPMatrix(pMatrix);


        //カメラビュー変換(視野変換)-----------------------------------
        //カメラ視点が原点になるような変換
        Matrix.setLookAtM(c1Matrix, 0,
                (float) (viewlength * Math.sin(beta) * Math.cos(alph)),  //カメラの視点 x
                (float) (viewlength * Math.sin(alph)),                    //カメラの視点 y
                (float) (viewlength * Math.cos(beta) * Math.cos(alph)),  //カメラの視点 z
                0.0f, 0.0f, 0.0f, //カメラの視線方向の代表点
                0.0f, 1.0f, 0.0f);//カメラの上方向
        Matrix.setIdentityM(c2Matrix, 0);
        Matrix.rotateM(c2Matrix, 0, -AngleHor, 0, 1, 0);
        Matrix.rotateM(c2Matrix, 0, -AngleVer, 1, 0, 0);
        Matrix.rotateM(c2Matrix, 0, -RotAngle, 0, 0, 1);
        Matrix.multiplyMM(cMatrix, 0, c2Matrix, 0, c1Matrix, 0); //cMatrix = c2Matrix * c1Matrix
        //カメラビュー変換はこれで終わり。
        GLES.setCMatrix(cMatrix);

また,シングルショートタップ,ロングタップを受け取っているのは,MyGLSurfaceView.javaの
public void onShortTap(GestureDetector detector)
public void onLongTap(GestureDetector detector)
ですが,ここでは受け付けても,Toastで,画面に短いメッセージを出すだけになっています。
また,ショートタップやロングタップ時に,指先が多少動いてしまうことが有りますが,その分をもとに戻すようにしています。

回転動作終了直後や,ピンチイン・ピンチアウト終了直後での誤動作を回避する必要があり,
GestureDetector.javaの
        if   (deltat<TimeConstant) {
            mListener.onShortTap(this);
        }  else   if  (TimeConstant2<deltat &&   deltat<TimeConstant3   &&   d2<DisplacementLimit) {
            mListener.onLongTap(this);
        }
ここで工夫しています。
また,ロングタップは,スワイプによる微修正動作と区別する必要があり,3秒程度の指の停止を求めています。

実際にシングルショートタップとロングタップを試してみてください。画面にtoast表示が出てきたらOKです。
またピンチインピンチアウトも2本指と3本指では動作が異なります。
2本指でピンチアウト拡大すると,カメラが前進するので,カメラがオブジェクトと衝突してしまい,オブジェクトがニアクリップ効果で見えなくなります。
3本指でピンチアウト拡大すると,カメラのズーム機能と同じ動作をするので,カメラの位置は変化しないので,カメラがオブジェクトと衝突することはありません。


2.2 テクスチャサンプルプログラムなどを改造し2本指・3本指ジェスチャを有効にする

gles2sample09,gles2sample11 を修正し

gles2sample09B.zip
gles2sample11B.zip

の2つ例を作りました。
新規プロジェクトGLES2Sample09B,GLES2Sample11Bの作成をします。
こうして出来たものを実行すると図A2.2,図A2.3の様になります。

gles2sample09Bはテクスチャ+2本指3本指ジェスチャ対応版です。
gles2sample11Bはマルチシェーダ+テクスチャ+2本指3本指ジェスチャ対応版なので,テンプレートとして今後使えると思います。

この例では,シングルショートタップは,視点変換します。
1本指のスワイプは,座標原点を中心としたティルト回転をさせます。
2本指のスワイプは,カメラ自体を左右上下に回転させるようにしています。
2本指ピンチイン・ピンチアウトでは,カメラを前後に移動させ,オブジェクトに近づいたり遠ざかったりします。
2本指ローテーションはカメラを視野中心軸回りに回転させます。
3本指スワイプは,2本指スワイプと同じ動作をします。
3本指ローテーションは,2本指ローテーションと同じ動作をします。
3本指ピンチイン・ピンチアウトはカメラの視野角(画角)を変化させ,カメラのズーム機能(ワイドから望遠)に対応させています。

このサンプルでは,時刻をテクスチャとして作成し,最前面に表示していますが,AndroidのTextViewに表示する例を付録7に載せています。

pinchin.jpg
図A2.2 回転しピンチインした画面(元の画面は図12.1)

pinchout.jpg
図A2.3 回転しピンチアウトした画面(元の画面は図12.1)


付録3 ショートタップしたところにあるオブジェクトを検出する



ショートシングルタップで,表示されているオブジェクトに指示を出したい場合があります。
ジェスチャ取得のためのハンドラは,タップされたデバイス座標を受け取ることが出来ます。
このデバイス座標は,MyGLSurfaceViewクラス内onTranslationEndで取得し ているので,MyGLSurfaceViewの左上を原点とした座標で,その範囲はGLRendererのonSurfaceChangedで得られた領 域です。(MainActivity内で取得すると表示画面全体の左上が原点になります。)
この座標をGLRendererのsingleshorttap()で受け取って,正規化座標系  -1<x,y<1に変換し,オブジェクト(図形)の原点を正規化座標系に変換したもののx,y座標を比較判断して,どのオブジェクトがタップ されたかをToastで通知するようにプログラムしました。

また,表示最上面(正規化座標系)に4つのボタンを描いたテクスチャを貼り,ボタンの位置がタップされたら,Toastで通知するようにプログラムしました。




gles2sample10 を修正し

gles2sample10C.zip

を作りました。この例では,大きい地球,小さい地球,光源になっている球,およびボタンをタップすると,タップされたことを表示します。
GLES2Sample10を元にして作っているので,それ以外の場所をタップすると視点も変わります。

 「MyGLSurfaceView.java」の「onTranslationEnd」中でシングルタップが認識されると「GLRenderer.java」の
「 singleshorttap(float DeviceX, float DeviceY)」が呼び出されます。この時,得られる座標系は,デバイス座標系で,DeviceXはViewの左端が0,右端が1になっている座標です。
また,DeviceYは上端が0,下端が1になっている座標です。一方正規座標系は,中央が(0,0),右端中央が(1,0),上端中央が(0,1) になる座標系なので,座標変換してから,各オブジェクトの正規座標系での座標と比較しています。

なお,タップ座標とオブジェクト座標の誤差をどの程度に設定するのが良いかは試して決めるのが良いように思います。

実行の様子を図A3.1に示します。速い動きだとなかなか正確にタップできないので,ゆっくり動くようにしてあります。

detectedshorttap.jpg
図A3.1 大きな地球をタップしたところ


図A3.2 ボタン赤をタップしたところ




付録4 オブジェクトの前後関係表示異常に対処する(zバッファー値の対数化)



図7.1(GLES2Sample04)において,前後方向表示範囲を前方に延ばして,手前のオブジェクトを表示するため,
GLRenderer.javaのpublic void onDrawFrame(GL10 glUnused)で

        GLES.gluPerspective(pMatrix,
                45.0f,  //Y方向の画角
                aspect, //アスペクト比
                1.0f,   //ニアクリップ   z=-1から
                100.0f);//ファークリップ  Z=-100までの範囲を表示することになる



        GLES.gluPerspective(pMatrix,
                45.0f,  //Y方向の画角
                aspect, //アスペクト比
                0.0001f,   //ニアクリップ   z=-0.0001から
                100.0f);//ファークリップ  Z=-100までの範囲を表示することになる

にしたところ,オブジェクトの前後関係が崩れて,図A4.1の様になってしまいました。


    図A4.1 オブジェクトの前後関係がおかしい
      球体,立方体には円軌道が突き刺さるはず
これは,OpenGLのオブジェクト表示の時に前後関係を加味して表示するからくりであるzバッファ値を計算する関数の振る舞いが悪く,
手前のオブジェクトに対して分解能が高く,後方のオブジェクトでは分解能が殆ど無いことに起因しています。(運良くうまく表示されることもあります。)

これへの対処は
http://outerra.blogspot.com.ar/2012/11/maximizing-depth-buffer-range-and.html
http://qpp.bitbucket.org/translation/maximizing_depth_buffer_range_and/
で述べられているように,zバッファ値を対数化することで改善されます。

バーテックスシェーダ中で,zバッファ値はgl_Position.zに与えればよいのですが,後ほど見えないところの計算でgl_Position.wで割られて正規化されるため,
gl_Position.wをかけた値をgl_Position.zに保存する必要があります。ただし,ただし,プロジェクション変換
gl_Position=u_PMatrix*u_MMatrix*a_Position;
が終わった段階で,z値はgl_Position.wに保存されています。しかも左手系座標になっているため,z軸が反転し,
0<gl_Position.wになっています。
対数化して,z値(gl_Position.wに保存されている)のnearからfarまでを-1から1の範囲に割り当てれば良いので,
if (0.<gl_Position.w) {
    gl_Position.z = 2.0*log(gl_Position.w/near)/log(far/near) - 1;
    gl_Position.z *= gl_Position.w;
}
このような変換を行えばよいことになります。(ただし,far,nearはGLRenderer.javaから設定することが必要です。)

しかし,near付近の面を表示するためにはnearより手前にある頂点でz座標が正の領域も計算対象にできるようにする必要があります。



で変換ができます。Cの値は対数化関数をどれだけずらして使うかで決定します。
//p=log(z + c) を計算
//z=near のとき pn=log(near+c)
//z=far のとき pf=log(f + c)
//p=pn → depthz=-1 p=pf → depthz=1 になるように1次変換
// AA = 2/(pf - pn)    BB =-(pf + pn)/(pf - pn) とおいて
//depthz = AA p + BB とおく
if (0. < gl_Position.w + u_CC) {
    float p = log(gl_Position.w + u_CC);
    gl_Position.z = u_AA * p + u_BB;
    gl_Position.z *= gl_Position.w;
}

Cの値を変化させて比較してみてください。

gles2sample04F.zip (マルチシェーダ版は付録7を参照してください。)


が得られ,これを実行すると,正しい表示が得られます。


       図A4.2 正しい前後関係の表示

zバッファ値の保存時のbit幅

なお,zバッファ値の計算はfloat型で行われていますが,内部に保存される時は16bit整数になっています。
ですから16bitで表現できる範囲で比較が正しく行われます。16bitの分解能は1/32768ですから,4桁程度の精度です。
せっかく対数化zバッファ値を使っても16bit幅では桁落ちしてしまう場合は正しい比較が出来ません。
実はこのzバッファ値が16ビット幅であるというのは, MyGLSurfaceView.java のコンストラクタ中の
        setEGLConfigChooser(8, 8, 8, 8, 16, 0);
で設定していました。必要もないのに広い幅を取ってしまうと,メモリーの無駄遣いになるからです。
引数の最初の8,8,8,8は,カラーバッファのbit幅で,RGBAの4つの値が8bitずつ使うように設定されています。その次の16がzバッファ値 のbit幅です。最後の0はステンシルバッファ値のbit幅です。すべてのbit幅が自由に設定することができるわけではないのですが,zバッファ値の bit幅は24が使えます。
対数化zバッファ値を使ってもまだ足りない場合はzバッファ値のbit幅を24にして
        setEGLConfigChooser(8, 8, 8, 8, 24, 0);
で使ってみても良いでしょう。24bitの分解能は1/8388608ですから,7桁程度の精度です。
(NEXUS7(2012)は24bitモードに対応していません。他にも未対応のタブレットがあるかもしれないのでしばらくの間24bitモードは使わないほうが良いかもしれません。)


zバッファ値の作り方の考察

near=0.001,far=1000の場合について,次の4つのzバッファの作り方と変換値の振る舞いを考察します。
複数の変換方法の中で,(2)A+B/zの変換がOpenGLで使われている変換です。



z=0.001~z=1000までを横軸対数化グラフで比較します。ただし,(4)Alog(z+C)+BにおけるCの値はnearと等しくとっています。

zを横軸対数化グラフで比較すると次の図A4.3のようになります。
(1)Az+Bはnearに近い側の分解能の低い部分が広く,(2)A+B/zはfarに近い側の分解能の低い部分が広いことがわかります。

        図A4.3 変換方法の比較 z=0.001~z=1000

near近傍の様子をグラフで比較すると図A4.4の様になります。
(1)Az+Bはほぼ0になってしまいます。(2)A+B/zはz=0周辺が大きく変化し過ぎます。
(3)Alog(z)+B,(4)Alog(z+C)+Bは単純増加関数ですが,logの中が負になると値を持ちません。


     図A4.4 変換方法の比較 z=0の近傍

さらにnear近傍の拡大をグラフで比較すると図A4.5の様になります。
(4)Alog(z+C)+Bでは,zが負の領域まで値を持っています。z=near(=0.001)周辺でも値が急激な変化をしていません。


     図A4.5 変換方法の比較 z=0の近傍


付録5 シェーダの追加 頂点色指定のオブジェクト


マルチシェーダ構成では,必要に応じて新たなシェーダプログラム対(バーテックスシェーダとフラグメントシェーダ)を追加することができます。

これまで作成したシェーダプログラムはGLES2Sample11にて
・単色オブジェクトをそのまま表示
・単色オブジェクトに光を当てて表示
・オブジェクトにテクスチャを貼り付けてそのまま表示
・テクスチャを貼り付けたオブジェクトに光を当てて表示
の4つでした。

更にgles2sample12にて
・単色オブジェクトをフォンシェーディングで処理して表示
・テクスチャを張ったオブジェクトにフォンシェーディングで処理して表示
のシェーダプログラムを追加した例も16で示しました。

ここでは,一番簡単な,頂点色を指定して図形を表示するシェーダプログラムを追加します。
光を当てないで表示しますが,オブジェクトの頂点の色を設定し,オブジェクト内部の色は頂点間の色でグラデーションがかけられた色になるシェーダプログラムとなっています。
プログラム例では,このシェーダプログラムを使って次の2つの例を表示します。
(1) 4つの頂点の色を与えた矩形を表示する。矩形の内部は綺麗なグラデーションになります。これにはRectangularGradationクラスが用いられています。
(2) 円を描き,描く円の色が変化させる。これにはCircleGradationクラスが用いられています。

gles2sample11G.zip



  図A5.1 地球の円軌道が色が変化しながら描かれている。
      中央にはグラデーションで色付した矩形が描かれている。
      (この矩形の表示にはシェーディングは使われていない)


一般的に新たなシェーダプログラム対を追加するには,次のような手順となります。
・GLES.中に新たなシェーダプログラム対(バーテックスシェーダとフラグメントシェーダ)を定義する
 (String SimpleObjectVTXColor_VSCODEとString SimpleObjectVTXColor_FSCODE)
・GLES.中にシェーダプログラムIDを変数として追加する。
 (SP_SimpleObjectVTXColor)
・GLES.中のmakeProgram()でプログラムをコンパイルしてシェーダプログラムIDに番号を得る。
        SP_SimpleObjectVTXColor = makeProgram0(SimpleObjectVTXColor_VSCODE, SimpleObjectVTXColor_FSCODE);
        if (SP_SimpleObjectVTXColor == INVALID) return false;
・シェーダプログラム中のattribute変数が指示できるようにハンドルを得る。
    public static int colorHandle;  //頂点色ハンドル
・GLES.中のselectProgram()でシェーダプログラムIDが指示された時の作業を記述する。
        } else if (programID == SP_SimpleObjectVTXColor) {
            GLES20.glUseProgram(programID);
            //光源を使わない時の頂点色のハンドルの取得
            colorHandle = GLES20.glGetAttribLocation(programID, "a_Color");
            useLighting = false;
            GLES20.glEnableVertexAttribArray(colorHandle);
・GLES.中のdeselectProgram()でシェーダプログラムIDの時の処理を記述
        } else if (currentProgram == SP_SimpleObjectVTXColor) {
            GLES20.glDisableVertexAttribArray(colorHandle);
・頂点の色を定めたオブジェクトを定義し,表示する。
 例1
 ・CircleGradation.javaを定義する
 ・GLRenderer.javaでCircleGradationのインスタンスを表示する。
  ただし,シェーダプログラムIDのプログラムの使用宣言を忘れないこと
        GLES.selectProgram(GLES.SP_SimpleObjectVTXColor);
 例2
 ・RectangularGradation.javaを定義する
 ・GLRenderer.javaでRectangularGradationのインスタンスを表示する。
  ただし,シェーダプログラムIDのプログラムの使用宣言を忘れないこと
        GLES.selectProgram(GLES.SP_SimpleObjectVTXColor);




付録6 シェーダの追加 PointSpriteでblur


マルチシェーダ構成では,必要に応じて新たなシェーダプログラム対(バーテックスシェーダとフラグメントシェーダ)を追加することができます。

ここでは,ある点を中心としたぼんやりとした円形図形を表示するシェーダプログラムと点を表示する
シェーダプログラムを追加します。
(一般的なシェーダプログラムの追加方法は付録5を参照してください。)
図A6.1のように光源を表す白い球に重ねると光源が明るく光っているように見えます。
プログラムの大枠としてはPointSprite.javaで点表示のクラスを作成し,GLRenderer.java内で,特定の位置に点を描かせてい ます。点というのは,数学で言う点ではなく,ある大きさの正方形です。しかもどの方向から見ても同じ大きさの正方形に見えるちょっと不自然なずけいです。 正方形の大きさは自由に指定できます。
同じPointSpriteで点を描きますが,どちらのシェーダ
プログラムを用いるかによって,ぼんやりとした円形図形になったり,
ある大きさの正方形になったりします。
参考として,PointSpriteの正方形上にテキスチャを貼り付けたものもしまします。

シェーダプログラムSP_PointBlur:   ぼんやりとした円形図形
シェーダプログラムSP_PointSprite:  ある大きさの正方形
シェーダプログラムSP_TexPointSprite: ある大きさの正方形にテキスチャを貼り付けたもの

参考 GL_XXXXXの表現

 


gles2sample11H.zip


 


図A6.1 光源を表している白っぽい球体
のまわりに光芒が見えている。
明るい青の正方形は大きな点,緑色の正
方形(一部が見えている)も正方形,奥
に5つの小さな点も正方形です。
図A6.2 点(正方形)はどの方向から見
ても正方形に見える。
またリング板によって切られるところも
見えている。


このサンプルプログラムでは,バーテックスシェーダ側では,与えられた点の座標をカメラビュー座標gl_Positionに設定して,
フラグメントシェーダに渡しています。gl_Positionはビルトイン変数であり,宣言しなくても使えます。

シェーダプログラムSP_PointBlur

 点の大きさgl_PointSizeも次のように設定し,フラグメントシェーダに渡しています。
gl_PointSizeはビルトイン変数であり,宣言しなくても使えます。
            //点の大きさ指定
            "gl_PointSize=u_PointSize / gl_Position.w;"+
gl_Position.wの導入は遠近感を持たせるためです。
(gl_Position.wにはカメラビュー座標系における-zの値が入っています。そのため,gl_Position.wの値は常に正で,点が遠くにいるほど大きな値を持っています。)

gl_PointSizeの値には上限がありますが,これはハードウェア依存のようです。手元のタブレットでも機種によって異なります。
64以上は使わないようにしておくのが良いようです。
 
フラグメントシェーダでは大きさを指定された正方形(点が正方形となっている)内の
各フラグメントの色を定めています。gl_PointCoordもビルトイン変数ですが,
正方形内の位置を与える2次元ベクトル(x,y)で,0<x<1,0<y<1なので,正方形の中心は(0.5,0.5)となります。
(0,0)が中心になるように座標変換し,中心からの距離の2乗に対応させて,
指示した色情報u_ObjectColorの透明度を変化させてぼんやりとした輪郭を作成しています。

GLES中のフラグメントシェーダで次のように実装されています。
    //フラグメントシェーダのコード
    private final static String PointBlur_FSCODE =
        "precision mediump float;" +
        "uniform vec4 u_ObjectColor;" +
        "vec3 n;"+
        "vec4 cl;"+

        "void main() {" +
            "n.xy = gl_PointCoord * 2.0 - 1.0;"+
            "n.z = 1.0 - dot(n.xy, n.xy);"+
            "if (n.z < 0.0) {" +
                "discard;"+
            "} else {" +
                "cl=u_ObjectColor;"+
                "cl.a *= n.z;"+
                "gl_FragColor = cl;"+
            "}"+
        "}";

シェーダプログラムSP_PointSprite

 GLES中のフラグメントシェーダを次のような単純なものに置き換えると,正方形が描かます。
その正方形の大きさはgl_PointSizeで設定されたものになります。
球が半分正方形に埋もれて見えるようになります。ここに描かれた正方形が描かれた「点」です。
点ですと言われても困りますが,XXXXX.draw(0f,1f,.5f,1f,150f);のように描かせると,大きさ150で描かれた点ですので正方形が見えます。
大きさを6fくらいにするとよく見ると正方形ですが,点らしくなります。
 
    //単純なフラグメントシェーダのコード
    private final static String PointSprite_FSCODE =
        "precision mediump float;" +
        "uniform vec4 u_ObjectColor;" +

        "void main() {" +
            "gl_FragColor = u_ObjectColor;"+
        "}";



図A6.3 PointSpriteにテキスチャを貼り付けたもの
地球のテキスチャを貼り付けた正方形が5個で来ている


シェーダプログラムSP_TexPointSprite

 GLES中のフラグメントシェーダを次のような単純なものに置き換えると,
gl_PointSizeで設定された正方形にテキスチャが貼り付けられます。
XXXXX.draw(100f);のように描かせると,大きさ100で描かれた
テキスチャ付きの正方形が見えます。
 
    //フラグメントシェーダのコード
    private final static String TexPointSprite_FSCODE =
            "precision mediump float;" +
            "uniform sampler2D u_Texture;" +

            "void main() {" +
                "gl_FragColor = texture2D(u_Texture, gl_PointCoord);" +
            "}";


なお,このサンプルには,土星の輪のような平面リングを複数の台形で近似したリングを表示するため,
planeRing.javaとテクスチャ付きのリングを表示するTexPlaneRing.javaを紹介している。



付録7 マルチシェーダ時のzバッファー値の対数化


zバッファー値の対数化により,far/nearクリップ範囲が広い場合のオブジェクト前後関係の異常への対処法を付録4で述べていましたが,ここではマルチシェーダにした場合について述べます。GLES2Sample02E2を下敷きにして作りなおしています。
zバッファー値の対数化のためにシェーダに新たな変数が増えましたが,その変数への値の設定時期は,他の変数の設定時期に合わせなければなりません。すな わち。GLRendererは関数gluPerspectiveでGLESにnear,far値を設定しますが,シェーダにそれらの値を与えるの は,GLRendererが関数updateMatrixを呼んだときに関数updateMatrix内で行われます。これは光源の設定やカメラビュー変 換行列などを与えるのと同時になります。
zバッファー値の対数化は,GLES.javaにあるように各シェーダ毎に行うようになっています。

次のファイルをダウンロードして,作業に使います。
gles2sample13.zip 

ただし,圧縮ファイル中の buttondesign.xml は res -> drawable にコピーします。
activity_main.xml はres/layout中にコピーせずに参考にしてください。(Android Studioのバージョンに依存するため)
activity_main.xmlをそのまま使う場合はactivity_main.xml中の次の2つの行
 tools:context="com.example.tommy.gles2sample13.MainActivity"
 com.example.tommy.gles2sample13.MyGLSurfaceView
のgles2sample13をプロジェクトの名前に合わせてください。

activity_main.xml
Axes.java
BufferUtil.java
buttondesign.xml
CircleGradation.java
Cube.java
GestureDetector.java
GestureListener.java
GLES.java
GLRenderer.java
MainActivity.java
MyGLSurfaceView.java
PointSprite.java
Sphere.java

また,この例では時刻をTextViewに表示している。
layoutにおいて,FrameLayout上に,GLSurfaceView,TableLayout(button が並んでいる),RelativeLayout(TextView)の3つを重ねています。
時刻表示用のTextViewはMainActivityに所属していますが,これを GLSurfaceView経由でGLRendererに渡して,renderer内から表示できるようにしています。ただし,TextViewはUIス レッドでないと操作できないので,renderer内でhandlerを用いて新しいスレッドを作って,別スレッドで表示するようになっています。




図A7.1 実行の様子(下部にはTextViewで時刻を表示している)





付録8 半球,円柱のプリミティブを用いたドロイド君


半球,円柱のプリミティブを用いてドロイド君を描きます。

次のファイルをダウンロードして,作業に使います。
gles2sample14.zip 

ただし,圧縮ファイル中の buttondesign.xml は res -> drawable にコピーします。
activity_main.xml はres/layout中にコピーせずに参考にしてください。(Android Studioのバージョンに依存するため)
activity_main.xmlをそのまま使う場合はactivity_main.xml中の次の2つの行
 tools:context="com.example.tommy.gles2sample14.MainActivity"
 com.example.tommy.gles2sample14.MyGLSurfaceView
のgles2sample14をプロジェクトの名前に合わせてください。

activity_main.xml
Axes.java
BufferUtil.java
buttondesign.xml
CircleEX.java
ConeEX.java          
円柱プリミティブ(両端は閉じられていない),平面リングはこれでつくる 
Droid.java
GestureDetector.java
GestureListener.java
GLES.java
GLRenderer.java
MainActivity.java
MatStackUtil.java
MyGLSurfaceView.java
SphereEX.java       
半球プリミティブはこれでつくる
TexConeEX.java
TexTorusEX.java
TorusEX.java         1/4トーラス(胴体下部の丸み)はこれで作る

public void drawDroid()
  原点に,元のサイズ(大体大きさ1)のドロイド君モデルを描く(図の中央のドロイド君)

public void drawDroid(float[] DroidMatrix)
  ドロイド君モデルをモデル変換行列DroidMatrixで変換して描く(図で円周状を回るドロイド君)




図A8.1 実行の様子 中央がドロイド君,回りには回転しているドロイド君や,使用しているプリミティブ図形が描かれています。


基本資料 ドロイド君描画に用いた,回転体プリミティブの紹介


次のファイルをダウンロードして,作業に使います。
gles2sample14P.zip 

ただし,圧縮ファイル中の buttondesign.xml は res -> drawable にコピーします。
activity_main.xml はres/layout中にコピーせずに参考にしてください。(Android Studioのバージョンに依存するため)
activity_main.xmlをそのまま使う場合はactivity_main.xml中の次の2つの行
 tools:context="com.example.tommy.gles2sample14P.MainActivity"
 com.example.tommy.gles2sample14P.MyGLSurfaceView
のgles2sample14をプロジェクトの名前に合わせてください。

activity_main.xml
Axes.java
BufferUtil.java
buttondesign.xml
CircleEX.java
             Circle.javaを拡張し,自由度を高めている
ConeEX.java              
円錐を描くが,広い活用方法がある
earthpicture.jpg
GestureDetector.java
GestureListener.java
GLES.java
GLRenderer.java
MainActivity.java
MyGLSurfaceView.java
SphereEX.java             Sphere.javaを拡張し,自由度を高めている
TexConeEX.java            ConeEX.javaを継承してテクスチャを晴れるようにしている
TexSphereEX.java
          SphereEX.javaを継承してテクスチャを晴れるようにしている
TexTorusEX.java
           TorusEX.javaを継承してテクスチャを晴れるようにしている
Texture.java
TorusEX.java             トーラスを描くが,半分や1/4なども描けるようになっている

球は半球や1/4球など経度方向・緯度方向を任意に切り取った立体を描きます。
ただし,裏側の図形はケアしていないため,汚いままです。
また,それらにテクスチャを貼り付けることも出来ます。

円錐は円柱,円錐台,円錐,平面リング,円,これらの回転体の回転角度を任意の大きさにしたものを描きます。
ただし,裏側の図形はケアしていないため,汚いままです。
また,それらにテクスチャを貼り付けることも出来ます。

トーラス(ドーナツ型)やトーラスを経度方向・緯度方向を任意に切り取った立体を描きます。
ただし,裏側の図形はケアしていないため,汚いままです。
また,それらにテクスチャを貼り付けることも出来ます。




図A8.2 実行の様子 ただし,すべて同じテクスチャを貼っているため不自然な描画もあります。




付録9 日食・月食を想定した遮蔽球による影の描画


日食・月食の影を描画します。

次のファイルをダウンロードして,作業に使います。
gles2sample15.zip 

ただし,圧縮ファイル中の buttondesign.xml は res -> drawable にコピーします。
activity_main.xml はres/layout中にコピーせずに参考にしてください。(Android Studioのバージョンに依存するため)
activity_main.xmlをそのまま使う場合はactivity_main.xml中の次の2つの行
 tools:context="com.example.tommy.gles2sample14P.MainActivity"
 com.example.tommy.gles2sample14P.MyGLSurfaceView
のgles2sample14をプロジェクトの名前に合わせてください。

activity_main.xml
Axes.java
BufferUtil.java
buttondesign.xml
earthpicture.jpg
GestureDetector.java
GestureListener.java
GLES.java         ここに新たなシェーダプログラムを追加し,必要な変数の転送機能も追加した
GLRenderer.java
MainActivity.java
moonpicture.jpg
MyGLSurfaceView.java
SphereEX.java
TexSphereEX.java
Texture.java

OpenGLの描画では,光源と描画面との傾き,描画面の反射特性を考慮して,描画点の明るさを決定しています。光源と描画点の間に遮蔽物があって,その 影が生ずることは考慮されていません。遮蔽物の影の描写は様々な方法が知られていますが,代表的なのはシャドウマッピングがあります。しかしシャドウマッ ピングは光源を点光源と仮定するため,本影・半影を描画することが出来ません。そこで,日食や月食の描画に限定して,光源・遮蔽物がともに球の場合を考え ることにします。図A9.1のように描画点から見た光源球の見かけの大きさ(角度α),遮蔽球の見かけの大きさ(角度β),光源球と遮蔽球のそれぞれの中 心間の見かけの距離(角度r)を使って,描画点の明るさを制御する方法で影を描くことができます。


                 図A9.1 描画点,遮蔽球と光源球

光源球と遮蔽球の見かけの大きさによって,図A9.2のように次の2つの場合が考えられます。
しかし,日食・月食を考える場合はα<βを考えるだけでよいと思います。金環食はβ<αの場合ですが,
微かに大きいだけなので無視することにします。


     (1) α<βのとき          (2) β<αのとき
      図A9.2 光源球と遮蔽球の見かけの大きさと,α,β,rの関係

ここで明るさについて図A9.3のように大胆に一次近似します。
納得できない人のほうが大半だと思いますので,厳密解を自分で導いてください。
もっとも,光源球が見かけの円盤に見えるとき,明るさが一様であるかどうかも怪しいですね。


      (1) α<βのとき        (2) β<αのとき
            図A9.3 明るさについての大胆な一次近似

明るさ率 d 大胆な一次近似

d=1             β+α<r のとき

d=1/(2α)(r-β+α)  βーα<r<β+α のとき

d=0             r<βーα のとき

そして,新たなシェーダプログラムを,光源有り・テクスチャ使用タイプを拡張してGLES.javaに作ります。
バーテックスシェーダは,すでに光源の座標と描画点(観測点)の座標は知っているので,
  描画点から見た光源球の視半径,
  遮蔽球の座標,
  描画点から見た遮蔽球の視半径
を追加して与える必要があります。
描画点から見た2つの球の視距離はバーテックスシェーダ内で計算します。

描画点から光源球へのベクトルをL,描画点から遮蔽球までのベクトルをOとし,
光源球の半径をLr,遮蔽球の半径をOrとすると,
α=arctan(Lr/length(L))
β=arctan(Or/length(O))
r=arccos(Norm(L)・Norm(O))
ただし,Length(X)はベクトルXの大きさを表し,
Norm(X)はX/Length(X)を表し,
X・YはベクトルX,Yの内積を表します。

GLRenderer.java では,地球を描くときには遮蔽球として月を登録し,月を描くときには遮蔽球として地球を登録します。

影は普通は黒ですが,月への地球の影は大気の影響で赤みを帯びています。(皆既月食は赤みを帯びている)
このことを,模擬するため,影の色を与えることができるように拡張しています。

その結果,図A9.4-図A9.6のような描画が得られます。



        図A9.4 月が地球の影に入る直前の様子



          図A9.5 月が地球の影に入ったところ
(左半分は本影に突入し,赤みを帯びた影領域になっている。右半分は半影領域)



         図A9.6 月が地球に影を落としているところ




付録10 タブレットの姿勢取得 (VRへの応用) Rotation Vector


VRではタブレットの姿勢に合わせて,カメラ視点を動かします。
タブレットの姿勢取得においては,3軸重力加速度センサ,3軸磁力センサ,3軸ジャイロセンサを用いますが,これらのセンサから直接値を取得してもノイズ が多かったり,動揺が激しかったりで,実用に向きません。
実用に耐える姿勢値を得られるのが Rotation Vectorセンサです。Rotation Vectorは同次座標系回転行列(4×4)で,姿勢を与えてくれます。
ここで得られた回転行列を使っても,まだ動揺が大きいので,気になる場合は,更にローパスフィルタをかけると改善します。
この項目の最後を見てください。(gles2sample09R2.zip)
 
次のファイルをダウンロードして,作業に使います。
gles2sample09R.zip 

ジャイロセンサを持たないデバイスでも使用可能な方法はこの項目の最後を見てください。
(gles2sample09R2.zip)

このセンサの基準になる座標系は水平面がxy平面になっており,北(磁北)がy軸+,東(磁東)がx軸+で,天頂方向がz軸+になっています。
この基準からタブレットがどのように回転しているかを
,同次座標系回転行列(4×4)で取得できます。
Rotation VectorセンサはRotationSensor.javaのクラスで取得されます。

GLRenderer.jave内の次の設定は,カメラ位置を原点,カメラの焦点方向を地面真下に向け,北方向をカメラ視野の上方向に設定しています。
こうして得たカメラビュー変換行列「
c1Matrix」に,センサで得られた変換行列「c2Matrix」を後ろからかけて,使用するカメラビュー行列を作っています。

        Matrix.setLookAtM(c1Matrix, 0,
                0f,0f,0f,  //カメラの視点 x,y,z
                0.0f, 0.0f, -1.0f, //カメラの焦点
                0.0f, 1.0f, 0.0f);//カメラの上方向
        c2Matrix = myRotationSensor.getRotMatrix();

        //次の1行を挿入すると,カメラ位置が,原点ではなく(6,6,6)になるため,6個の立方体が俯瞰できる
        //Matrix.translateM(c2Matrix,0,-6f,-6f,-6f);

        Matrix.multiplyMM(cMatrix, 0, c1Matrix, 0, c2Matrix, 0); //cMatrix = c1Matrix * c2Matrix

        //カメラビュー変換はこれで終わり。
        GLES.setCMatrix(cMatrix);


注意 これまでと座標系のとり方のイメージが異なります。
   これはタブレットを縦位置(ポートレイト portrait)に置いて,
   タブレットを写真を取るときのように立てて見ていました。
   x軸+が画面右方向,y軸+が上方向を示していました。そしてz軸+が画面手前になっていたので,
   zx平面が水平面,y軸+が真上のように見ていました。
   今回はタブレット画面上のxyz軸のとり方は同じですが,タブレットを縦位置において,
   タブレットを後方に90度押し倒して水平に置いています。
   
短手方向右がx軸+で,長手方向奥側がy軸+,画面手前側がz軸+となっているのは変わりません。


図 A10.1 これまでの座標系(左)とここでの座標系(右)
実はタブレット自身では何も変わっていない。置き方が異なっているだけ。

サンプルプログラムでは画面の向きをportraitに固定しています。この状態で,タブレットを様々な方向に向けても,androidの通知バー(ここ では表示していないがアプリケーションバー)の位置が固定しているだけで,portraitでもlandscapeでも正しい方位と角度で見えるようにな ります。

GLRenderer.jave内では,
 北(磁北)y軸+
 東(磁東)x軸+
 天頂方向 z軸+

であることに合わせて,
North,East,South,West,Upper,Lowerと書かれた6個の立方体を,
北東南西,天頂・地底に置き,東の立方体のすぐ脇に座標軸を描いてみました。
そして,センサの精度を上げるための8の字運動を行った後に,
タブレットを東西南北,真上・真下に掲げるとそれぞれの方向に置いた立方体が見えました。
カメラ位置が原点なので,東西南北と真上真下に6個の立方体で囲まれている感じです。
(通常は磁気センサは電源OFFをになっているため,サンプルプログラム起動時に電源ONになります。
そのため,サンプルプログラム起動後暫くの間はセンサが不安定になり,方位角が乱れる場合があります。)


RotationSensor.javaでは,センサからの読み取り要求に従ってセンサリスナmSensorEventListenerがセンサ値を読み出して,
変換行列rotmatrixとして保存しています。
そしていつでも,float[] getRotMatrix() で外部から読み出せるようになっています。

注意する点

センサへの電源供給のON-OFFです。特に使わなくなったらOFFしておかないと,電池の無駄な消耗になります。
int setSensorsOn()の実行で  電源供給ON
void setSensorsOff()の実行で 電源供給OFF
そのため,この2つ関数をそれぞれMainActivityのonResume()とonPause()から呼び出します。

onResume()とonPause()で電源供給ONOFFが可能なように,
RotationSensorのインスタンスmyRotationSensorはMainActivity内につくります。
  myRotationSensor = new RotationSensor(this, mDisplay);
しかしmyRotationSensorを使用するのはGLRendererなので,GLRendererに渡します。
  glView.renderer.setRotationSensor(myRotationSensor);
GLRenderer内ではMainActivityからもらったmyRotationSensorを保持しているので,
  c2Matrix = myRotationSensor.getRotMatrix();
で変換行列を取得できます。


次の実行の様子は,タブレットを東にかざしたところ(東側の写真を写すように構えたところ)です。
アプリがportrait仕様なだけで,タブレットを縦位置(portrait)でも横位置(landscape)でも正しく見えます。

 
図 A10.2 画面表示
タブレットを東側に向け,縦位置・横位置で見ているところです。Eastと書かれた緑色の立方体が見えています。
座標軸では,東はx軸+方向で一貫していることがわかると思います。


★RotationSensorに関するメモ

portraitのアプリでRotationSensorを使う場合は,座標変換せずに使うようになります。

    //現在のセンサの値から生の変換行列 rMat を得る
    SensorManager.getRotationMatrixFromVector(rMat, event.values);
   
    //(生の変換行列)rMat から
座標変換して (サンプルプログラムで用いる変換行列)rotmatrix を生成する
    //第2引数は機器のx軸(
図 A10.1 参照)は,サンプルプログラムではどの軸にするのかを指示する
    //
第3引数は機器のy軸(図 A10.1 参照)は,サンプルプログラムではどの軸にするのかを指示する
    SensorManager.remapCoordinateSystem(rMat, SensorManager.AXIS_X, SensorManager.AXIS_Y, rotmatrix );
    //機器のx軸(図 A10.1 参照)は,サンプルプログラムではx軸
    //機器のy軸(図 A10.1 参照)は,サンプルプログラムではy軸
    //結局この場合は何も変わっていないので,この作業は不要で,rMAtをそのままrotMatrixにしてよかった


landscapeのアプリで
RotationSensorを使う場合は,
SensorManager.remapCoordinateSystemで
座標変換してから使うようになります。

RotationSensor.javaでコメントアウトされていますが,この用途のときは差し替えます。
 
SensorManager.remapCoordinateSystem(rMat, SensorManager.AXIS_Y, SensorManager.AXIS_MINUM_X, rotmatrix );
この関数で座標変換していますが,その意味は,
    機器のx軸(図 A10.1 参照)は,サンプルプログラムではy軸(第2引数)
    機器のy軸(図 A10.1 参照)は,サンプルプログラムではx軸マイナス方向(第3引数)



           図 A10.2 portraitからlandscapeへの座標変換

また,portrait,landscape両用のアプリの場合に関しても,
画面面内回転90度ごとに異なる変換行列を生成する方法もコメントアウトされている部分を差し替えると使えます。


★カメラの座標を変更したときに関するメモ

GLRenderer.jave内のカメラビュー行列を変換するところで,コメントアウトされていた
  
Matrix.translateM(c2Matrix,0,-6f,-6f,-6f);
を有効にすると,カメラ位置が(6,6,6)になります。北東方向で斜め上方に上がったところから俯瞰するため,
タブレットを南西方向の斜め下方に向けると原点が見えます。また原点の周りの
北東南西,天頂・地底においた6個の立方体も見えます。
原点から見て東側に見えていた緑色の立方体が見えています。


もし
       
Matrix.translateM(c2Matrix,0,-6f,-6f,-6f);

        Matrix.rotateM(c2Matrix,0,90f,0f,1f,0f);
        Matrix.translateM(c2Matrix,0,-6f,-6f,-6f);
の2行に変更したら,原点は南西の斜め上方向に見えるはずです。



すべてのandroidデバイスがRotation Vectorセンサを使えるわけではありません。
Rotation Vectorセンサのハードウェアは3軸加速度センサ,3軸ジャイロセンサ,3軸磁気センサが使われているようです。
デバイスの中には3軸ジャイロセンサを持たないため,
3軸加速度センサ,3軸磁気センサのみで姿勢を作らなければならない場合が出てきます。このような場合でも使える方法があります。
次のファイルをダウンロードして,作業に使います。
gles2sample09R2.zip 




                                                                   サンプル一覧表


サンプル
プログラム
解説場所 主な特徴 シェーダ テクスチャ ジェスチャ
GLES2Sample01 2.1 座標軸の表示 シングル
一本指
GLES2Sample02 3.1 座標軸+自転立方体の表示 シングル
一本指
GLES2Sample03 6.1 座標軸+公転立方体の表示 シングル
一本指
GLES2Sample04 7.1 立方体,球の自転公転 シングル
一本指
GLES2Sample05 7.3 立方体,球の自転公転 シングル
一本指
GLES2Sample06 オブジェクトの中心を線で結ぶ シングル
一本指
GLES2Sample07 10 視点移動 シングル
一本指
GLES2Sample08 11 テクスチャの組み込み(第1歩) シングル △説明用
一本指
GLES2Sample09 12 テクスチャ付きの球,立方体 シングル 一本指
GLES2Sample10 13 文字の最前面表示 シングル 一本指
GLES2Sample11 15 テクスチャ付きの球,立方体 マルチ 一本指
GLES2Sample12 16 フォンシェーディング マルチ 一本指
GLES2Sample01A 付録1.1 座標軸の表示,layout シングル
一本指
GLES2Sample01B 付録2.1 座標軸の表示 シングル
2本指・3本指
GLES2Sample02E 付録1.2 立方体,layout,menu シングル
一本指
GLES2Sample02E2 付録1.3 立方体,layout,ボタン シングル
一本指
GLES2Sample02E3 付録1.4 立方体,layout,イメージボタン シングル
一本指
GLES2Sample04F 付録4 立方体,球の自転公転 Zバッファ値対数化 シングル
一本指
GLES2Sample09B 付録2.2 テクスチャ付きの球,立方体 シングル 2本指・3本指
GLES2Sample09R 付録10 タブレットの姿勢取得 (VRへの応用) シングル
GLES2Sample10C 付録3 オブジェクトタッチ指示,最前面ボタン シングル 一本指
GLES2Sample10D 14 オブジェクトの最前面表示 シングル 一本指
GLES2Sample11B 付録2.2 テクスチャ付きの球,立方体 マルチ 2本指・3本指
GLES2Sample11G 付録5 テクスチャ付きの球,頂点色指定 マルチ 2本指・3本指
GLES2Sample11H 付録6 光芒blur,平面リング マルチ 2本指・3本指
GLES2Sample13 付録7 立方体,layout,ボタン,頂点色指定,
光芒blur,TextView, Zバッファ値対数化
マルチ
2本指・3本指
GLES2Sample14 付録8 layout,ボタン,球(拡張),円錐(拡張),
トーラス(拡張),ドロイド君,
Zバッファ値対数化
マルチ
2本指・3本指
GLES2Sample14P 付録8 layout,ボタン,円(拡張),球(拡張),
円錐(拡張),トーラス(拡張),
Zバッファ値対数化
マルチ
2本指・3本指
GLES2Sample15 付録9 layout,ボタン,遮蔽球による影
(日食・月食想定),球,Zバッファ値対数化
マルチ
2本指・3本指