Matrixで頂点の座標変換を行う

OpenGL ESによる描画は、座標を設定して図形(ポリゴン)を描く形で行います。その座標は3次元で指定するので、2次元のAndroid端末の画面に表示するには、何らかの「計算」で指定された3次元座標をAndroid端末の画面上の座標に変換しなくてはいけません。

この座標変換では、3次元空間上に設定した特定の視点(カメラ位置)からポリゴンを見た場合の「見せ方」を設定することになります。

まず、3次元空間(ワールド座標系)を視点を原点に視点からの距離と方向を基準とするビュー座標系に変換し、続いてビュー座標系のポリゴンをOpenGL ES 2.0の2次元論理座標(-1.0から1.0とかいうあれですね)に透視投影(変換)する。ここまでやれば、後はシステムの方でAndroid端末の画面上の座標に出力してくれます。

OpenGL ES1.0(とAndroidのGLUライブラリ)では、こうした座標変換の設定をgluLookAt()やgluPerspective()でやっていたわけですね。

しかし、Open GL ES 2.0では、Vertexシェーダーのコードとして座標変換の式を記述する必要があるので、座標変換用の行列を作成し、データとして渡さなくてはいけません。頂点の座標変換(ジオメトリ変換)をGLSL ESのコードで記述しVertexシェーダーに設定する必要があるわけです。

今回は、3次元空間に立方体を配置し描画することで、OpenGL ES 2.0における座標変換の基本的な流れを確かめてみましょう。

幸い、AndroidにはMatrixという変換行列を作成するための補助クラスが用意されます。このMatrixを使えば、OpenGL ES 1.0に近い感覚で座標変換の指定を行うことができます。

さらに、「行列の掛け算」を行うメソッドもあるので、ビュー、プロジェクション(透視投影変換)行列を掛け合わせれば、4*4の行列を16要素のfloat型配列に格納する形で最終的な「頂点の座標変換行列」を得ることができます。

VertexシェーダーのGLSE ELでも、*演算子を使って「ベクトルと行列の掛け算」を行うことができます。vec4(同次座標)で頂点データを、mat4(4*4行列)で座標変換行列を渡してやれば、それを掛け合わせるだけでVertexシェーダーのコードは出来上がりですね。

MatrixクラスにはsetLookAtM()というgluLookAt()と同じように使える関数が用意されているので、視点の設定はこれで行います。ただ、透視投影変換行列の作成にはgluPerspective()相当の関数が(旧バージョンのAndroid API向けには)ありません。こちらは、投影スクリーンの上下左右の端の座標と、視界範囲の遠近点を指定する形のfrustumM()メソッドを使います。

Matrix.frustumM(matrix, offset, x1, x2, y1, y2, near, far);

とすると、(x1, y1)と(x2, y2)を対角線とする長方形がスクリーン上の可視範囲になり、nearで指定された距離に投影スクリーンとして配置される変換行列がmatrixに格納されます。そのスクリーンには、カメラからnear以上far以内の距離にあるものが投影されるわけです。

nearは投影スクリーンまでの距離なので、nearの値が大きくなるほど視野が狭まる(同じ位置にあるものでも大きく見える)ことになります。たとえば、x1/y1に-1、x2/y2に1を指定すると、縦横それぞれ2の大きさを持つスクリーンができます。スクリーンができたらnearに1を設定して1の距離に配置、さらに視点を(0, 0, -1.01)に置き原点方向を見つめてみましょう。この状態で

(-0.5, 0, 0)-(0.5, 0, 0)の直線

を引くと、長さ1の直線が画面の横幅半分程度の長さを持つ直線として描かれるわけです。この状態でnearを0.5にすれば、1/4程度の長さになります。スクリーンの大きさとnearで画角(視野角)を調整できるわけですね。

視野の大きさを一つの抽象的な数値(角度)として設定するgluPerspective()に比べるとやや使いにくい面がありますが、frustumM()の指定方式の方が描画される具体的な大きさをイメージしやすいとも言えるかもしれません。APIレベル14(Android 4.0)以降になると、MatrixクラスでもgluPerspective()と同様の方式で使える関数が追加されているようです。

実際に座標変換の行列を作る際は、4*4の行列を格納する要素数16のfloat型配列を用意し、それをMatrixクラスの関数に渡します。

透視変換の設定は、Android端末の画面サイズにあわせて行いたいでしょうから、画面サイズが渡されるGLSurfaceView.RendererのonSurfaceChange()で行うのが楽でしょうか。今回は、視点の移動も行わないので、ここでまとめて座標変換の行列を作っておくことにしました。

// 行列格納用配列
private float[] mVPMatrix = new float[16];
private float[] mPMatrix = new float[16];
private float[] mVMatrix = new float[16];

