RSS2.0

LWJGL で立方体を回転させてみる

 今日も Java + LWJGL でのゲーム製作について書いてみたいと思います。前回は四角形のポリゴンを表示しましたが、今回は3次元に挑戦ということで、立方体を表示してみたいと思います。ついでにキーボードから立方体を回転できるようにしてみます。
60ede2b8033050477408c23081263079a70d.png
完成図はこちら~。

ソースコード

まずはまるごとソースコードです。
前回と同じ test プロジェクトの中に test.monolith.v1 というパッケージを新しく用意して、その中にクラスを作ってください。今回は全 5 ファイルです。

DrawMonolith.java
package test.monolith.v1;

import static org.lwjgl.opengl.GL11.GL_BACK;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glCullFace;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glOrtho;
import static org.lwjgl.opengl.GL11.glRotatef;
import static org.lwjgl.opengl.GL11.glScalef;
import static org.lwjgl.opengl.GL11.glTranslatef;

import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

public class DrawMonolith {
	private int			width = 300;
	private int			height = 200;
	private int			depth = 200;

	private float			xAngle = 0;
	private float			yAngle = 0;
	private Direction		input = null;

	public void start() {
		try {
			//	ウインドウを生成する
			Display.setDisplayMode(new DisplayMode(width, height));
			Display.setTitle("monolith");
			Display.create();
		} catch(LWJGLException e) {
			e.printStackTrace();
			return;
		}
		
		try {
			//	OpenGL の初期設定

			//	ポリゴンの表示面を表のみに設定する
			glEnable(GL_CULL_FACE);
			glCullFace(GL_BACK);

			//	カメラ用の設定変更を宣言する
			glMatrixMode(GL_PROJECTION);
			//	設定を初期化する
			glLoadIdentity();
			//	視体積(目に見える範囲)を定義する
			glOrtho(0, width, 0, height, 0, depth);

			//	物体モデル用の設定変更を宣言する
			glMatrixMode(GL_MODELVIEW);
			
			while (!Display.isCloseRequested()) {
				//	オフスクリーンを初期化する
				glClear(GL_COLOR_BUFFER_BIT);
				
				//	キー入力を処理する
				update();
				
				//	オフスクリーンに描画する
				render();

				//	オフスクリーンをスクリーンに反映する
				Display.update();
			}
		} catch(Exception e) {
			e.printStackTrace();
		} finally {
			Display.destroy();
		}
	}

	private void update() {
		input = Direction.getPressing();
		
		if (input != null) {
			xAngle = input.rotateXAngle(xAngle);
			yAngle = input.rotateYAngle(yAngle);
		}
	}

	private void render() {
		//	設定を初期化する
		glLoadIdentity();

		//	画面中央の座標を (0, 0, 0) とするよう座標系を移動する
		glTranslatef(width / 2f, height / 2f, depth / -2f);

		//	座標のスケールを指定する
		//	ここで指定した x, y, z 毎の係数が、次に指定する座標にそれぞれ掛けられる
		glScalef(20f, 20f, 20f);

		glRotatef(xAngle, 0, 1, 0);	//	y 軸を中心に xAngle 度回転させる
		glRotatef(yAngle, 1, 0, 0);	//	x 軸を中心に yAngle 度回転させる
		
		glBegin(GL_QUADS);

		for (Face face: Face.values()) {
			face.draw();
		}
		
		glEnd();
	}

	public static void main(String[] args) {
		new DrawMonolith().start();
	}

}

Direction.java
package test.monolith.v1;

import org.lwjgl.input.Keyboard;

public enum Direction {
	UP {
		@Override
		public float rotateYAngle(float angle) {
			angle -= UNIT;
			
			if (angle < 0) {
				angle = 360;
			}
			
			return angle;
		}
	},
	DOWN {
		@Override
		public float rotateYAngle(float angle) {
			angle %= 360;
			angle += UNIT;
			
			return angle;
		}
	},
	RIGHT {
		@Override
		public float rotateXAngle(float angle) {
			angle %= 360;
			angle += UNIT;
			
			return angle;
		}
	},
	LEFT {
		@Override
		public float rotateXAngle(float angle) {
			angle -= UNIT;
			
			if (angle < 0) {
				angle = 360;
			}
			
			return angle;
		}
	};
	
	private static final float	UNIT = 0.1f;
	
	public float rotateXAngle(float xAngle) {
		return xAngle;
	}

