第1章 ARKit

73回生 object

1.1 自己紹介

みなさんこんにちは、73回生(高1)のobjectです。今年はARKitのことについて部誌を書きました。

1.2 ARKitとは

さて、多くの人はARKitというのが何なのかわからないと思いますが、なんとなくAR関連の何かだということはわかるでしょう。じゃあそのARとは何なのでしょうか。

ARってなに

AR(拡張現実)とは、人が知覚する現実環境をコンピュータにより拡張する技術、およびコンピュータにより拡張された現実環境そのものを指す言葉。(Wikipediaより抜粋)

ARの定義を言葉で言い表すのは難しいですが、わかりやすく言うと現実空間に仮想のものをくっつける技術です。

社会現象にもなったPokemon GO(もう二年近く前)で有名になりましたが、それよりも前からARは存在します。

現実にポケモンがいるように見える

図1.1: 現実にポケモンがいるように見える

VRとよく混同されますが、ARとVRは全く違う技術です。

VR
現実をまるごと作り変える(全部仮想)
AR
現実と仮想を組み合わせる(現実と仮想のハイブリッド)

ARKitってなに

ARが何なのかわかったところで、次はARKitの説明をします。ARKitというのはAppleが発表したiOS用のARフレームワークです。

もっとわかりやすく言うとARKitはARを使ったiOSアプリを簡単に作るためのキットで、いままでとても難しかったiOSでのAR開発が劇的に簡単になるという画期的なものです。

ARKitはA9チップ以降を搭載し、iOS11が入ったiOS端末(つまりiPhoneなら6s以降)さえあれば特別なカメラ等が必要なく、誰でも簡単に体験できるのが特徴です。

1.3 ARKitを使う

ARKitの凄さ

ARKitを使えばiOSでのAR開発が劇的に簡単になるとか言いましたが、実際にどれだけ簡単なのかというと、数行コードを書くだけでただのアプリをARアプリにできるレベルです。

それでは実際に数行コードを書くだけでARアプリを作ってみましょう。

三行ARとHello,World!

ARKitは実質三行でARが実装できます。早速Hello,World!を表示するARアプリを作ってみましょう。

環境構築

まず、iOSアプリを作るにはXcodeというアプリが必要です。そして、XcodeはMacでしか使えません。つまりiOSアプリ開発にはMacが必要です。

Macが準備できたら、次にXcodeをインストールします。Xcodeはここからインストールできます。

また、表示する3Dモデルを作るためにBlenderを使うのでここからインストールしておきましょう。

モデルの作成

インストールしたBlenderを開くと立方体のオブジェクトが出てきますが、使わないのでXキーで削除しておきます。

次に、左側のツールシェルフの作成タブからテキストを選択し、Tabキーで編集モードにした後、予め表示されたテキストをすべて消してHello,World!と打ち込みます。

Hello,World!を打ち込んだ状態

図1.2: Hello,World!を打ち込んだ状態

次に右側にある[ジオメトリ]プロパティの[押し出し]で厚さを調整します。また、[フォント]プロパティではフォントを変更することもできます。

厚さとフォントを変更した状態

図1.3: 厚さとフォントを変更した状態

UV展開ができるように左下のオブジェクト→変換でテキストをメッシュに変換します。

次にテクスチャを貼ります。左上のスクリーンレイアウトをDefaultからUV Editingに変更し、テキストを編集モードで選択した状態でUキーを押してスマートUV展開を選択します。

今回は全部白くていいので白い正方形の画像を用意して左下の[開く]から用意した画像を開きましょう。

UV展開後画像を開いた状態

図1.4: UV展開後画像を開いた状態

できたらスクリーンレイアウトをDefaultに戻し、右のマテリアル(丸のアイコン)からマテリアルを追加し、その横のテクスチャで[新規作成]を押して下の[画像]のところでさっきの画像を開きます。

下にあるシェーディングのところでソリッドからテクスチャに切り替えて白くなっていればモデル作成は成功です。

テクスチャが反映された状態

図1.5: テクスチャが反映された状態

次に、作成したモデルをXcodeで使えるようにエクスポートします。Xcodeで使える3Dモデルの拡張子は.scnと.daeと.objです。メニューバーからエクスポートを選択し、Collada(.dae)でエクスポートします。

Xcodeでの作業

インストールしておいたXcodeを開きます。。"create a new project"のところを選択するとこのような画面が出てきます。

アプリテンプレート選択

図1.6: アプリテンプレート選択

