[Flutter開発]FlutterとARCoreを組み合わせてAndroidのARアプリを作成してみる

# はじめに
AndroidのスマートフォンでAR（拡張現実）を表示できるアプリを作ってみようと思う。
Androidで動作するARアプリを作る場合はGoogle公式のARCoreを使用するのが有力な候補だと思うが、このARCoreをクロスプラットフォームアプリを作成できるFlutterで使えるようにしたい。

有り難いことに、FlutterでARCoreを使用するための「arcore_flutter_plugin」というパッケージがpubで公開されており、さらにこのパッケージを使用したサンプルも公式のGithubで公開されているので、まずはこのサンプルをAndroid上で動かすことを目標とする。

## iPhoneでARアプリを作成したい場合
[[Flutter開発]FlutterとARKitを組み合わせてARアプリを作成してみる](https://qiita.com/s_harada/items/18bec5db482ab1e2d92c) という記事も作成しているので、こちらを参考にしてほしい。

# 環境情報
## 開発環境
* Windows 10 Home
* Android Studio `3.4.2`
* Flutter `1.7.8+hotfix.4`
* arcore_flutter_plugin 

## 端末環境
* Pixel2(Emulator) `Android 8.1 Oreo`

# 作成手順
## 新規のFlutterプロジェクトを作成
新規のFlutterプロジェクトを作成する。名前は「flutter_arcore_test_app」としてみた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/bed8bf51-f2cd-b8cc-f591-eebd76ce1499.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/dbc404e3-a830-13e1-ddcf-830f15998905.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/1656ba3a-5cd6-8173-9eab-887689624bce.png)

## arcore_flutter_pluginのインストール
Flutterプロジェクトを作成できたら、pubspec.yamlを開いてarcore_flutter_pluginの記述を追加する。

```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  arcore_flutter_plugin: ^0.0.2+1 #この行を追加
```

main.dartを開き、「Get Dependenceis」を押してarcore_flutter_pluginをインストールする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/19c582ee-9963-6b14-9fba-505a3e94c7ce.png)

コード0が表示されればインストールは成功。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/79a2962b-9d07-167b-d4ec-3484ccf4d485.png)

## main.dartの書き換え
main.dartを以下の内容に書き換える。

```main.dart
import 'package:arcore_flutter_plugin/arcore_flutter_plugin.dart';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;

class HelloWorld extends StatefulWidget {
  @override
  _HelloWorldState createState() => _HelloWorldState();
}

class _HelloWorldState extends State<HelloWorld> {
  ArCoreController arCoreController;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Hello World'),
        ),
        body: ArCoreView(
          onArCoreViewCreated: _onArCoreViewCreated,
        ),
      ),
    );
  }

  void _onArCoreViewCreated(ArCoreController controller) {
    arCoreController = controller;

    _addSphere(arCoreController);
    _addCylindre(arCoreController);
    _addCube(arCoreController);
  }

  void _addSphere(ArCoreController controller) {
    final material = ArCoreMaterial(
        color: Color.fromARGB(120, 66, 134, 244), texture: "earth.jpg");
    final sphere = ArCoreSphere(
      materials: [material],
      radius: 0.1,
    );
    final node = ArCoreNode(
      shape: sphere,
      position: vector.Vector3(0, 0, -1.5),
    );
    controller.add(node);
  }

  void _addCylindre(ArCoreController controller) {
    final material = ArCoreMaterial(
      color: Colors.red,
      reflectance: 1.0,
    );
    final cylindre = ArCoreCylinder(
      materials: [material],
      radius: 0.5,
      height: 0.3,
    );
    final node = ArCoreNode(
      shape: cylindre,
      position: vector.Vector3(0.0, -0.5, -2.0),
    );
    controller.add(node);
  }

  void _addCube(ArCoreController controller) {
    final material = ArCoreMaterial(
      color: Color.fromARGB(120, 66, 134, 244),
      metallic: 1.0,
    );
    final cube = ArCoreCube(
      materials: [material],
      size: vector.Vector3(0.5, 0.5, 0.5),
    );
    final node = ArCoreNode(
      shape: cube,
      position: vector.Vector3(-0.5, 0.5, -3.5),
    );
    controller.add(node);
  }

  @override
  void dispose() {
    arCoreController.dispose();
    super.dispose();
  }
}
```