	public float rotateYAngle(float yAngle) {
		return yAngle;
	}

	public static Direction getPressing() {
		if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
			return UP;
		} else if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
			return DOWN;
		} else if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) {
			return RIGHT;
		} else if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) {
			return LEFT;
		}
		
		return null;
	}
}

Color.java
package test.monolith.v1;

import static org.lwjgl.opengl.GL11.glColor3f;

public enum Color {
	RED(1f, 0.6f, 0.6f),
	DARK_RED(1f, 0.5f, 0.5f),
	BLUE(0.6f, 0.6f, 1f),
	DARK_BLUE(0.5f, 0.5f, 1f),
	GREEN(0.6f, 1f, 0.6f),
	DARK_GREEN(0.5f, 1f, 0.5f),
	YELLOW(1f, 1f, 0.6f),
	DARK_YELLOW(1f, 1f, 0.5f);

	private final float	r;
	private final float	g;
	private final float	b;

	private Color(float r, float g, float b) {
		this.r = r;
		this.g = g;
		this.b = b;
	}
	
	public void set() {
		glColor3f(r, g, b);
	}
}

Face.java
package test.monolith.v1;

import static test.monolith.v1.Color.*;
import static test.monolith.v1.Point.*;

public enum Face {
	F_FRONT(new Point[] {
			P_FRONT_TOP_RIGHT,
			P_FRONT_TOP_LEFT,
			P_FRONT_BOTTOM_LEFT,
			P_FRONT_BOTTOM_RIGHT
	}, BLUE, RED),
	F_BACK(new Point[] {
			P_BACK_TOP_LEFT,
			P_BACK_TOP_RIGHT,
			P_BACK_BOTTOM_RIGHT,
			P_BACK_BOTTOM_LEFT,
	}, BLUE, RED),
	F_LEFT(new Point[] {
			P_FRONT_TOP_LEFT,
			P_BACK_TOP_LEFT,
			P_BACK_BOTTOM_LEFT,
			P_FRONT_BOTTOM_LEFT
	}, BLUE, GREEN),
	F_RIGHT(new Point[] {
			P_BACK_TOP_RIGHT,
			P_FRONT_TOP_RIGHT,
			P_FRONT_BOTTOM_RIGHT,
			P_BACK_BOTTOM_RIGHT
	}, BLUE, GREEN),
	F_TOP(new Point[] {
			P_BACK_TOP_RIGHT,
			P_BACK_TOP_LEFT,
			P_FRONT_TOP_LEFT,
			P_FRONT_TOP_RIGHT			
	}, DARK_BLUE, DARK_BLUE),
	F_BOTTOM(new Point[] {
			P_FRONT_BOTTOM_RIGHT,
			P_FRONT_BOTTOM_LEFT,
			P_BACK_BOTTOM_LEFT,
			P_BACK_BOTTOM_RIGHT
	}, DARK_RED, DARK_RED);

	private final Point[]  points;
	private final Color	color1;
	private final Color	color2;
	
	private Face(Point[] points, Color color1, Color color2) {
		this.points = points;
		this.color1 = color1;
		this.color2 = color2;
	}

	public void draw() {
		color1.set();
		points[0].point();
		points[1].point();

		color2.set();
		points[2].point();
		points[3].point();
	}
}

Point.java
package test.monolith.v1;

import static org.lwjgl.opengl.GL11.glVertex3f;

public enum Point {
	P_FRONT_TOP_LEFT(-1, 1, 1),
	P_FRONT_TOP_RIGHT(1, 1, 1),
	P_FRONT_BOTTOM_LEFT(-1, -1, 1),
	P_FRONT_BOTTOM_RIGHT(1, -1, 1),
	P_BACK_TOP_LEFT(-1, 1, -1),
	P_BACK_TOP_RIGHT(1, 1, -1),
	P_BACK_BOTTOM_LEFT(-1, -1, -1),
	P_BACK_BOTTOM_RIGHT(1, -1, -1)
	;

	private final float		x;
	private final float		y;
	private final float		z;
	
	private Point(float x, float y, float z) {
		this.x = x * 2;
		this.y = y * 4;
		this.z = z;
	}
	
	public void point() {
		glVertex3f(x, y, z);
	}
}
ソースをコピペしたら実行しましょー。
デバック設定の java.library.path 指定をお忘れなく。

キーボードの矢印キーでくるくる回転します。動き始めるとゲームっぽくなりますね。
ひとしきり遊んだらソースコードを見ていきましょー。