ここでアプリのテンプレートを選択することができます。中央上にAR用のテンプレートがあるのでそれを選びたくなりますが、AR用テンプレートを選ぶと最初からARが実装されてしまっているので、ARKitの凄さを伝えるためにあえてSingle View Appを選びます。

名前を適当に入れてアプリを作ったら、info.plistを開き、"Information Properly List"に"Privacy - Camera Usage Description"を追加します。

次に、Main.storyboardを開き、右下の検索欄で"ARKit SceneKit View"を探し、画面全体に広げます。

storyboard

図1.7: storyboard

次に先程作成し、エクスポートしたモデルとテクスチャをプロジェクト内に追加します。モデルのサイズが合わないので右上のところで調整しておきましょう

Xcodeでの表示

図1.8: Xcodeでの表示

次にARを表示するためのコードを書きます。

まず最初に右上のベン図みたいなマークを押してMain.storyboardとViewController.swiftを並べ、Ctrlキーを押しながらARSCNViewをViewController.swiftの最後の中括弧の上にドラッグ&ドロップし、sceneViewという名前でIBOutlet接続します。

ViewController.swift

// IBOutlet接続
@IBOutlet var sceneView: ARSCNView!

次に、ARKitをインポートします。

ViewController.swift

//ARKitのインポート
import ARKit

最後に.viewDidLoad()に次の三行を追加します。

ViewController.swift

// シーンの生成
sceneView.scene = SCNScene(named: "helloworld.dae")!

// セッションのコンフィギュレーション生成
let configuration = ARWorldTrackingConfiguration()

// セッション開始
sceneView.session.run(configuration)

これでARアプリを作ることができました。早速動かしてみましょう。

ARKitの起動にはカメラが必要なのでシュミレータではなく実機を接続してシュミレーションします。

平面を検出できたらHello,World!が表示されると思います。

Hello,World!

図1.9: Hello,World!

違う方向から見ても固定されているようにみえるのがわかります

違う方向から

図1.10: 違う方向から

ね?簡単でしょ?

モデルをいちいち作ったりしたので若干面倒でしたが、肝心のARの実装は非常に簡単にできました。

1.4 ARKitの機能

さっきの説明

次のアプリを作る前にさっきのHello,World!の説明をします。

ViewController.swift

import UIKit
import ARKit

class ViewController: UIViewController {

         override func viewDidLoad() {
           super.viewDidLoad()
           sceneView.scene = SCNScene(named: "helloworld.dae")!

           let configuration = ARWorldTrackingConfiguration()

           sceneView.session.run(configuration)
         }

         override func didReceiveMemoryWarning() {
           super.didReceiveMemoryWarning()
         }

         @IBOutlet var sceneView: ARSCNView!
}

コードを書き加えるとこのようになっているはずです。

最初から書かれているコードは無視して、まず2行目のimport ARKitというのはアプリ内でARKitを使えるようにするためのコードです。

次に、8行目のsceneView.scene = SCNScene(named: "helloworld.dae")!はシーン(表示するもの)を生成して、ARSCNViewにセットするコードで、この場合はhelloworld.daeという名前の3Dモデルを表示しています。

10行目のlet configuration = ARWorldTrackingConfiguration()はコンフィギュレーションの作成です。

ARWorldTrackingConfigurationというのはデバイスの向き等をトラッキングし、現実世界の面を検出するコンフィギュレーションです。

12行目のsceneView.session.run(configuration)はセッションの開始、つまり現実世界の認識やデバイスのモーションの解析を開始するということです。

また、session.pause()メソッドでセッションを中断することもできます。

水平、垂直面検出

ARKitには平面検出及び垂直面検出の機能があります。*1

[*1] 垂直面検出はiOS11.3以降でのみ使用可能

デフォルトではこの機能が無効になっているので、使いたい場合は自分で設定する必要があります。

まずは平面を検出して、可視化するアプリを作りましょう。

Xcodeを開いてARテンプレートでアプリを作った後、ViewController.swiftをこのように変更します。

ViewController.swift

import UIKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate,ARSessionDelegate{

  @IBOutlet var sceneView: ARSCNView!

  override func viewDidLoad() {
      super.viewDidLoad()

      // デリゲートをセット
      sceneView.delegate = self
      sceneView.session.delegate = self

      // シーンをセット
      sceneView.scene = SCNScene()

      // コンフィギュレーションの生成
      let configuration = ARWorldTrackingConfiguration()

      // 平面検出を有効化
      configuration.planeDetection = .horizontal

      // セッション開始
      sceneView.session.run(configuration)
  }

  // ARSCNViewDelegate

  // 追加されたとき
  func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
      guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
      print("anchor:\(anchor), node: \(node), node geometry: \(String(describing: node.geometry))")

      // 平面ジオメトリを作成
      let geometry = SCNPlane(width: CGFloat(planeAnchor.extent.x),
                              height: CGFloat(planeAnchor.extent.z))
      geometry.materials.first?.diffuse.contents = UIColor.yellow.withAlphaComponent(0.5)

      // 平面ジオメトリを持つノードを作成
      let planeNode = SCNNode(geometry: geometry)
                        //x-z平面に合わせる
      planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2.0, 1, 0, 0)

      DispatchQueue.main.async(execute: {
          // 検出したアンカーに対応するノードに子ノードとして持たせる
          node.addChildNode(planeNode)
      })
  }

  // 更新されたとき
  func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
      guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}

      DispatchQueue.main.async(execute: {
          // 平面ジオメトリのサイズを更新
          for childNode in node.childNodes {
              guard let plane = childNode.geometry as? SCNPlane else {continue}
              plane.width = CGFloat(planeAnchor.extent.x)
              plane.height = CGFloat(planeAnchor.extent.z)
              break
          }
      })
  }
  // 削除されたとき
  func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
      print("\(self.classForCoder)/" + #function)
  }

  // ARSessionDelegate

  // 追加されたとき
  func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
      print("\(self.classForCoder)/" + #function)
  }
  // 更新されたとき
  func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
      print("\(self.classForCoder)/" + #function)
  }
  // 削除されたとき
  func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
      print("\(self.classForCoder)/" + #function)
  }
}

これで動かすとしっかりと平面検出ができるのがわかります。

平面検出

図1.11: 平面検出

まず平面検出を有効化するためには、コンフィギュレーションのplaneDetectionプロパティに.horizontalを追加する必要があります。

垂直面検出の場合は.horizontalの代わりに.verticalを追加します。

垂直面検出

図1.12: 垂直面検出

        // 平面検出の有効化
        configuration.planeDetection = .horizontal

ARAnchor

ARAnchorはARの空間内に何らかのオブジェクトを配置するための位置、向きを表すクラスです。

これによって、デバイスを動かしてもオブジェクトはアンカー(錨)で固定されているように表示されます。

また、ARAnchorにはARPlaneAnchorというサブクラスがあり、平面,垂直面検出時に得られるアンカーはこの型で動きます。

ARPlaneAnchorには平面の中心を表すcenterプロパティと大きさを表すextentプロパティ、アラインメントを示すalignmentプロパティがあります。

center、extentともにvector_float3型ですが、extentは平面の大きさを示すのでx,zのみで大きさは決まり、yの値は常にゼロです。

ARSCNViewDelegate

ARSCNViewDelegateではアンカーと対応するノードが追加、更新、削除されるときにメソッドを呼ぶことができます。

        // ノードが追加されたとき
        func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor)

        // 更新
        func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor)

        // 削除
        func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor)

ARSessionDelegate

ARSessionDelegateではアンカーが追加、更新、削除されたときにメソッドを呼ぶことができます。

        // アンカーが追加されたとき
        func session(_ session: ARSession, didAdd anchors: [ARAnchor])

        // 更新
        func session(_ session: ARSession, didUpdate anchors: [ARAnchor])

        // 削除
        func session(_ session: ARSession, didRemove anchors: [ARAnchor])

平面に物を置く

最後に、平面を検出した後、そこにオブジェクトを置くアプリを作ってみましょう。

ViewController.swift

import UIKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate, ARSessionDelegate {

  @IBOutlet var sceneView: ARSCNView!

  override func viewDidLoad() {
    super.viewDidLoad()

    sceneView.delegate = self
    sceneView.session.delegate = self

    // シーンを生成してARSCNViewにセット
    sceneView.scene = SCNScene()

    // セッションのコンフィギュレーションを生成
    let configuration = ARWorldTrackingConfiguration()
    configuration.planeDetection = .horizontal

    // セッション開始
    sceneView.session.run(configuration)
  }

  // MARK: - ARSCNViewDelegate

  func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
    print("anchor:\(anchor), node: \(node), node geometry: \(String(describing: node.geometry))")

    // 平面ジオメトリを作成
    let geometry = SCNPlane(width: CGFloat(planeAnchor.extent.x),
                            height: CGFloat(planeAnchor.extent.z))
    geometry.materials.first?.diffuse.contents = UIColor.yellow.withAlphaComponent(0.5)

    // 平面ジオメトリを持つノードを作成
    let planeNode = SCNNode(geometry: geometry)
    planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2.0, 1, 0, 0)

    DispatchQueue.main.async(execute: {
      // 検出したアンカーに対応するノードに子ノードとして持たせる
      node.addChildNode(planeNode)
    })
    // シーンの生成
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    // オブジェクトのノード作成
    let virtualObjectNode = SCNNode()

    // 別のノードに載せ替える
    for child in scene.rootNode.childNodes {
      virtualObjectNode.addChildNode(child)
    }

    //  オブジェクトを載せる
    node.addChildNode(virtualObjectNode)
  }

  func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}

    DispatchQueue.main.async(execute: {
      // 平面ジオメトリのサイズを更新
      for childNode in node.childNodes {
        guard let plane = childNode.geometry as? SCNPlane else {continue}
        plane.width = CGFloat(planeAnchor.extent.x)
        plane.height = CGFloat(planeAnchor.extent.z)
        break
      }
    })
  }

  func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
    print("\(self.classForCoder)/" + #function)
  }

  // MARK: - ARSessionDelegate

  func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
    print("\(self.classForCoder)/" + #function)
  }

  func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
    print("\(self.classForCoder)/" + #function)
  }

  func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
    print("\(self.classForCoder)/" + #function)
  }
}

このように変更して、動かしてみましょう。

平面検出とオブジェクト

図1.13: 平面検出とオブジェクト

平面の上に3Dモデルが乗っているのがわかります。

内容は簡単で、先程は平面のアンカーに対応したノードを作りましたが、そこに別の、オブジェクトのノードを載せただけです。

デバッグオプション

ARSCNViewにはdebugOptionsというプロパティがあり、その名の通りデバッグに関する機能を利用できます。

2018年5月現在、debugOptionsの型であるSCNDebugOptionsの持つオプション一覧は以下のとおりです。

表1.1: SCNDebugOptions一覧

オプション名内容
showBoundingBoxes任意のノードのバウンディングボックス(境界を示す箱)を可視化する
showWireframeシーンのジオメトリをワイヤーフレーム付きで描画する
renderAsWireframeジオメトリをワイヤーフレームだけで描画する
showSkeletonsボーンを可視化する
showCreases細分化した状態で表示する #@#英語がよくわからなかったので消すかも
showConstraintsシーン内のノードに作用する制約オブジェクトを可視化する
showCamerasカメラを可視化する
showLightInfluencesシーン内のSCNLightの位置を可視化する
showLightExtents各SCNLightに影響を受ける領域を可視化する
showPhysicsShapes各ノードに追加されているSCNPhysicsBodyを可視化する
showPhysicsFields各SCNPhysicsBodyに影響を受ける領域を可視化する

ARKitではSCNDebugOptionsに加えて、ARSCNView用のARSCNDebugOptionsが用意されています。

表1.2: ARSCNDebugOptions一覧

オプション名内容
showWorldOriginAR空間の座標軸を可視化する
showFeaturePointsARKitが検出した特徴点群を可視化する

最も、ARSCNDebugOptionsはSCNDebugOptions型なのでdebugOptionsプロパティに普通に指定できます。

実際に適用する

まずはshowBoundingBoxesオプションを適用してみましょう。

  sceneView.debugOptions = [.showBoundingBoxes]

このコードをセッションを開始するコードの前に記述して動かすとこうなります。

他のオプションを使う場合は.showBoundingBoxes.showWireframe等に変更します。

また、ARSCNDebugOptionsを使う場合はARSCNDebugOptions.showFeaturePointsのようにします。

.showBoundingBoxes

図1.14: .showBoundingBoxes

以下、オプションを適用した場合の画像を載せます。

.showWireframe

図1.15: .showWireframe

.renderAsWireframe

図1.16: .renderAsWireframe

.showSkeletons

図1.17: .showSkeletons

showSkeletonsオプションはボーンが存在するモデルでのみ機能します。

.showCameras

図1.18: .showCameras

.showLightInfluences

図1.19: .showLightInfluences

ライトを追加したためモデルの見た目が変わっていますが、このオプションの影響ではありません。

.showLightExtents

図1.20: .showLightExtents

.showWorldOrigin

図1.21: .showWorldOrigin

.showFeaturePoints

図1.22: .showFeaturePoints

1.5 おわりに

最後まで読んでいただき、ありがとうございます。

この記事ではARKitの基本的な機能を説明したつもりですが、それはARKitの機能のほんの一部に過ぎません。

この記事を機にARおよびARKitに興味を持っていただけたら幸いです。

それでは、また来年お会いしましょう!(部誌を書くとは言ってない)

1.6 参考文献

ARKit公式ドキュメント(英語)

iOS11 Programing