・・・

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {

    GLES20.glViewport(0, 0, width, height);

    // 画面のアスペクト比を算出
    float aspect = (float) width / height;

    // ビュー変換行列を作成
    Matrix.setLookAtM(mVMatrix, 0, 1.0f, 1.0f, -2.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

    // 透視投影変換行列を作成
    Matrix.frustumM(mPMatrix, 0, -aspect, aspect, -1.0f, 1.0f, 1.0f, 100f);

    // 透視投影変換・ビュー変換を乗じて頂点座標の変換行列を作成
    Matrix.multiplyMM(mVPMatrix, 0, mPMatrix, 0, mVMatrix, 0);

}

これで、mVPMatrixに行列が作成できました。

Vertexシェーダー側では、行列を受け取るmat4型Uniform変数uVPMatrixを用意し、頂点のvec4ベクトルと掛け合わせましょう。

uniform mat4 uVPMatrix;
attribute vec4 vPosition;

void main() {
	gl_Position = uVPMatrix * vPosition;
}

float型配列に格納したmat4行列をUniform変数に渡すには、glUniformMatrix4fv()を使います。今回は、立方体の各面で色を変えるために各面の色もglUniform4f()を通してFragmentシェーダーのvec4型Uniform変数に渡してみました。

ActivityのJavaソース。

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

public class MainActivity extends Activity {

	private GLSurfaceView mGLView;

	@Override
	public void onCreate(Bundle savedInstanceState) {

		mGLView = new GLSurfaceView(this);

		super.onCreate(savedInstanceState);

		mGLView = new GLSurfaceView(this);

		// OpenGL ES 2.0を使用
		mGLView.setEGLContextClientVersion(2);

		// Rendererを設定
		mGLView.setRenderer(new TestRenderer());

		setContentView(mGLView);

	}

	@Override
	public void onPause() {

		mGLView.onPause();

		super.onPause();

	}

	@Override
	public void onResume() {

		super.onResume();

		mGLView.onResume();

	}

}

RendererのJavaソース。

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

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

import android.opengl.GLES20;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.Matrix;

public class TestRenderer implements Renderer {

	// Vertexシェーダーコード
	private String vertexShaderCode = 
	        "uniform mat4 uVPMatrix; attribute vec4 vPosition; void main() { gl_Position = uVPMatrix * vPosition; }";

	// Fragmentシェーダーコード
	private String fragmentShaderCode = 
	        "precision mediump float; uniform vec4 uRGBA; void main() { gl_FragColor = uRGBA; }";

	private int mProgram;

	// 行列格納用配列
	private float[] mVPMatrix = new float[16];
	private float[] mPMatrix = new float[16];
	private float[] mVMatrix = new float[16];

	// 頂点データ格納用バッファ
	private FloatBuffer mVertexBuffer;

	// シェーダー変数アクセス情報
	int mVPositionPos;
	int mVPMatrixPos;
	int mRGBAPos;
	
	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {

		int vshader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
		int fshader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);

		// Vertexシェーダーのコードをコンパイル
		GLES20.glShaderSource(vshader, vertexShaderCode);
		GLES20.glCompileShader(vshader);

		// Fragmentシェーダーのコードをコンパイル
		GLES20.glShaderSource(fshader, fragmentShaderCode);
		GLES20.glCompileShader(fshader);

		// Programを作成
		mProgram = GLES20.glCreateProgram(); 

		// Programのシェーダーを設定
		GLES20.glAttachShader(mProgram, vshader);
		GLES20.glAttachShader(mProgram, fshader);

		GLES20.glLinkProgram(mProgram);
		
		GLES20.glClearColor(0.0f, 0.0f, 0.5f, 1.0f);

		// 頂点データ作成
		float[] vertexList = {
				-0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, // X-
				0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, // X+
				-0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, // Z+
				-0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, // Z-
				-0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, // Y+
				-0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f // Y-
				};

		// 頂点データをバッファに格納
		ByteBuffer bb = ByteBuffer.allocateDirect(vertexList.length * 4);
		bb.order(ByteOrder.nativeOrder());
		mVertexBuffer = bb.asFloatBuffer();
		mVertexBuffer.put(vertexList);
		mVertexBuffer.position(0);

		GLES20.glUseProgram(mProgram);

		// シェーダー変数へのアクセス情報を保存
		mVPMatrixPos = GLES20.glGetUniformLocation(mProgram, "uVPMatrix");
		mVPositionPos = GLES20.glGetAttribLocation(mProgram, "vPosition");
		mRGBAPos = GLES20.glGetUniformLocation(mProgram, "uRGBA");

	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {

		GLES20.glViewport(0, 0, width, height);

		// 画面のアスペクト比を算出
		float aspect = (float) width / height;

		// ビュー変換行列を作成
		Matrix.setLookAtM(mVMatrix, 0, 1.0f, 1.0f, -2.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

		// 透視投影変換行列を作成
		Matrix.frustumM(mPMatrix, 0, -aspect, aspect, -1.0f, 1.0f, 1.0f, 100f);

		// 透視投影変換・ビュー変換を乗じて頂点座標の変換行列を作成
		Matrix.multiplyMM(mVPMatrix, 0, mPMatrix, 0, mVMatrix, 0);

	}

	@Override
	public void onDrawFrame(GL10 gl) {

		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

		GLES20.glEnable(GLES20.GL_DEPTH_TEST);

		// Uniform変数uVPMatrixに座標変換行列を渡す
		GLES20.glUniformMatrix4fv(mVPMatrixPos, 1, false, mVPMatrix, 0);

		GLES20.glEnableVertexAttribArray(mVPositionPos);

		// 頂点データのバッファをAttribute変数vPositionに設定
		GLES20.glVertexAttribPointer(mVPositionPos, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer);

		// Uniform変数uRGBAに描画色を設定して面を描く
		GLES20.glUniform4f(mRGBAPos, 1.0f, 0.0f, 0.0f, 1.0f);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

		GLES20.glUniform4f(mRGBAPos, 0.0f, 1.0f, 0.0f, 1.0f);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 4, 4);

		GLES20.glUniform4f(mRGBAPos, 0.0f, 0.0f, 1.0f, 1.0f);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 8, 4);

		GLES20.glUniform4f(mRGBAPos, 1.0f, 1.0f, 0.0f, 1.0f);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 12, 4);

		GLES20.glUniform4f(mRGBAPos, 0.0f, 1.0f, 1.0f, 1.0f);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 16, 4);

		GLES20.glUniform4f(mRGBAPos, 1.0f, 0.0f, 1.0f, 1.0f);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 20, 4);

	}

}

実行すると、斜め上から見下ろす形で各面の色を変えた立方体を描画します。


創作プログラミングの街