## AndroidManifest.xmlにエントリを追加する

`/android/app/src/main/AndroidManifest.xml`を開き、以下のエントリを追加する。

```AndroidManifest.xml
<!-- "AR Required" apps must declare minSdkVersion ≥ 24 -->
<uses-sdk android:minSdkVersion="24" />

<uses-permission android:name="android.permission.CAMERA" />

<!-- Indicates that app requires ARCore ("AR Required"). Ensures app is only
     visible in the Google Play Store on devices that support ARCore.
-->
<uses-feature android:name="android.hardware.camera.ar" />
```

```AndroidManifest.xml
<application>
    …
    <!-- Indicates that app requires ARCore ("AR Required"). Causes Google
         Play Store to download and install ARCore when the app is installed.
    -->
    <meta-data android:name="com.google.ar.core" android:value="required" />
</application>
```

エントリを追加すると、AndroidManifest.xmlは以下のようになる。

```AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="xxx.xxxxx.flutter_arcore_test_app">

    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
         calls FlutterMain.startInitialization(this); in its onCreate method.
         In most cases you can leave this as-is, but you if you want to provide
         additional functionality it is fine to subclass or reimplement
         FlutterApplication and put your custom class here. -->

    <!-- "AR Required" apps must declare minSdkVersion ≥ 24 -->
    <uses-sdk android:minSdkVersion="24" />

    <uses-permission android:name="android.permission.CAMERA" />

    <!-- Indicates that app requires ARCore ("AR Required"). Ensures app is only
         visible in the Google Play Store on devices that support ARCore.
    -->
    <uses-feature android:name="android.hardware.camera.ar" />

    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="flutter_arcore_test_app"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- This keeps the window background of the activity showing
                 until Flutter renders its first frame. It can be removed if
                 there is no splash screen (such as the default splash screen
                 defined in @style/LaunchTheme). -->
            <meta-data
                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                android:value="true" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!-- Indicates that app requires ARCore ("AR Required"). Causes Google
         Play Store to download and install ARCore when the app is installed.
        -->
        <meta-data android:name="com.google.ar.core" android:value="required" />
    </application>
</manifest>
```

## ビルドの依存関係を確認
### プロジェクトのbuild.gradleを確認
`/android/gradle/build.gradle`を開き、allprojectsにgoogleのレポジトリ指定が含まれていることを確認。

```build.gradle
allprojects {
    repositories {
        google()
        jcenter()
    }
}
```

### アプリのbuild.gradleにARCoreとSceneformの依存関係を追加
`/android/app/build.gradle`に以下の記述を追加する。

```build.gradle
android {
    // compileSdkVersionの指定はもともとあるはず
    compileSdkVersion 28

    // 省略

    // compileOptionsの指定は追加する
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }

    // defaultConfigのminSdkVersion指定を24に書き換える
    defaultConfig {
        // 省略
        minSdkVersion 24
        // 省略
    }
    
    // 省略
}

// もともとあるdependenciesの指定に以下の3行を追加する
dependencies {
    // 省略
    implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.8.0'
    implementation 'com.google.ar.sceneform:core:1.8.0'
    implementation 'com.google.ar:core:1.8.0'
}
```

## AndroidXを有効にする
`/android/gradle.properties`を開き、useAndroidXがtrueであることを確認しておく。

```gradle.properties
org.gradle.jvmargs = -Xmx1536M
android.useAndroidX = true
android.enableJetifier = true
```

# 実行してみる
## ARCoreに対応している実機
ARCoreは、Android 7.0（Nougat）以降であることが動作前提となっているが、これを満たしていてもすべてのAndroid端末でARCoreが動作するわけではないところに注意が必要。

