[ARKit]LiDARスキャナを使ったアプリを動かしながら実装を見る

LiDARスキャナを体感してみた記事です。
2020.10.19

iPad ProにはLiDARスキャナが搭載されています。iPad Proが手元にあって試せる環境なので実際試してみました。

LiDARとは何か

LiDARスキャナは赤外線を使って奥行きを計測することができるセンサーでiPad Pro 第4世代の他に直近のイベントで発表されたiPhone12 Proと iPhone 12 Pro Maxにも搭載されています。

カメラを向けた範囲の三次元形状を高精度で把握できるのでARが高速度・高精度になります。LiDARはLight Detection and Rangingの略です。レーザー光を照射して物体に当たって跳ね返ってくるまでの時間を計測するToF(TIme of Flight)方式を採用していて、他にもXbox OneのKinectセンサもこの仕組みを採用しています。

これまでのARアプリだとアプリを開始する前に儀式としてカメラを明るい場所に向けて動かしたり円を描くように動かしつづけるよう指示をされる場合がありました。これは特徴点を認識して平面などを推定する必要があったからですが、iPad Proを初めとしたLiDARスキャナを搭載したデバイスの場合はLiDARでそのまま周囲の形状や奥行きを把握できるためこのような事前の儀式が不要になります。

LiDARによりARの速度や精度の向上だけでなくオブジェクトオクルージョンにも対応します。オベブジェクトオクルージョンを使うことで仮想オブジェクトの前後関係を正しく描画できます。既存のARアプリでよくある現実の物体との前後関係が破綻してしまうことも少なくなります。

Visualizing and Interacting with a Reconstructed Sceneを読む

LiDARを手軽に試せるサンプルコードがAppleから提供されています。動かす前にドキュメントを片手にソースコードを読んでみます。

LiDARスキャナは、ユーザーの目の前の広い範囲の奥行き情報を素早く取得するため、ARKitはユーザーが移動することなく実世界の形状を推定することができます。ARKitは、奥行き情報を一連の頂点に変換してメッシュを形成できます。情報を分割するために、複数のアンカーを作成します。メッシュアンカーは、ユーザーの周りの実世界のシーンを表しています。

このメッシュアンカーを利用することで実世界の点の位置をより正確に特定することができたり、ARKit が認識できる実世界のオブジェクト分類、バーチャルコンテンツを違和感なく現実世界と連動させることができます。

ARに関する複雑な知識を必要とせずアプリケーションの実装に集中できるようにリリースされたARKitですがシーンメッシュを有効にするのも非常に簡単です。

arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.sceneReconstruction = .meshWithClassification

実際に動作させるとこのように空間が認識されてシーンメッシュが有効になっているのが確認できると思います。

ソースコードのViewController.swiftの39行目を見るとshowSceneUnderstandingというケースがデバッグオプションで有効化されているのが確認できます。

// Display a debug visualization of the mesh.
arView.debugOptions.insert(.showSceneUnderstanding)

アプリがシーン再構成で平面検出を有効にした場合、ARKitはその情報を考慮してメッシュを作成します。LiDAR スキャナが現実の表面上でわずかに不均一なメッシュを生成する場合、ARKit はその表面上の平面を検出したところでメッシュを滑らかにします。アプリでPlane DetectionのStopとStartを操作できるのはこの効果を体感できるようにするためです。

表面の認識とRaycast

メッシュを使って表面の位置を取得するアプリはLiDARの恩恵を受けてこれまでにない精度を実現することができます。メッシュを考慮することで、レイキャストは、非平面のサーフェスや、白い壁のような特徴のほとんどない面と交差している部分も認識できます。

ソースコードやドキュメントを読むために必要な用語の説明になりますが、レイキャスティングというのは視点からRay(光線)をCast(放射)して物体までの距離を計算する手法です。このレイキャスティングに関する実装を行っているのがViewController.swiftの76行目です。ユーザーが画面をタップした際に呼ばれる処理になります。

レイキャスティングを正確に行うためにユーザーが画面をタップした時にレイキャスティングしています。メッシュ化された現実世界のオブジェクト上の点を取得するためにARViewのインスタンスメソッドraycast(from:allowing:alignment:)をコールして、引数allowingにestimatedPlaneというケースを、引数alignmentにanyというケースを渡すことでメッシュも考慮に入れたレイキャストを行っています。