立方体を表示する

立方体の表示といってもそれ自体はたいした事はなく、単純に四角形のポリゴンを 6 つ描けば立方体のできあがりです。ただ立方体の各面となる四角形の向きを意識して座標を指定するだけです。かんたんかんたん。

あ、それと今回からポリゴンやポリゴンを使って作る物体のことをモデルと呼びたいと思います。OpenGL では描く対象物をモデルといいます。今日は立方体のモデルを描くぜー超描くぜー。

各面の座標をゴリゴリ書いていけばいいのですが、わたしは頂点座標の管理を考えて仰々しくも Java の enum を使ってみました。C 言語なんかだと多次元配列で書いちゃうことが多いのですが、Java でゲームを作るからには可読性や再利用性を意識してきちんとクラス分けしたいと思います!フンス

Point クラス

立方体を構成する 8 つの頂点座標を表す enum です。
メンバー変数として x, y, z を持っています。頂点の位置は立方体の中では固定なので、メンバー変数はすべて定数としています。
これらの座標には、コンストラクタの引数として 1 か -1 を渡しているので、このままだとすべての辺の長さが等しい正六面体になります。が、コンストラクタ内でそれに各軸方向毎の係数をかけているので、直方体の頂点座標となります。
こうしたのは各軸方向毎の長さの比率を一律管理したかったからで、書き方自体は好みの問題ですね。

