Model3Dに、止まっている場合は回転して投げ上げられ、動いている場合は止まる「awakeOrAsleepメソッド」を定義します。また具体的な動作を定義する「rotateBallメソッド」と「translateBallメソッド」も定義します。
drawメソッドに「もし起きている場合は自分自身を回転させ投げ上げるアフィン変換を適用する」処理を追加することで、テニスボールのアニメーションが行われます。
なお「アフィン変換を適用する順序」でも説明したように、アフィン変換を適用する順序には注意する必要があります。本稿の実装では、以下のようにAndARに最初から実装されている「マーカーに対して3Dモデルを回転、移動、拡大・縮小するアフィン変換」の“後”に「3Dモデルをアニメーションさせるアフィン変換」を実装します。
public class Model3D extends ARObject implements Serializable{ // 省略 private static final float ROTATE_SECOND = 5.0f; // ボールが1回転するために要する秒数(s) private static final float G = 1.62f; // 月の重力加速度 (m/s^2) private static final float V0 = 2.0f; // 投げ上げ速度の初期値 (m/s) private static final float REPULSION_FACTOR = 0.8f; // 反発係数 private static final float H_SCALE = 10.0f; // 見やすくするための高さ拡大率 private static final float V_THRESHOLD = 0.01f; // 投げ上げ速度の限界値 private long rotateSTime = 0L; // ボールの回転が始まった時刻 private long translateSTime = 0L; // 鉛直投げ上げが始まった時刻 private float velocity = V0; // 現在の鉛直投げ上げ速度(初期値はV0) private boolean isSleeping = true; // 睡眠フラグ public void awakeOrAsleep() { isSleeping = !isSleeping; // 睡眠フラグの切り替え } // 省略 @Override public void draw(GL10 gl) { super.draw(gl); if (!isSleeping) { // 起きている場合 // 3Dモデルをアニメーションさせるアフィン変換 translateBall(gl); // 2-2.適切な高さへテニスボールが投げ上げられる rotateBall(gl); // 2-1.適切な角度へテニスボールが回転する } else { // 寝ている場合は初期値にする velocity = V0; translateSTime = 0L; rotateSTime = 0L; } //gl = (GL10) GLDebugHelper.wrap(gl, GLDebugHelper.CONFIG_CHECK_GL_ERROR, log); // マーカーに対して3Dモデルを回転、移動、拡大・縮小するアフィン変換(AndARに最初から実装済み) gl.glScalef(model.scale, model.scale, model.scale); // 1-5.指定された大きさへ拡大・縮小する gl.glTranslatef(model.xpos, model.ypos, model.zpos); // 1-4.定義されている座標へ移動する gl.glRotatef(model.xrot, 1, 0, 0); // 1-3.定義されているx軸回転角度に従い、x軸を中心に回転する gl.glRotatef(model.yrot, 0, 1, 0); // 1-2.定義されているy軸回転角度に従い、y軸を中心に回転する gl.glRotatef(model.zrot, 0, 0, 1); // 1-1.定義されているz軸回転角度に従い、z軸を中心に回転する // 省略 } // ボールを回転させるアニメーション処理 private void rotateBall(GL10 gl) { float degree; // ボールを回転させる角度 if (rotateSTime == 0L) { rotateSTime = System.currentTimeMillis(); // 回転処理開始時間を記録 degree = 0.0f; } else { float t = (System.currentTimeMillis() - rotateSTime) / 1000.0f; // 回転処理開始時からの経過時間 degree = 360.0f * ((t / ROTATE_SECOND) % 1.0f); // 1回転するまでに要する時間を元に、今回レンダリングする回転角度を計算 } gl.glRotatef(degree, 0, 0, 1.0f); // z軸を中心に、計算された角度にボールを回転させるアフィン変換を実施 } // ボールを投げ上げる private void translateBall(GL10 gl) { float height; // 投げ上げられたボールの高さ if (velocity < V_THRESHOLD) { // 投げ上げ速度が限界より小さい場合、地面に着いて動かなくなったとみなす translateSTime = 0L; return; } if (translateSTime == 0L) { translateSTime = System.currentTimeMillis(); // 鉛直投げ上げ処理開始時間を記録 height = 0.0f; } else { float t = (System.currentTimeMillis() - translateSTime) / 1000.0f; // 鉛直投げ上げ処理開始時からの経過時間 height = (-0.5f * G * t * t + velocity * t) * model.scale * H_SCALE; // 経過時間から、今回レンダリングする高さを計算 if (height < 0.0f) { // 地面に着いた場合、反発係数を乗じて投げ上げ速度が減衰して跳ね上がる translateSTime = System.currentTimeMillis(); height = 0.0f; velocity = velocity * REPULSION_FACTOR; } } gl.glTranslatef(0, 0, height); // z軸正の方向へ、計算された高さにボールを移動させるアフィン変換を実施 } // 省略 }
まず「awakeOrAsleepメソッド」を解説します。このメソッドでは、実質的な処理は何もしていません。睡眠フラグのON/OFFをトグルさせるだけです。
private boolean isSleeping = true; // 睡眠フラグ public void awakeOrAsleep() { isSleeping = !isSleeping; // 睡眠フラグの切り替え }
drawメソッドでは、睡眠フラグのON/OFFに従って「ボールが回転するアフィン変換」と「ボールが投げ上げられるアフィン変換」を適用するか分岐させています。
if (!isSleeping) { // 起きている場合 // 3Dモデルをアニメーションさせるアフィン変換 translateBall(gl); // 2-2.適切な高さへテニスボールが投げ上げられる rotateBall(gl); // 2-1.適切な角度へテニスボールが回転する } else { // 寝ている場合は初期値にする velocity = V0; translateSTime = 0L; rotateSTime = 0L; }
では、ボールが回転するアフィン変換「rotateBallメソッド」を確認しましょう。実施している内容はシンプルで、一回転するために必要な秒数 ROTATE_SECOND と回転を開始した時刻から、「レンダリングしようとしている時刻「t」で回転しているはずの角度 degree」を計算し、回転アフィン変換を適用します。
ただし、360度を超えて回転させるのは無駄なので、剰余を取って360度以内で回転角度を計算しています。
本稿では、z軸正方向のベクトルを軸に回転させていますが、異なるベクトルを軸に回転させても問題ありません。
private static final float ROTATE_SECOND = 5.0f; // ボールが1回転するために要する秒数(s) private long rotateSTime = 0L; // ボールの回転が始まった時刻 // ボールを回転させるアニメーション処理 private void rotateBall(GL10 gl) { float degree; // ボールを回転させる角度 if (rotateSTime == 0L) { // 回転処理開始時間が初期値の場合 rotateSTime = System.currentTimeMillis(); // 回転処理開始時間を記録 degree = 0.0f; } else { float t = (System.currentTimeMillis() - rotateSTime) / 1000.0f; // 回転処理開始時からの経過時間 degree = 360.0f * ((t / ROTATE_SECOND) % 1.0f); // 1回転するまでに要する時間を元に、今回レンダリングする回転角度を計算 } gl.glRotatef(degree, 0, 0, 1.0f); // z軸を中心に、計算された角度にボールを回転させるアフィン変換を実施 }
次に、ボールが投げ上げられるアフィン変換「translateBall」メソッドを確認します。重力加速度を「G」、鉛直方向への投げ上げ速度成分を「velocity」とすると、レンダリングしようとしている時刻「t」での高さ「height」は、「height = -1 / 2 * G * t ^ 2 + velocity * t」で計算できます。
検証に用いたデバイスの計算性能と描画性能上、あまりに速く移動させると、コマ落ちしたカクカクしたアニメーションに見えてしまいます。そこで本稿では、「月の重力加速度」を用い、月面上でゆっくりとバウンドするように見えるアニメーションを行わせます。
高さが0以下になった時点で「地面に着いた」と判断し、反発係数を乗じて投げ上げ速度が幾分減衰した後に再度鉛直投げ上げ運動が再開します。このようにして高さを計算し、3Dモデルへ移動アフィン変換を適用することで、「テニスボールが徐々に高さを減らしながらバウンドして少し経ったら止まる」アニメーションが実現できます。
なお、投げ上げ速度がある一定値より小さくなった場合「地面に着いて動かなくなった」とみなし、移動アフィン変換を適用せずにメソッドを抜けます。
なお本稿では、シンプルな「鉛直投げ上げ」運動を行わせてるため、x軸やy軸方向には移動しません。x軸・y軸方向にも初期速度成分を与え、「レンダリングしようとしている時刻tでのx軸の位置、y軸の位置」も計算しアフィン変換させることで、「バウンドしながらだんだん近づいてくる/遠ざかっていくテニスボール」を実装しても面白いでしょう。
private static final float G = 1.62f; // 月の重力加速度 (m/s^2) private static final float V0 = 2.0f; // 投げ上げ速度の初期値 (m/s) private static final float REPULSION_FACTOR = 0.8f; // 反発係数 private static final float H_SCALE = 10.0f; // 見やすくするための高さ拡大率 private static final float V_THRESHOLD = 0.01f; // 投げ上げ速度の限界値 private long translateSTime = 0L; // 鉛直投げ上げが始まった時刻 private float velocity = V0; // 現在の鉛直投げ上げ速度(初期値はV0) // ボールを投げ上げる private void translateBall(GL10 gl) { float height; // 投げ上げられたボールの高さ if (velocity < V_THRESHOLD) { // 投げ上げ速度が限界より小さい場合、地面に着いて動かなくなったとみなす translateSTime = 0L; return; } if (translateSTime == 0L) { translateSTime = System.currentTimeMillis(); // 鉛直投げ上げ処理開始時間を記録 height = 0.0f; } else { float t = (System.currentTimeMillis() - translateSTime) / 1000.0f; // 鉛直投げ上げ処理開始時からの経過時間 height = (-0.5f * G * t * t + velocity * t) * model.scale * H_SCALE; // 経過時間から、今回レンダリングする高さを計算 if (height < 0.0f) { // 地面に着いた場合、反発係数を乗じて投げ上げ速度が減衰して跳ね上がる translateSTime = System.currentTimeMillis(); height = 0.0f; velocity = velocity * REPULSION_FACTOR; } } gl.glTranslatef(0, 0, height); // z軸正の方向へ、計算された高さにボールを移動させるアフィン変換を実施 }
ビルドと実機へのデプロイを行ってください。マーカー上に表示されたテニスボールは回転しながらバウンドしましたか?
最後に、バウンドしているテニスボールにフリック操作で干渉し、投げ上げ速度を加速できるようにしましょう。
「GestureDetector」「AndARSimpleOnGestureListener」は、すでに導入されているため、「AndARSimpleOnGestureListener」にフリック操作のイベントハンドラ「onFlingメソッド」を実装するだけで事足ります。
public class CustomActivity extends AndARActivity implements SurfaceHolder.Callback { // 省略 // タップやフリックのListener private class AndARSimpleOnGestureListener extends SimpleOnGestureListener { // シングルタップ @Override public boolean onSingleTapUp(MotionEvent e) { // 省略 } // フリック @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { float deltaX = e2.getX() - e1.getX(); // X方向の移動距離(上から下が正の値) if (deltaX < 0 && Math.abs(velocityX) > Math.abs(velocityY)) { // 下から上にフリックした場合 model3d.accelerate(); // 3Dモデルへ「加速」を通知する } return true; } } // 省略 }
onFlingメソッドでは、フリックを開始したタッチスクリーン上の位置「e1」と、フリックを終了したタッチスクリーン上の位置「e2」、およびタッチスクリーン上での指の移動速度の長辺方向成分「velocityX」、短辺方向成分の「velocityY」が得られます。
本稿では、タッチスクリーンの下から上にフリックした動作を「X方向の移動距離が負の値(Androidのタッチスクリーンの座標系は、左上隅が原点で右下に向かうに従ってx座標もy座標も増加するため)」かつ「長辺方向の速度成分の方が短辺方向の速度成分よりも大きい」という条件で検出しています。
「現実世界のユーザーの下から上へのフリック操作」を「仮想世界の3Dモデル(model3d)」へ通知することで、テニスボールの鉛直方向の速度を増速させます。
本稿のModel3Dではシンプルに、accelerateメソッドが呼ばれた場合は無条件で現在速度を増速するように実装しています。
public class Model3D extends ARObject implements Serializable{ // 省略 private static final float ACCELERATION = 0.5f; // 投げ上げ速度の増速値 (m/s) // 加速を通知された場合、現在の投げ上げ速度を一定値増加させる public void accelerate() { velocity = velocity + ACCELERATION; } // 省略 }
本稿では実装しませんでしたが、タッチスクリーン上のフリック開始位置を「GLU.gluUnProject」関数を用いてワールド座標系の3D座標へ変換し、「カメラ座標と、ワールド座標系内でのフリック開始座標と、3Dオブジェクトが同一直線上にある」ことを判定すれば、「スクリーン上に表示されているテニスボールを触って上に投げ上げた場合に加速させる」という実装ができます。
このような実装により「現実世界のユーザー」と「仮想世界の3Dモデル」がより緊密にコミュニケートできる、より高度な「両想いのAR」が実現できます。
ただし高度になればなるほど、OpenGL ESと3Dプログラミングの世界により深く入り込まなければなりません。OpenGL ESと3Dプログラミングの知識を身に付けた暁には、ぜひ実装にチャレンジしていただければ、と思います。
第2回、第3回、および今回と、Android上でのマーカー型ARの実現方法について解説してきました。次回はiOS上でARを実現する手法について、ライブラリの比較や簡単なサンプルの実装方法について説明します。
Copyright © ITmedia, Inc. All Rights Reserved.