【Android】OpenGL ES入門。スクリーン座標をワールド座標に変換してタッチ位置にキューブ弾3

touch3ShootTop2

はじめに


今回もスクリーン座標系からワールド座標系への変換についてです。

過去記事とプロジェクトは以下URLを参照してください。

【Android】OpenGL ES入門。スクリーン座標をワールド座標に変換してタッチ位置にキューブ弾

【Android】OpenGL ES入門。スクリーン座標をワールド座標に変換してタッチ位置にキューブ弾2

gitPurobana

PhysXワールドプロジェクト / GitHub

全体の流れ


前回でスクリーンのXマックス値からYマックス値までの奥に伸びるベクトルを作れば、カメラ位置からタッチ座標に対応した方向にキューブ弾を飛ばすことができることがわかりました。

「ではそのベクトルを作るにはどうすればいいのか?」

これが最大の問題です。

今回はそこにたどり着くまでの手順を実際のコードと共にまとめてしまいましょう。

public Vector3f getRayTo(int x, int y){
	float top = 1f;
	float bottom = -1f;
	float nearPlane = 1f;
	float tanFov = (top - bottom) * 0.5f / nearPlane;
	float fov = 2f * (float) Math.atan(tanFov);

	Vector3f rayFrom = new Vector3f(eyeX, eyeY, eyeZ);
	Vector3f rayForward = new Vector3f();
	rayForward.sub(new Vector3f(lookX, lookY, lookZ), new Vector3f(eyeX, eyeY, eyeZ));
	rayForward.normalize();
	float farPlane = 10000f;
	rayForward.scale(farPlane);

	Vector3f vertical = new Vector3f(upX, upY, upZ);

	Vector3f hor = new Vector3f();
	hor.cross(rayForward, vertical);
	hor.normalize();
	vertical.cross(hor, rayForward);
	vertical.normalize();

	float tanfov = (float) Math.tan(0.5f * fov);

	float aspect = height / width;

	hor.scale(2f * farPlane * tanfov);
	vertical.scale(2f * farPlane * tanfov);

	if (aspect < 1f) {
		hor.scale(1f / aspect);
	}else {
		vertical.scale(aspect);
	}

同じくWorld#getRay()を参照してください。

float top = 1f;
float bottom = -1f;
float nearPlane = 1f;
float tanFov = (top - bottom) * 0.5f / nearPlane;
float fov = 2f * (float) Math.atan(tanFov);

FOVのタンジェントと角度を求めています。

それぞれを視覚的に表すとこうなります。

touch3ShootTan

こうして見るとわかるはずです。

FOVを半分に割ると、直角三角形ができあがるのです。

と、言うことはタンジェントも使えますね。

この直角三角形のタンジェントは1/1になります。

コード4行目のatan()は、アークタンジェントと呼ばれ、FOVの角度そのものを求めることができる関数です。

Vector3f rayFrom = new Vector3f(eyeX, eyeY, eyeZ);
Vector3f rayForward = new Vector3f();
rayForward.sub(new Vector3f(lookX, lookY, lookZ), new Vector3f(eyeX, eyeY, eyeZ));
rayForward.normalize();
float farPlane = 10000f;
rayForward.scale(farPlane);

これは1つ目の記事でやりましたが、カメラの注意点から位置を引き算するとカメラの向くベクトルへの弾道ができあがるというものです。

できあがったベクトルを正規化してスケーリングしています。

さて、この変数farPlaneの10000という数字。

これは、さきほどのnearPlaneの逆で一番遠い場所を指しています。

試しにこの値を10とかにしてキューブ弾を撃ってみると、ほとんど飛びません。

タッチしたワールド座標の遠距離の最大値が10になってしまったため、本当は100の座標をタッチしたのに、強制的に10と認識されてしまうわけです。

touch3ShootFar

Vector3f vertical = new Vector3f(upX, upY, upZ);

Vector3f hor = new Vector3f();
hor.cross(rayForward, vertical);
hor.normalize();
vertical.cross(hor, rayForward);
vertical.normalize();

OpenGL ESのメソッド、gluLookAt()で設定した上の向きであるupX~upZを元に、ワールドの真上を向くベクトルverticalを作成します。

ここでhor(horizon、地平線のこと)にcross()が使われていますね。

前回の最後で、ベクトルの外積を求めるものであると軽く触れました。

外積とはベクトル同士の掛け算を意味していて、答えが直角のベクトルになるのです。

touch3ShootCrossVec

float tanfov = (float) Math.tan(0.5f * fov);

float aspect = height / width;

hor.scale(2f * farPlane * tanfov);
vertical.scale(2f * farPlane * tanfov);

if (aspect < 1f) {
	hor.scale(1f / aspect);
}else {
	vertical.scale(aspect);
}

FOVのfarPlaneである10000の位置の縦横を求め、Androidのアスペクト比によって縦か横を合わせます。

touch3ShootVecAspect

またまた次回に続きます。