ここで、立方体を回転させようとしているのに頂点は固定なの??と思った方、さすがです(笑
それについては後ほど、座標系の移動の説明時に改めて解説しますねー。

さらに、OpenGL の原点座標は (0, 0, 0) なのに座標に負の値を指定しちゃったら見えないんじゃ??と思った方、すばらしいです(笑
ていうか座標の値が 2 とか 4 とかなんだけど、画面の横幅が 300 なのにこれじゃ小さすぎて見えないんじゃない??と思った方、その通りです(笑
それらについては後ほど、スケールの変更で改めて(以下略
public void point() {
	//	この頂点の座標を指定する
	glVertex3f(x, y, z);
}
Point クラスの point() メソッドを呼び出すと、頂点座標が指定されます。

Face クラス

立方体を構成する 6 つの面を表す enum です。
1つの面は4つの頂点を持つので、Point 型のメンバー変数を4つ持っています。面はポリゴンなので、表裏を間違えないようにしてください。(詳細は前回のポリゴンの表裏を参照)
あと色を表す Color 型のメンバー変数が2つありますが、これは各面をグラデーションで塗りつぶすための色情報です。
public void draw() {
	//	この面の座標を指定する
	color1.set();
	points[0].point();
	points[1].point();

	//	色は辺(座標2つ)毎に変える
	color2.set();
	points[2].point();
	points[3].point();
}
Face クラスの draw() メソッドを呼び出すと、4つの頂点座標が指定されます。

Color クラス

立方体の頂点に割り当てる色情報を表す enum です。
定数として使いまわしたいので enum に切ってみました。
public void set() {
	glColor3f(r, g, b);
}
Color クラスの set() メソッドを呼び出すと、色情報が設定されます。

立方体を描画してみる

実際に立方体を表示しているのは、DrawMonolith.render() 内のこのコードですね。
glBegin(GL_QUADS);

//	立方体のすべての面を描画する
for (Face face: Face.values()) {
	face.draw();
}

glEnd();
GL_QUADS で6つの四角形(Face)を描いて立方体に見せています。

モデルを平行移動する

 さて、ここでモデルの移動について触れたいと思います。ゲームでは登場キャラクターやマップ上のオブジェクトが動き回るので、モデルの移動はゲームプログラミングでは必須となります。
 表示するモデルの位置を移動させようとすると、モデルの各頂点座標は移動先の座標に毎回変更する必要があります。座標 (100, 0, 0) を右の 200 移動させるためには (300, 0, 0) に変更してあげなければなりません。
 しかし移動する度に、モデルのすべての頂点座標を再計算するのは面倒です。その解決法として、OpenGL ではモデルをまるごと平行移動させる関数が用意されています。
 LWJGL では、平行移動は glTranslatef() メソッドで行います。(いつも通り、引数の型による亜種あり)
//	画面中央の座標を (0, 0, 0) とするよう座標系を移動する
glTranslatef(width / 2f, height / 2f, depth / -2f);
//	width = 300, height = 200, depth = 200 なので、
//	実際には glTranslatef(150f, 100f, -100f) となる
 モデルを平行移動する場合、モデルの頂点座標自体は変更する必要がありません。実際の処理としては、glVertex3f() で指定した座標が glTranslatef() で移動した分だけ補正されるためです。
 上のソースコードでは、原点座標を x 軸方向に +150f、y 軸方向に +100f、z 時期方向に -100f 移動させているものとイメージしてください。この移動によって、原点の座標は (0, 0, 0) から視体積の中心点 (150, 100, -100) に移動したとみなせます(今回は立方体を画面の中央に表示したいので、座標系全体を視体積の中心に移動させました)。座標の基準となる原点自体が移動したため、各頂点座標は(値を変えなくても)新しい原点に基づいた座標として認識されるのです。
f1479a440f7710490908358030206f7b5386.png
 平行移動を使うメリットは、一度移動先を指定するだけで、モデル全体の座標をまるごと移動できる点です。
 立方体の頂点座標は、原点からの距離を表すと同時に、潜在的には各頂点間の位置関係をも表しています。ある頂点が他の頂点に対してどのあたりにあるという情報は、原点からの距離と比べると、より複雑なものです。入り組んだモデルほど、頂点はたくさんありますからね。これを頻繁にいじるとバグって頂点座標が狂う(モデルが歪む)可能性が増えるため、あまり触りたくありません。なので頂点座標の基本値を定数として定義しておき、移動時には原点からの距離だけを(glTranslatef() で)変えてやるのです。

モデルのスケールを変更する

 座標系の移動によって、モデルと原点との距離を、モデルの頂点座標から独立して扱えるようになりました。
 同じように、スケール変更を利用することで、モデルの大きさ(倍率)を、モデルの頂点座標から独立して扱えるようになります。

 スケールは、プラモデルの 1/144 スケールとか 1/60 スケールとかいうあれと同じですね。モデルを何倍の大きさにして表示するかを、LWJGL では glScalef() メソッドによって指定することができます。
//	座標のスケールを指定する
//	ここで指定した x, y, z 毎の係数が、次に指定する座標にそれぞれ掛けられる
glScalef(20f, 20f, 20f);
 引数には x 軸方向の倍率、 y 軸方向の倍率、 z 軸方向の倍率を float 型で指定します(この辺はいつ戻り亜種メソッドあり)。座標 (2, 2, 2) を glScalef() メソッドでそれぞれ +20f でスケールすると、座標 (40, 40, 40) になります。

 ただし、モデルのすべての頂点座標が同じ符号を持つ場合、拡大処理なのに、同時に原点から離れて移動したように見えてしまいます。座標(1, 1, 1)を3倍すると (3, 3, 3) になるので、x 座標だけ見ると +2 移動したことになるのです。
 これは、モデルの頂点座標が原点(0, 0, 0)をモデルの中心として設定されていれば発生しません。例えばモデルの中心より左にある頂点は、あらかじめ x 座標の値を負の値に取っておく、というような工夫をしておけばいいのです。モデルの座標がどこであろうと、平行移動は glTranslatef() で簡単にできますからね。
5000f2a900cb10433009b91000489b04c747.png
図にするとこんな感じ(簡略化のため2次元での図となっています)。
イメージしづらければ、実際に Point クラスの座標をすべて正の値に書き直してスケールしてみてください。

モデルの設定を初期化する

 glTranslatef() や glScalef() ではモデルの各座標への補正値を指定できますが、これを描画処理毎に行うと、その補正値がどんどん累積されていってしまします。glTranslatef() で右に 2 移動する場合、2 回目の描画処理では右に 4、3 回目の描画処理では右に 6 移動してしまうことになります。
 LWJGL では glLoadIdentity() を使うことで、モデルの移動やスケールに関するの設定内容をすべてリセットすることができます。
//	設定を初期化する
glLoadIdentity();
サンプルプログラムでは、移動やスケールの変更を伴う描画処理のはじめで、まず glLoadIdentity() を呼び出し、過去の設定内容をリセットするようにしています。
正確には、glLoadIdentity() でリセットされるものには条件があります(次のセクションに続く)。

モデルビュー変換と射影変換

 OpenGL では、座標や色、平行移動など、モデル自体に関するデータを設定することを、モデルビュー変換といいます。
また、視体積など、モデルの見せ方に関するデータを設定することを、射影変換といいます。

 このモデルビュー変換と射影変換に関する情報は、行列スタックと呼ばれる独立した記憶領域にそれぞれ格納されています。なので、モデルビュー変換を行う時にはモデルビュー変換用の行列スタックを、射影変換を行うときには射影変換用の行列スタックを設定するように切り替えてやる必要があります。
 
 サンプルプログラムの OpenGL 初期設定時に glMatrixMode() というメソッドを使っていますが、LWJGL ではこのメソッドによって操作する行列スタックを切り替えることができます。glMatrixMode() メソッドによって、使う行列スタックの種別を宣言しているのです。
//	射影(カメラ用)の設定変更を宣言する
//	この後は射影変換しか行えない
glMatrixMode(GL_PROJECTION);
//	設定を初期化する
glLoadIdentity();
//	視体積(目に見える範囲)を定義する
glOrtho(0, width, 0, height, 0, depth);

//	モデルビュー(モデル用)の設定変更を宣言する
//	この後はモデルビュー変換しか行えない
glMatrixMode(GL_MODELVIEW);
 視体積を設定する glOrtho() は射影変換の時にしか使用できません。同じように、モデルの座標やスケール、平行移動は、モデルビュー変換中にしか行えません。

 前のセクションで取り上げた glLoadIdentity() メソッドは、glMatrixMode() で宣言した行列スタックを初期化します。モデルビュー変換中に呼び出すとモデルビュー変換用の行列スタックを初期化するので、モデルの座標や平行移動等の設定がリセットされます。しかし、射影変換用の設定は別の行列スタックに入っているので、視体積などについては初期化されません。

 以上で立方体を表示に関する説明は終わりです。ふー、長かった。

2012-5-14 追記
 モデルビュー変換、射影変換についてはもう少し詳しい内容を記事にしてみましたので、もう少し知りたいという方はそちらも読んでみてください。

キー入力を取得する

 矢印キーで立方体をくるくると回転させるために、次はキー入力について解説します。
 キーボードによる入力処理は LWJGL の org.lwjgl.input.Keyboard クラスが提供してくれています。
if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
	//	上矢印キーが押されている
	...
} else if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
	//	下矢印キーが押されている
	...
} else if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) {
	//	右矢印キーが押されている
	...
} else if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) {
	//	左矢印キーが押されている
	...
}
 Keyboard.isKeyDown() メソッドを使うことで、現在のキー入力状態を調べることができます。キーボードのキーは一通り Keyboard クラスの static 定数として定義されているので、調べたいキーに対応した定数を引数に渡してあげてください。サンプルプログラムでは、メインループの DrawMonolith.update() 内で入力状態をチェックしています。
 ただし、Keyboard.isKeyDown() メソッドは、呼び出した時点でそのキーが押されているかを返すメソッドなので、注意してください。DrawMonolith.update() の処理が終わってから、次の DrawMonolith.update() が呼び出されるまでにキーが押され、離された場合には、その入力を検知することはできません。LWJGL の他の API を使えばこれについてもきちんと検知することはできますが、それはまたの機会に解説したいと思います。