let tapLocation = sender.location(in: arView)
if let result = arView.raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .any).first {

このアプリでは以下にようにタップした時に球体オブジェクトを表示することがありますが、これはレイキャスティングの結果が返ってきた時に交点にオブジェクトを置いている空です。

動画を見ると球体オブジェクトの隣にTableと記載されているのが確認できると思います。これはARMeshClassificationというARKitの機能で実現されています。

ARMeshClassificationを有効にしているのがviewDidLoad()の以下の処理です。

arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.sceneReconstruction = .meshWithClassification

該当の処理を行っているのが以下で

Classificationを行っている実装を読んでいる時とfaceという言葉が登場します。これはソースコードが紹介されているページに記載がありますが、メッシュ内の3つの頂点がそれぞれ三角形を形成していてそれを面と呼ぶそうです。この面にClassification(ARMeshClassification)がある場合、アプリ内でそのClassificationをテキストで画面に表示します。負荷のかかる処理なのでglobal queueで行うようにしてレンダリングに影響を与えないようにしているようです。

ドキュメンテーションコメントにも以下のようにコメントがあります。

// Perform the search asynchronously in order not to stall rendering.

ClassificationはenumでARMeshClassificationという型で定義されています。先ほどの動画はtableと判定されていました。

case none = 0
case wall = 1
case floor = 2
case ceiling = 3
case table = 4
case seat = 5
case window = 6
case door = 7

手前にある物体が背後にある物体を隠して見えない状態するなど前後関係をうまく表現するオクルージョンを有効にするにはsceneUnderstandingプロパティを変更します。

arView.environment.sceneUnderstanding.options.insert(.occlusion)

この錯視を実現するために、RealityKitはユーザーが見ている仮想コンテンツの前にメッシュがあるかどうかをチェックし、そのメッシュで見えなくなっている仮想コンテンツの部分の描画を省略しています。サンプルでは、環境の sceneUnderstandingプロパティにオクルージョンオプションを追加することで、オクルージョンを有効にしています。

物理計算の有効化もsceneUnderstandingプロパティに追加で行います。

arView.environment.sceneUnderstanding.options.insert(.physics)

物理計算はClassificationを表すテキストを表示した後、一定の時間が経った後にテキストを落下させるのに使用されています。

ドキュメントでの説明だけでは想像が付かなかったため、Sceneにextensionで定義されている実装を見ます。

func addAnchor(_ anchor: HasAnchoring, removeAfter seconds: TimeInterval) {
    guard let model = anchor.children.first as? HasPhysics else {
        return
    }

    // Set up model to participate in physics simulation
    if model.collision == nil {
        model.generateCollisionShapes(recursive: true)
        model.physicsBody = .init()
    }
    // ... but prevent it from being affected by simulation forces for now.
    model.physicsBody?.mode = .kinematic

    addAnchor(anchor)
    // Making the physics body dynamic at this time will let the model be affected by forces.
    Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { (timer) in
        model.physicsBody?.mode = .dynamic
    }
    Timer.scheduledTimer(withTimeInterval: seconds + 3, repeats: false) { (timer) in
        self.removeAnchor(anchor)
    }
}

物理シミュレーションで使用される物理プロパティを提供していることを表すHasPhysicsプロトコルに準拠しているかどうかをHasAnchoring型の値から取り出しています。

最初は物理法則に従わないようにmodeプロパティにkinematicを指定しています。そしてaddAnchorした後に数秒後に物理法則に従うようにdynamicを指定しています。そしてその3秒後にremoveAnchorを呼び出してAnchorを画面から削除しています。

参考リンク

まとめ

LiDARを使ってみたいと思ったもののLiDARについて何もわからん、といった状態だったので公式に提供されているサンプルコードをまずは動かしてみて、その後コードを読んでみて知識のギャップを埋めながら理解できるようになるところから始めました。

そのままいくつかのコード片を利用する人もいそうなぐらいうまく処理が切り分けられていてARのこと以外でも学べることが多かったです。次はここまでで知ったことを使ってLiDARで遊んでみた内容を記事にしたいと思います。