第1回 「カメラ画面の表示」
AR (Augmented Reality = 拡張現実感) が現在はやりだといわれています。特に、市場を席巻しつつあるスマートフォン上で動くARが大人気のようです。そこで、この講座では、Android携帯で動くARゲームの作成を通じて、Androidアプリの開発技法やARアプリを支える技術についていろいろお勉強していきたいと考えています。
講座の前半は、ARだけに限らずAndroidアプリの開発に用いるいくつかの技法を紹介しますので、さまざまな目的を持つ初心者の皆さんの役に立つのではないかと思います。また、後半では画像認識を含むAR技術の本質をかじる程度のところまで行く予定なので、ARアプリを手軽に自分でも作ってみようという方の参考になると思います。
題材として取り上げるのはシューティングゲームです。いやな上司とか、嫌いな友人の顔をカメラ画面上に捉えて発射ボタンを押すと、ミサイルが飛んでいって目標を粉々に(?)するというものです。相手が逃げても、いったんロックオンしたミサイルは目標を追従するので、決して逃すことはありません。
この講座では、第1回から簡単なプログラムを紹介し、回を重ねるごとにさまざまな要素をそこに追加していきます。1回ごとの追加、変更はわずかですが、最後の講義まで読めば、ちょっとしたアプリが完成しているというものです。
予定している内容は次のとおりです:
- 第1回: カメラ画面の表示
- 画面上にカメラのプレビュー画像を表示し続けるだけのアプリで、ARの基本中の基本です。ここではカメラのプレビュークラスの構造について紹介します。
- 第2回: AR画面の表示
- カメラ画面の上のレイヤにAR画面を作成し、カメラ画面上にいろいろな画像を重ね合わせます。講座の前半部で紹介する技術のほとんどは、カメラ画面がなければ単なる画像表示でしかありません。世の中の多くの「AR」は所詮そんなものです。
- 第3回: ジェスチャの認識
- 画面上のタップ、ダブルタップ、長押し、フリックなどのジェスチャを認識し、それぞれに対応する処理を行うというプログラムで、シューティングゲームの基本操作となっています。
- 第4回: アニメーションの表示
- 目標に向かってミサイルが飛んでいく単純なアニメーションを表示するプログラムです。タイマーを使ってアニメーションの各フレームを表示するところがポイントです。
- 第5回: 複雑なアニメーションの表示
- 目標にミサイルが当たった後の爆発や残像の表示など、少々凝ったアニメーションを表示するプログラムです。
- 第6回: 複数の目標の扱い
- 同時に複数個の目標を設定し、それぞれをミサイルで撃破するプログラムです。骨組みは前回とまったく同じで、単にハッシュマップを用いるだけで複数個の扱いを可能にします。
- 第7回: ズーム機能の導入
- このアプリを戸外に持ち出し、遠くの建物や自動車を狙おうとすると、目標が小さくて苦労します。それを解消するために、ここでズーム機能を導入します。ただ、カメラのズーム機能が便利に使えるのはOS 2.2以上であったり、モデルごとに機能が異なったりしていて、われわれのアプリのためには不都合が多いので、ここでは完全に手作りでズーム機能を実現します。
- 第8回: ネイティブコードの利用
- 前回の手作りズームは、実際に動かしてみると遅くてぎごちないことがわかります。これはJavaだけを使って画像データをのどかに処理しているからです。そこでアプリの高速化のために、C、C++を使ったネイティブコードを導入します。構文はよく似ていますから、Javaコードの一部を切り出して別ファイルにするだけで、驚くほど簡単にアプリの高速化が実現します。
- 第9回~: いよいよ本題へ
- 前回までの8回分の講義の内容は、カメラ画面を消して考えると、単なる画像やアニメーションの表示だけでした。それでも、いかにも「AR」っぽく見えるから面白いものですが、ここからは、いよいよ目標のロックオン、追従の処理に入ります。ようやく本当のARに足を踏み入れるというわけです。内容全体の各講義への振り分けは現在検討中ですが、講義が8回に近づくころには決まっていると思います。
では、今回のタイトル、「カメラ画面の表示」に入りましょう。
メインのアクティビティは単純で、次のとおりです:
AimnShoot.java
package com.artiscc.aimnshoot;
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
public class AimnShoot extends Activity
{
private static final String TAG = "AimnShootMain";
private CamView mCamView = null;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
mCamView = new CamView(this);
setContentView(mCamView);
}
}
非常に簡単ですね。ここで、"TAG" という定数はログ出力時に用いるもので、今回のコードには入っていませんが、デバッグ時に次のような感じで用います:
Log.e(TAG, String.format("Foo: pos=(%d,%d)", x, y));
次に、AndroidManifest.xml は次のようになります:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.artiscc.aimnshoot"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="4" />
<uses-permission android:name="android.permission.CAMERA"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name="AimnShoot"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ここでは、パーミッションとしてカメラの利用と外部ストレージ(SDカード)への出力を設定しておきます。後者は、デバッグ用に画像の数値データを保存するような場合に用いるためのもので、アプリ公開時には外してもかまいません。なお、SDKのバージョンは4以上とし、OS 1.6から動作するようにしておきます。
アプリ名は res > values > strings.xml にあり、「Aim'n Shoot (= Aim and Shoot)」 としておきます。アクティビティ名やパッケージ名では、簡単に「AimnShoot」とします。
strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello World, AimnShoot!</string>
<string name="app_name">Aim\'n Shoot</string>
</resources>
この AndroidManifest.xml と strings.xml は、この先、何も変更する必要はないので、後に続く講義でも今回のコードをそのまま使えば問題ありません。
では最後にCamViewクラスの中身です:
CamView.java
package com.artiscc.aimnshoot;
import android.content.Context;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class CamView extends SurfaceView implements SurfaceHolder.Callback
{
CamView(Context context)
{
super(context);
mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
Log.e(TAG, "CamView created");
}
public void surfaceCreated(SurfaceHolder holder)
{
try{
mCamera = Camera.open();
surfaceDestroyed = false;
Log.e(TAG, "Camera surface created");
}catch (Exception e){
}
try{
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
checkPreviewCallback();
}catch(Throwable ex){
surfaceDestroyed = true;
Log.e(TAG, "Camera surface destroyed");
if(mCamera != null){
mCamera.release();
}
mCamera = null;
}
}
public void surfaceDestroyed(SurfaceHolder holder)
{
surfaceDestroyed = true;
Log.e(TAG, "Camera surface destroyed");
if(mCamera != null){
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
}
mCamera = null;
}
public boolean isSurfaceDestroyed()
{
return surfaceDestroyed;
}
private void setRotation(Camera.Parameters params, int rotationValue)
{
try{
Method rotateSet = Camera.class.getMethod("setDisplayOrientation", new Class[] {Integer.TYPE} );
Object arguments[] = new Object[] { new Integer(rotationValue) };
rotateSet.invoke(mCamera, arguments);
}catch(NoSuchMethodException nsme){
}catch(IllegalArgumentException e){
}catch(IllegalAccessException e){
}catch(InvocationTargetException e){
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
if(mCamera == null){
return;
}
mCamera.stopPreview();
Camera.Parameters params = mCamera.getParameters();
if (getResources().getConfiguration().orientation != Configuration.ORIENTATION_PORTRAIT){
params.set("orientation", "landscape");
screenRotation = 0;
setRotation(params, screenRotation);
}else{
params.set("orientation", "portrait");
screenRotation = 90;
setRotation(params, screenRotation);
}
mCamera.setParameters(params);
try{
mCamera.startPreview();
}catch(RuntimeException re){
}
}
public void setPreviewCallback(PreviewCallback cb)
{
Log.e(TAG, "Camera: setPreviewCallback");
this.previewCallback = cb;
checkPreviewCallback();
}
private void checkPreviewCallback()
{
if((mCamera == null) || (previewCallback == null) || surfaceDestroyed){
return;
}
mCamera.setPreviewCallback(previewCallback);
}
private static final String TAG = "CamView";
private SurfaceHolder mHolder;
private Camera mCamera;
private boolean surfaceDestroyed = true;
private int screenRotation = 0;
private PreviewCallback previewCallback = null;
}
このコードは、カメラを利用する場合にはお決まりになっているので、基本的には何も手を加えずにそのまま使えばいいのですが、とりあえず2、3のポイントを簡単に解説しておきます。
surfaceCreated()、surfaceDestroyed()、surfaceChanged() は、カメラの起動時、終了時、縦横切り替え時に呼ばれます。
setPreviewCallback()、checkPreviewCallback() は、第7回以降のズーム機能や目標の認識までは使いませんが、一応入れておきます。受け手側のコールバック関数を自分で定義し、それを setPreviewCallback() に設定しておくと、画像を連写して次々とコールバック関数に送ってくれます。受け手側では、自分の処理のタイミングに従って、任意にそれを利用したり捨てたりすることができます。
なお、Camera.PreviewCallback には、もうひとつ setOneShotPreviewCallback() という関数があります。機能はよく似ていますが、こちらは連写するのではなく、画像を1枚コールバック関数に送ってくれたら、あとは次のリクエストまで休みます。したがって、受け手側の処理のタイミングで、カメラ画像が欲しいときにリクエストを発行すればいいわけです。
われわれのアプリの場合は、どちらを使ってもほぼ同様ですが、ズーム機能(第7~8回予定)を作るにあたって、カメラから休みなく画像を送ってもらえるように連写の方を利用することにしました。
以上で、カメラ画面の表示は終わりです。次回は、この上にAR画面を重ねてみます。
第1部 ARアプリ作成の基本
第1回: カメラ画面の表示画面上にカメラのプレビュー画像を表示し続けるだけのアプリで、ARの基本中の基本です。
第2回: AR画面の表示
カメラ画面の上にAR画面を載せ、カメラ画面上にいろいろな画像を重ね合わせます。
第3回: ジェスチャの認識
タップ、ダブルタップ、長押し、フリックなどの画面操作を認識し、対応する処理を行います。
第4回: アニメーションの表示
タイマーを用いてミサイルが目標に向かって飛んでいく単純なアニメーションを表示します。
第5回: 複雑なアニメーションの表示
ミサイルが当たった後の爆発や残像の表示など、複雑なアニメーションを表示します。
第6回: 複数の目標の扱い
同時に複数個の目標を設定し、それぞれをミサイルで撃破する枠組みを導入します。
第7回: ズーム機能の導入
ズームボタンを設置し、カメラ画面をズームして表示する機構を手作りで実装します。
第8回: ネイティブコードの利用
ズーム機構を高速化するため、C、C++ を使ったネイティブコードを導入します。