立方体を回転させる

 最後にモデルの回転をしたいと思います。LWJGL では glRotatef() メソッドを使うことで、各頂点座標を回転させることができます。glTranslatef() や glScalef() と同じようにモデルビューについて設定する API です。
glRotatef(xAngle, 0, 1, 0);	//	原点を通る y 軸を中心に xAngle 度回転させる
glRotatef(yAngle, 1, 0, 0);	//	原点を通る x 軸を中心に yAngle 度回転させる
 第1引数は回転させる角度で、0 ~ 360° を float 型で渡します。第2引数以降は回転の中心軸を指定します。渡す引数についてはソースのコメントにある通りです。y 軸方向に回転させたい場合、回転の中心軸は x 軸となります。
 サンプルプログラムでは入力されたキーの種類によって、x 軸に対する角度と y 軸に対する角度をそれぞれ増減させています。

終わり~

 今回はコードの量が多いので長くなってしまいましたが、これにて立方体モデルの表示はおしまいです。お疲れ様でした。四角形が表示できれば立方体は作れますから、もうちょっと頑張ればダンボーくらいなら表示できる…かな、うん、頑張ればきっと。
 今日はキー入力も取り扱ったので、そろそろ簡単なゲームなら作れそうな気がします。ぜひぜひいろいろいじってみてくださいー。
  LWJGL  コメント (0)  2012/03/04 22:28:35


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2019, 11>>
272829303112
3456789
10111213141516
17181920212223
24252627282930