Lesson 3A.6 Guided Project: AR Drawing
このユニットを通して、平面検出、3Dでのオブジェクトの位置決め、画像認識など、多くのARKitトピックについて学びました。それでは、これらのARKit機能の多くを活用する1つのアプリにその知識を組み合わせる時が来ました。
このガイド付きプロジェクトでは、SceneKitオブジェクトを使用してARシーンを構築するAR描画と呼ばれる独自の3D描画アプリを構築します。図形とモデルのリストから、ユーザーはシーンに配置するものを選択できます。次に、一連のコントロールを使用して、カメラの前、平面の上、または検出された画像に関連して、選択した場所にオブジェクトを配置します。
p.506
パート1 - スタータープロジェクト
学生リソースフォルダで、「AR描画」というスタータープロジェクトを見つけます。プロジェクトを起動すると、動作するポップオーバー、テーブルビュー、セグメント化されたコントロールなど、あらかじめ構築されたインターフェイスが表示されます。これ以上掘り下げる前に、エラーなくアプリをビルドして実行できることを確認してください。問題が発生した場合は、物理的なiOSデバイスがターゲットデバイスとして設定されていない可能性が最も高いです。他のARアプリと同様に、このプロジェクトはiOSシミュレータでは実行されません。
AR描画アプリにはすでに何がありますか?画面に表示されているものをプロジェクト内のファイルと比較してください。
Main.storyboard以降、セグメント化されたコントロールはビューの左下に適切に制限されており、ビューコントローラのchangeObjectMode(_:)メソッドがアクションとして設定されています。オプションボタンはビューの右下に制限されており、Options.storyboardのビューコントローラーにポップオーバーセグエを実行します。これはオブジェクト選択プロセスを管理するビューコントローラであり、プロジェクトナビゲータのOptionsグループには、リストの表示とリスト間の遷移に関連するコードが含まれています。
BasicShape.swiftには、ユーザーのオプションに対応するいくつかの列挙が含まれています。各列挙型には、「ボックス」や「オレンジ」など、ポップオーバーに表示されるタイトルを表す文字列値があります。[基本図形の選択] オプションでは、ShapeOption 列挙型には最初のリストの 5 つのケースが含まれています。ユーザーは、図形(形状列挙型を使用)、色、サイズ(サイズ列挙型を使用)を選択できます。[シーンファイルの選択]オプションでは、レッスン5のmodels.scnassetsに配置したモデルのリストから選択できます。他の3つの選択肢は今のところ何もしません。プロジェクトを進めながら、あなたは彼らのところに戻ってくるでしょう。
p.507
ViewController.swiftには、ユーザーインターフェイスコントロールの操作を容易にする追加のコードが含まれています。セグメント化されたコントロールの3つの選択を表すObjectPlacementMode型のobjectMode変数があります。前述のchangeObjectMode(_:)メソッドは、objectModeを適切な値に更新し、アプリがシーンにオブジェクトをどのように配置するかを判断するのに役立ちます。
ファイルの下部には、オプションメニューの項目が選択されたときに呼び出されるメソッドのリストがあります。
objectSelected(node:)は、ユーザーが図形、色、サイズを選択し、ユーザーがモデルを選択した後に呼び出されます。
togglePlaneVisualization() は、ユーザーが Plane Visualizationを有効/無効にタップすると呼び出されます。
undoLastObject() は、ユーザーが最後のオブジェクトの取り消しをタップしたときに呼び出されます。
resetScene() は、ユーザーが [シーンのリセット] をタップすると呼び出されます。
パート2 - オブジェクトリストをカスタマイズする
シーン作成に使用できるアセットをカスタマイズする機会はたくさんあります。ユーザーが選択できるSceneKitプリミティブを決定するだけでなく、どの色とサイズを指定することもできます。ガイドのこのセクションを進めながら、提供されたコードスニペットを自由に使用してください。
p.508
さまざまな選択肢を表示します。しかし、アプリのバージョンを区別できるように、時間をかけて独自のオプションを選択することを強くお勧めします。
カスタムモデル
ポップオーバーから[シーンファイルを選択]を選択すると、ユーザーはmodels.scnassets内に住んでいるすべてのモデルを配置できます。しかし、.scnファイルはモデルと同じ名前のフォルダ内に配置する必要があります。そうしないと、正しくリストされません。次の画像では、candle.scnは「candle」フォルダ内、「chair」フォルダ内のchair.scnなどにあることがわかります。
これらのモデルはどこから来たのですか?スタータープロジェクトでは提供されていませんが、簡単に入手できます。アプリで使用したい場合は、サンプルコード「拡張現実での3Dインタラクションの処理とUIコントロール」を確認してください。プロジェクトをダウンロードして、.scnassetsフォルダからモデルをコピーできます。
シェイプ
スタータープロジェクトの未修正バージョンでは、ShapeOption列挙型にはボックス、球、シリンダー、コーン、トーラスがオプションとしてあります。しかし、トーラスをピラミッドに置き換えたい場合はどうなりますか?列挙型を更新して、新しいケースセットを含めることから始めます。
p.509
コードをコンパイルすると、2つのエラーが表示されます。1つ目は、Shapeケースのコレクションを持つshapePicker()メソッドです。
Let shapes: [Shape] = [.box, .sphere, .cylinder, .cone, .torus]
.Torusはもはや有効な列挙型ケースではないため、代わりに.pyramidを使用するようにメソッドを更新する必要があります。
Let shapes: [シェイプ] =
[.Box、.sphere、.cylinder、.cone、.pyramid]
2番目のエラーは、ユーザーが図形、色、サイズの選択を終えたときに呼び出されるcreateNode(shape:, color:, size:)メソッドにあります。前のエラーと同様に、.torusはswitchステートメントで無効になったため、SCNTorusの代わりにSCNPyramidを初期化するためにケースを更新する必要があります。
2番目のエラーは、ユーザーが図形、色、サイズの選択を終えたときに呼び出されるcreateNode(shape:, color:, size:)メソッドにあります。前のエラーと同様に、.torusはswitchステートメントで無効になったため、SCNTorusの代わりにSCNPyramidを初期化するためにケースを更新する必要があります。
ケース.pyramid:
ジオメトリ = SCNPyramid(幅:メートル、高さ:メートル* 1.5、
長さ:メートル)
形状サイズ
スタータープロジェクトでは、図形のサイズの選択は小、中、大です。しかし、特大のオプションを追加したい場合はどうなりますか?Shape列挙型と同様に、サイズ列挙型を更新して新しいケースを含めます。
p.510
アプリをビルドして実行します。エラーが表示されるのは驚くことではないでしょう。繰り返しになりますが、switchステートメントは.extraLargeケースの処理方法を知らないため、createNode(shape:, color:, size:)が犯人です。問題を解決するためにメーター定数を設定する新しいケースを追加できます。
少し時間を取って、この方法が何をするかを研究してください。メーターを設定するには、ユーザーが選択したサイズが必要で、さまざまな図形のサイズを調整するために使用されます。ユーザーが図形としてBoxを選択すると、初期化されたSCNBoxオブジェクトの長さ、幅、高さはメートルに等しくなります。しかし、メーターの値に関連して、各形状のサイズを自由に決定できます。独自のサイズを作成するには、メーターの異なる値で遊んで、各ジオメトリの初期化子で値がどのように利用されるかを観察します。
シェイプカラー
ポップオーバーにリストされている色を制御する列挙型がないことに気づいたかもしれません。プロジェクトでcolorPicker()メソッドを見つけると、色の選択肢が(文字列、UIColor)タプルのコレクションであることがわかります。最初のタプル値は表示するタイトルで、2番目の値は図形に適用される実際の色です。このコレクションを調整して、アプリで使用したい色を含めます。
p.511
画像
レッスン5では、ARKitで検出する画像を指定する方法を学びました。プロジェクトナビゲータでAssets.xcassetsフォルダを開き、すでに作成されているARリソースグループを探します。選択した画像をそこに追加し、属性インスペクタで各画像の幅と高さを宣言することを忘れないでください。
ここまで、アプリで使用するモデル、図形、図形属性、画像を宣言しました。それでは、オブジェクトをシーンに配置できるように機能の追加を開始します。
パート3 - フリーフォームオブジェクト
セグメント化されたコントロールのフリーフォームオプションは、ユーザーが選択した形状またはモデル20cmをカメラの現在の位置の前に配置します。しかし、まず、アプリはどのSceneKitオブジェクトが選択されているかを知る必要があり、画面上のタップを検出するコードを書く必要があります。
選択したオブジェクトを保存する
このガイド付きプロジェクトのパート1では、ユーザーが図形またはモデルの選択を完了すると、objectSelected(node:)メソッドが呼び出されることを学びました。その情報を保存するには(画面をタップするたびに使用できます)、SCNNode?タイプのselectedNode変数を宣言し、メソッドのnodeパラメータを使用してその値を更新します。アプリが最初に起動されたとき、ユーザーは何も選択しないため、変数はオプションである必要があります。
タップの検出
他のアプリでは、ボタンとジェスチャーリコグナイザーを使用して画面のタップを検出しました。そのうちの1つがここで役に立ちますか?の冒頭で完成したアプリのビデオを確認してください
p.512
このガイド。ランディングパッドは、ディスプレイ上で指をドラッグして描画されました。つまり、ボタンやタップジェスチャーリコグナイザーを使用することはできません。検出された平面にオブジェクトをまだ配置していませんが、その相互作用を可能にするためにコードを構成する方法を検討することが重要です。
UIViewControllerクラスの一部であるメソッド、touchesBegan(_:, with:)があります。指または複数の指がデバイスの画面に触れるたびにトリガーされます。最初のパラメータは1つ以上のタッチのコレクションであるため、指に対応する少なくとも1つの値を含める必要があります。このメソッドでは、objectModeの現在の値に応じてタッチに応答する方法を決定できます。以下は実装例です。
他のオーバーライドメソッド(viewDidLoad()など)の標準的な規則に従って、まずsuperを使用して親のメソッドの実装を呼び出すことから始めます。ユーザーがポップオーバーからオブジェクトを選択していることを確認するには、selectedNodeに値があるかどうかを確認してから、少なくとも1本の指が画面に触れたかどうかを確認できます。これらの条件の両方が満たされている場合は、objectMode変数を調べて、オブジェクトをシーンにどのように配置するかを判断します。フリーフォーム設定(
p.513
セグメント化されたコントロールでデフォルトで選択されている場合)、新しいメソッドを呼び出してノードに渡します。他のすべてのケースでは、switchステートメントから抜け出すことができます(まだ実装していないため)。
以前のレッスンでは、カメラの前にオブジェクトを配置したので、addNodeInFront(_:)メソッドは比較的簡単なはずです。セッションのcurrentFrameにアクセスしてカメラの変換をつかみ、ノードの変換をカメラの変換に等しく設定し、さらに前面にさらに20cm(z軸に沿って)設定します。複数のタップでノードの複数のコピーを配置できるため、ノードのclone()メソッドを使用して、結果のコピーをシーンに追加できます。
アプリをビルドして実行します。オブジェクトを選択した後、画面をタップすると、選択したオブジェクトがカメラの前に表示されます。オブジェクトのサイズや形状によっては、より良いビューを得るために位置を調整する必要があるかもしれません。
p.514
前もって考える
ここまで、オブジェクトを配置するための3つのモードのうちの1つを完了しました。フリーフォームの配置を処理するために書いたコードのいずれかが、他の2つのモード(オブジェクトを平面の上に置くか、画像検出に応答して)に適用できるかどうかを検討してください。また、作成したコードが取り消し機能などの他のオプションと競合する可能性があるかどうかを確認する必要があります。
クローニングノード
オブジェクトがカメラの前、水平面、または画像の上に配置されているかどうかにかかわらず、作成したノードを複製してシーンのルートノードに追加する必要があります。addNodeToSceneRoot(_:)という再利用可能なメソッドでこの作業を実行し、addNodeInFront(_:)に必要な行を置き換えます。
p.515
配置されたオブジェクトの保存
最後のオブジェクトの取り消しオプションは興味深い問題を提示します。どのノードをシーンから削除すべきかをどのように知っていますか?ルートノードの最後の子ノードを削除することが解決策であり、ほとんどの場合機能すると思うかもしれません。しかし、検出された平面を視覚化すると、ユーザーが配置したオブジェクトを表さないノードを元に戻すため、問題が発生します。
この問題を防ぐ最善の方法は、独自のノードのコレクションを維持することです。ユーザーが画面をタップすると、ノードを配置されたノードと呼ばれるコレクションに追加できます。平面検出中に追加されたノードは、別のコレクションであるplanesNodesに追加できます。2つの異なるコレクションでは、最後のオブジェクトの取り消し、シーンのリセット、平面の視覚化の切り替えを実装することは簡単なはずです。
パート4 - 画像検出
コードの構造化が整ったので、画像検出を使用してシーンにオブジェクトを配置する機能を追加する準備が整いました。Renderer(_:, didAdd:, for:)メソッドは画像と平面の両方の検出に使用されますが、ARImageAnchorはサイズが大きくなったり、視覚的なフィードバックを必要としたりしないため、ARPlaneAnchorよりも少し作業が簡単です。メソッドが呼び出されたら、アンカーを検査し、ARImageAnchorかARPlaneAnchorかを判断します。
p.516
画像が見つかったらどうすればよいですか?まず、selectedNodeに値が含まれていることを確認する必要があります。そうしないと、画像の上にどのオブジェクトを配置するかわかりません。ユーザーがポップオーバーからオブジェクトを選択した場合は、オブジェクトの配置を続行できます。レッスン5で思い出すように、画像検出に応答して作成したノードは、ルートノードの子としてではなく、画像が検出されたときにARKitが作成したノードの子として追加する必要があります。addNodeToSceneRoot(_:)を使用する代わりに、ノードを適切な親にアタッチして、ほぼ同じ作業を行うメソッドが必要になります。
p.517
設定の調整
この機能を完了するには、ARKitにARリソースグループ内の画像を探すように指示します。しかし、セグメント化されたコントロールがフリーフォームまたはプレーンに設定されている場合、画像検出は行われるべきですか?答えはノーです。オブジェクトは、画像が選択されている場合にのみ、検出された画像の上に表示する必要があります。したがって、セグメント化されたコントロールの状態が変更されるたびに、セッション設定をリロードする方法を提供する必要があります。
reloadConfiguration() というメソッドを作成します。このメソッドは、objectModeの値に基づいてセッション設定のdetectionImagesプロパティを更新します。アプリが画像モードの場合、detectionImagesにはARリソースに画像を含める必要があります。アプリが他の2つのモードのいずれかにある場合、detectionImagesはnilである必要があります。viewWillAppear(_:) および objectMode が変更されるたびにメソッドを呼び出します。
p.518
アプリをビルドして実行します。SceneKitオブジェクトを選択し、セグメント化されたコントロールでイメージモードに切り替えます。検出された画像にカーソルを合わせると、オブジェクトをその上に置く必要があります。
パート5 - 飛行機の検出と可視化
平面検出を有効にするには、設定の planeDetection を reloadConfiguration() 内の値に設定します。水平面、垂直面、またはその両方を検索できます。飛行機の検出を無効にする必要がある時間はありますか?画像検出とは異なり、ARKitが周囲の知識を常に更新して改善できるように、常に平面検出を許可する必要があります。
パート4では、ARPlaneAnchorを処理するためのnodeAdded(_:, for:)メソッドを作成しましたが、まだ空です。レッスン3で行った作業と同様に、平面アンカーの寸法と一致するサイズのSCNPレーンを作成する必要があります。次に、ARKitとSceneKitの向きの違いを考慮して、平面を-90度回転させます。ノードをシーンに追加し、planesNodesコレクションに追加し、フレームワークによってより多くの情報が収集されたときに平面の位置とサイズを更新します。
p.520
アプリをビルドして実行します。検出された表面の上部に半透明の平面が見えるはずです。
飛行機の可視化を無効にする
飛行機の視覚化は、プレーンモードでオブジェクトを配置できる場所を確認するための優れた機能です。しかし同時に、飛行機はシーンの外観を混乱させる。ユーザーがSCNPlaneノードを表示/非表示にできるようにするには、ユーザーがポップオーバーからプレーンビジュアライゼーションを有効/無効にするたびに、tgglePlaneVisualizationをトリガーできます。
平面ノードは見えるか見えないかのどちらかであるため、ブール型は平面を表示するかどうかを制御するのにうまく機能します。ブール値がfalseからtrue、またはtrueからfalseに変更された場合は、一致するようにノードのisHiddenプロパティを更新します。planeNodes内の平面のリストをすでに維持しているため、これは単純なループです。
p.521
アプリをビルドして実行します。これで、セグメント化されたコントロールを使用して、平面ビジュアライゼーションの有効化と無効を切り替えることができるはずです。
パート6 - 飛行機に物体を乗せる
シーンにオブジェクトを配置するための最後のモードになりました。フリーフォームモードでオブジェクトを配置するときは、touchesBegan(_:, with:)メソッドを使用して画面上のタップを検出しました。それでは、その方法を再検討し、プレーンモードで何をすべきかを決定する時が来ました。
ヒットテストを実行する
プレーンモードでは、検出されたサーフェスに着地しない場合、タップは無視する必要があります。ARKitはどのように知っていますか?ヒットテストを実行して、タップが既存の平面と交差しているかどうかを判断します。まず、画面上のユーザーの指の位置をつかみ、その場所でヒットテストを実行します。一致する場合は、ノードの位置を現実世界のタップの位置に更新し、シーンに追加します。
p522
アプリをビルドして実行します。セグメント化されたコントロールを使用してプレーンモードに切り替え、プレーンにオブジェクトを追加します。シーンにオブジェクトが表示されない場合は、作成したばかりの平面視覚化機能を使用して、検出されたサーフェスをタップしていることを確認してください。
指をドラッグする
プレーンモードにはもう1つの楽しい機能があります。ユーザーは画面に沿って指をドラッグして、同じパスをたどる新しいノードを作成できるはずです。UIViewControllerから実装できる2つの追加メソッドがあり、タッチ位置に関する最新情報を提供します。1つ目は、指が開始位置から移動した後に呼び出されるtouchesMoved(_:, with:)です。2番目のメソッドは、画面上のユーザーの最後のタッチの場所を提供するtouchesEnded(_:, with:)です。
これは、touchesMoved(_:, with:)の簡単な実装です。touchesBegan(_:, with:)とほぼ同じ作業を行います。
p.523
アプリをビルドして実行します。このコードを使用すると、画面上で指をドラッグでき、指先の下から一定のノードストリームが流れ出ます。実際、ノードが多すぎることがわかります。オブジェクトは単にぼやけているだけです。
ノード作成の制限
指のドラッグによって作成されるノードの数を減らすにはどうすればよいですか?別のノードを追加する前に指がどこまで移動するかを決定するしきい値定数、touchDistanceThresholdを設定できます。まず、タッチが最後のノード(lastObjectPlacedPoint)を次のスニペットに作成したときに、指の位置を保存することから始めます。addNode(_:, toPlaneUsingPoint:) で変数の値を更新します。
p.524
これで、lastObjectPlacedPointと最新のタッチ位置までの距離を計算する準備が整いました。任意の2点間の距離を計算するには、ピタゴラスの定理を使用できます。
このシナリオでは、aは点間のx距離を表し、bはy距離、cは2間の距離を表します。Swiftコードを使用してcを解くと、次のようになります。
p.525
ユーザーが画面から指を離すと、ドラッグインタラクションが終了したため、lastObjectPlacedPointに保存されている値は適用されなくなりました。
このコードを使用してアプリをビルドして実行します。いくつかのテストを行った後、しきい値をシーンにとって意味のある金額に調整します。
パート7 - ユーザビリティ機能
あなたはもうすぐ終わりです!ユーザーは、モデルや図形を拡張現実に配置し、カメラの位置、画像検出、または検出された平面の上にオブジェクトを配置することができます。しかし、まだ実装していない2つのポップオーバーオプションがあります。最後のオブジェクトの取り消しとシーンのリセットです。
最後のオブジェクトを元に戻す
スタータープロジェクトから、undoLastObject()メソッドは、最初にdisclose(animated:, completion:)を呼び出さない唯一のメソッドです。なぜだかわかりますか?ユーザーがシーンに追加された最後の10個のオブジェクトを元に戻したいと想像してみてください。最後のオブジェクトの取り消しをタップした後にポップオーバーが却下された場合は、次の取り消し操作を実行する前に再表示する必要があります。とても面倒です。解決策は、ポップオーバーを却下しないことです。
最新のオブジェクト配置の取り消しは、最近配置されたノードを削除するのと同じくらい簡単です。placedNodesの最後のノードを見つけてシーンから削除し、ノードのコレクションから削除します。
p.526
アプリをビルドして実行し、元に戻す機能を確認します。シーンに複数のオブジェクトを追加し、元に戻すボタンをタップするたびに一度に1つずつ削除します。
シーンをリセットする
シーンのリセットはもう少し複雑です。ノードを削除することに加えて、平面と画像の両方の検出されたアンカーポイントを削除することが含まれます。reloadConfiguration() 内では、run(_:, options:) メソッドにオプション .removeExistingAnchors を指定できます。しかし、ユーザーがセグメント化されたコントロールを操作するたびにreloadConfiguration()が呼び出されるため、アンカーの意図しない削除がトリガーされます。
1つの解決策は、reloadConfiguration()を更新してremoveAnchorsブール引数を含めることです。ブール値を使用すると、.removeExistingAnchorsを含めるオプションであるかどうかを制御できます。ブール値がtrueの場合は、それを含めます。そうでない場合は、オプションを空白のままにしてください。
p.527
セグメント化されたコントロールが変更されると、reloadConfiguration()を呼び出すobjectModeの値が更新されます。これはアンカーを削除したくない場合なので、パラメータとしてfalseを渡します。
resetScene() が呼び出されたら、アンカーを削除する必要があるため、デフォルト値 (true) を使用します。
アプリをビルドして実行します。シーンのリセット機能を使用して、シーン全体を一掃し、クリーンなスレートからシーンの再構築を開始します。
p.528
まとめ
ARKitでアプリを構築しておめでとうございます!3Dでコーディングしたことがない場合は、5つの短いレッスンで多くの知識を得ました。あなたはためらってこのプロジェクトに入ってきたのかもしれません。このアプリでの作業は、特定のデリゲートメソッドを呼び出すタイミングと、オブジェクトを適切に配置する方法を理解するのに役立ったはずです。
この記事が気に入ったらサポートをしてみませんか?