【Flutter】 arkitを使ってARに3dモデルを表示する方法

この記事は、【 [可茂IT塾 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/kamo-it) 】の16日目の記事です。

# はじめに
arkit_pluginライブラリを使ってARにglTF・glb形式の3dオブジェクトを表示する方法を解説します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/1a0dbc5a-c338-88e0-0532-9028464e8d4a.jpeg" width="50%">

# 開発環境
- Flutter 3.14.0
- Dart  3.2.0
- arkit_plugin 1.0.7
- collection 1.18.0
- vector_math 2.1.4

# 実装手順
### 1. ライブラリのインストール
flutterでARを表示するには、**arkit_plugin** ( https://pub.dev/packages/arkit_plugin ) というパッケージを使用します。
また、本記事ではarkit_pluginに加えてcollection、vector_mathの２つのライブラリも使用します。こちらもインストールしてください。

| 使用ライブラリ一覧 |
| --- |
| arkit_plugin ( https://pub.dev/packages/arkit_plugin ) |
| collection ( https://pub.dev/packages/collection ) | 
| vector_math ( https://pub.dev/packages/vector_math ) | 


### 2. Info.plistの更新
ARKitはデバイスのカメラを使用するので`NSCameraUsageDescription`を指定する必要があります。
以下のコードを `ios/Runner/Info.plist` に追記してください。

```xml
<key>NSCameraUsageDescription</key>
<string>Describe why your app needs AR here.</string>
```
### 3. ライブラリのインポート
使用するライブラリをインポートします
```dart
import 'package:arkit_plugin/arkit_plugin.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;
```

### 4. ARを表示するクラスの実装

```dart
class _MyArAppState extends State<MyArApp> {
  late ARKitController arkitController;

  //初期化
  @override
  void dispose() {
    arkitController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('Christmas Decorations')),
        body: ARKitSceneView(
          showFeaturePoints: true, //特徴点を表示（オブジェクトを置ける場所）
          enableTapRecognizer: true, //タップ認識を有効にする
          planeDetection:
              ARPlaneDetection.horizontalAndVertical, //垂直方向と平面方向の両方で平面を検出
          onARKitViewCreated:
              onARKitViewCreated, //ARKitViewが作成されたときに呼ばれるコールバック関数
        ),
      );

  //ARKitViewが作成されたときの処理
  void onARKitViewCreated(ARKitController arkitController) {
    this.arkitController = arkitController;
    //特徴点を取得
    this.arkitController.onARTap = (ar) {
      final point = ar.firstWhereOrNull(
        (o) => o.type == ARKitHitTestResultType.featurePoint,
      );
      //特徴点が取得できたら
      if (point != null) {
        _onARTapHandler(point);
      }
    };
  }

  void _onARTapHandler(ARKitTestResult point) {
    //タップされた座標を取得
    final position = vector.Vector3(
      point.worldTransform.getColumn(3).x,
      point.worldTransform.getColumn(3).y,
      point.worldTransform.getColumn(3).z,
    );
    final node = _getNodeFromFlutterAsset(position); //ARオブジェクトを作成
    arkitController.add(node); //ARオブジェクトを追加
  }

  ARKitGltfNode _getNodeFromFlutterAsset(vector.Vector3 position) =>
      ARKitGltfNode(
        //ARオブジェクトの設定
        assetType: AssetType.flutterAsset, //アセットタイプ
        url: 'assets/Tree.glb', //① モデルデータ
        scale: vector.Vector3(0.1, 0.1, 0.1), //拡大率
        position: position, //位置
      );
}
```

### 4. モデルデータ（url）の設定方法について


```dart
ARKitGltfNode _getNodeFromFlutterAsset(vector.Vector3 position) =>
      ARKitGltfNode(
        //アセットタイプ
        url: 'assets/Tree.glb', //① モデルデータ
        ---省略---
      );
```
urlの部分で3DモデルのURLを指定します。本投稿時ではglb・glTF形式のみ対応していました(usdzも使えない)。
また、上記のコードのようにあらかじめダウンロードした3Dモデルデータを用いる場合は、pubspec.yamlに以下のように追記する必要があります。
```yaml
assets:
    - assets/Tree.glb
```

下記のように3dモデルのurlを貼り付ける方法でも実装できます。
```dart
ARKitGltfNode _getNodeFromFlutterAsset(vector.Vector3 position) =>
      ARKitGltfNode(
        //アセットタイプ
        url: 'https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Box/glTF-Binary/Box.glb',
        ---省略---
      );
```

### 実行結果
クリスマスが近いこともあって無事、クリスマスツリーを飾ることができました！
※キラキラしているところ（特徴点）にのみオブジェクトを配置できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/b7c65690-06bf-956e-fe48-0c71f126c09d.jpeg" width="50%">

### コード全文
```dart
import 'package:arkit_plugin/arkit_plugin.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Christmas Decorations',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: const MyArApp(),
    );
  }
}

class MyArApp extends StatefulWidget {
  const MyArApp({Key? key}) : super(key: key);
  @override
  State<MyArApp> createState() => _MyArAppState();
}

class _MyArAppState extends State<MyArApp> {
  late ARKitController arkitController;

  //初期化
  @override
  void dispose() {
    arkitController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('Christmas Decorations')),
        body: ARKitSceneView(
          showFeaturePoints: true, //特徴点を表示（オブジェクトを置ける場所）
          enableTapRecognizer: true, //タップ認識を有効にする
          planeDetection:
              ARPlaneDetection.horizontalAndVertical, //垂直方向と平面方向の両方で平面を検出
          onARKitViewCreated:
              onARKitViewCreated, //ARKitViewが作成されたときに呼ばれるコールバック関数
        ),
      );

  //ARKitViewが作成されたときの処理
  void onARKitViewCreated(ARKitController arkitController) {
    this.arkitController = arkitController;
    //特徴点を取得
    this.arkitController.onARTap = (ar) {
      final point = ar.firstWhereOrNull(
        (o) => o.type == ARKitHitTestResultType.featurePoint,
      );
      //特徴点が取得できたら
      if (point != null) {
        _onARTapHandler(point);
      }
    };
  }

  void _onARTapHandler(ARKitTestResult point) {
    //タップされた座標を取得
    final position = vector.Vector3(
      point.worldTransform.getColumn(3).x,
      point.worldTransform.getColumn(3).y,
      point.worldTransform.getColumn(3).z,
    );
    final node = _getNodeFromFlutterAsset(position); //ARオブジェクトを作成
    arkitController.add(node); //ARオブジェクトを追加
  }

  ARKitGltfNode _getNodeFromFlutterAsset(vector.Vector3 position) =>
      ARKitGltfNode(
        //ARオブジェクトの設定
        assetType: AssetType.flutterAsset, //アセットタイプ
        url: 'assets/Tree.glb', //モデルデータ
        scale: vector.Vector3(0.1, 0.1, 0.1), //拡大率
        position: position, //位置
      );
}
```

# 最後に
arkit_pluginを使うと、簡単に3DモデルをARに表示することができました。ARってテンション上がりますよね。arkit_pluginでは3Dモデルの色を変えたり、動かしたり、スクショを撮れるようにしたり、他にもワクワクする機能がたくさんあります。是非色々と試して、自分だけのAR（拡張現実）を作ってみてはいかがでしょうか？

# 参考

https://pub.dev/packages/arkit_plugin

https://github.com/olexale/arkit_flutter_plugin/blob/master/example/lib/load_gltf_or_glb_page.dart

https://sketchfab.com/3d-models/25k-followers-christmas-special-1fd1cdc4c2d94ecfa8d80a25f1818e8c