以下にARCoreが動作することを確認できている端末の一覧が公開されているので、持っているAndroid端末がこのリストに載っていれば実機で動作確認ができる。
[ARCore supported devices](https://developers.google.com/ar/discover/supported-devices)

ARCore対応のAndroid端末を持っていなかったり、そもそもiPhoneユーザだという方は、大人しくAndroidエミュレータで動作確認をすることになる。

## エミュレータで実行してみる
### 仮想端末の用意
GoogleのAndroid端末「Pixel2」を選択し、OSは「Android 8.1 Oreo」のシステムイメージを使用した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/58577558-d098-ee60-ede4-f68e11ac17aa.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/a1070734-e3c7-5c83-3d94-9d0a998c8e01.png)


### カメラエミュレータでの実行結果
作成したFlutterプロジェクトをエミュレータで実行するとまずはアプリが起動し、写真の撮影とビデオの保存を許可するかどうか聞いてくるのでALLOW（許可）をタップする。
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/51c19838-3293-c96d-c5f0-8fd54a89863f.png">

アプリに3Dの部屋が映し出され、円柱と立方体のARオブジェクトが表示される。
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329353/fa611aa0-1e6f-3456-8477-73ca67f0e473.png">

この3Dの部屋はエミュレータでカメラ映像を使用するときにデフォルトで表示されるもののようで、Altキーを押すとカメラのアングル変更や移動もできるようになっている。（ただし操作は結構難しい）

|行いたいカメラ操作|キー入力|
|---|---|
|カメラアングルを右に向ける|[Alt]キー + 右方向にスワイプ|
|カメラアングルを左に向ける|[Alt]キー + 左方向にスワイプ|
|カメラアングルを上に向ける|[Alt]キー + 上方向にスワイプ|
|カメラアングルを下に向ける|[Alt]キー + 下方向にスワイプ|
|カメラ位置を前に進める|[Alt]キー + [w]キー|
|カメラ位置を後ろに進める|[Alt]キー + [s]キー|
|カメラ位置を右に進める|[Alt]キー + [d]キー|
|カメラ位置を左に進める|[Alt]キー + [a]キー|
|カメラ位置を上げる|[Alt]キー + [e]キー|
|カメラ位置を下げる|[Alt]キー + [q]キー|

# まとめ
ARCoreの面白いところは、対応端末がAndroid端末だけでなくiOS端末も[含まれている](https://developers.google.com/ar/discover/supported-devices#ios)ところだ。
FlutterにARKitとARCoreを組み合わせれば、Android向けとiOS向けのARアプリを同じコードベースで開発できるのではないかと期待していたが、ARCoreがiOS端末もカバーするのならARKitを使用しなくてもよいのかもしれない。（Apple純正のARKitに対して、ARCoreのiOS対応が見劣りしなければの話ではあるが）

[[Flutter開発]FlutterとARKitを組み合わせてARアプリを作成してみる](https://qiita.com/s_harada/items/18bec5db482ab1e2d92c) という記事でiOS向けのARサンプルアプリを実行するところまでできているので、次のステップとしてはFlutterの強みを生かして同一のコードベースからAndroid・iOSの双方のアプリをビルドできるようにしてみたいと考えている。

# 参考URL
## arcore_flutter_pluginのパッケージ及びソース配布
[pub.dev (arcore_flutter_plugin)](https://pub.dev/packages/arcore_flutter_plugin)
[Github (arcore_flutter_plugin)](https://github.com/giandifra/arcore_flutter_plugin)

## arcore_flutter_pluginの使用方法を紹介した公式記事
[ARCore Flutter Plugin: configurations](https://medium.com/@difrancescogianmarco/arcore-flutter-plugin-configurations-3ee53f2dc749)
[ARCore Flutter Plugin: add object on the plane](https://medium.com/@difrancescogianmarco/arcore-flutter-plugin-add-object-on-the-plane-8b3d7cbde3d3)