Memo

メモ > 技術 > IDE: Xcode > SwiftUI+AR

SwiftUI+AR
■基本的なプログラムの作成 ※もともと「SceneKit」が使えたが、新しく「RealityKit」が使えるようになった 今後はこちらを採用する方がいいかもしれないが、現時点では解説が少なく、またSwiftUI同様機能不足の可能性はある 今回、「Augmented Reality App」で以下を登録してアプリを作成するものとする Content Technology: RealityKit Interface: SwiftUI 以下のコードが生成された 実機で実行するとカメラへのアクセスを求められるので許可する しばらくすると、画面上に四角の箱が現れる ContentView.swift
import SwiftUI import RealityKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) // Load the "Box" scene from the "Experience" Reality File let boxAnchor = try! Experience.loadBox() // Add the box anchor to the scene arView.scene.anchors.append(boxAnchor) return arView } func updateUIView(_ uiView: ARView, context: Context) {} } #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { ContentView() } } #endif
以下はコメントを追加したコード
import SwiftUI import RealityKit struct ContentView: View { var body: some View { // 全画面表示にする ARViewContainer().edgesIgnoringSafeArea(.all) } } // UIKitのビューや機能をSwiftUIでも使えるようにする。今回はUIViewContainerの機能を使用するためにUIViewRepresentableに準拠する(詳細は「SwiftUI+UIViewRepresentable」を参照) struct ARViewContainer: UIViewRepresentable { // 表示するViewの初期状態のインスタンスを生成する func makeUIView(context: Context) -> ARView { // ゼロで初期化することで、画面いっぱいの表示になる let arView = ARView(frame: .zero) // 3Dモデルの「Experience」ファイルから「Box」シーンを読み込む let boxAnchor = try! Experience.loadBox() // 読み込んだ「Box」シーンを「arView」シーンに表示する arView.scene.anchors.append(boxAnchor) return arView } // 表示するビューの状態が更新されるたびに呼び出される func updateUIView(_ uiView: ARView, context: Context) {} } #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { ContentView() } } #endif
Xcode15の時点では、以下のように変わっていた
import SwiftUI import RealityKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) // Create a cube model let mesh = MeshResource.generateBox(size: 0.1, cornerRadius: 0.005) let material = SimpleMaterial(color: .gray, roughness: 0.15, isMetallic: true) let model = ModelEntity(mesh: mesh, materials: [material]) // Create horizontal plane anchor for the content let anchor = AnchorEntity(.plane(.horizontal, classification: .any, minimumBounds: SIMD2<Float>(0.2, 0.2))) anchor.children.append(model) // Add the horizontal plane anchor to the scene arView.scene.anchors.append(anchor) return arView } func updateUIView(_ uiView: ARView, context: Context) {} } #Preview { ContentView() }
SwiftUIでARKitを触って考えたこと - Qiita https://qiita.com/y_taka/items/431a69d7f7acbf3387df 【やってみた】iOS13/Xcode11で登場の新機能「Reality Composer」を紹介するよ〜〜|ノースサンド|note https://note.com/northsand/n/nb245a2d4ab1f Reality Composerに任意の3Dオブジェクトをインポートする方法 - Qiita https://qiita.com/TokyoYoshida/items/678587f41ade3d04d14a 以下のようにプログラムを変更し、立方体・球体・テキストが表示されることを確認する ContentView.swift
import SwiftUI import RealityKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) // 3Dモデルの「Experience」ファイルから「Box」シーンを読み込む let boxAnchor = try! Experience.loadBox() // 読み込んだ「Box」シーンを「arView」シーンに表示する arView.scene.anchors.append(boxAnchor) // 水平面のアンカーを作成する。検出最小面積は「0.15×0.15」とする let anchor = AnchorEntity(plane: .horizontal, minimumBounds: [0.15, 0.15]) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor) // メッシュリソースで立方体を作成する。サイズは「0.1m」とする let boxMesh = MeshResource.generateBox(size: 0.1) // 光源を無視するマテリアルを作成する。色は単色の赤とする let boxMaterial = UnlitMaterial(color: UIColor.red) // モデルを作成する。メッシュとマテリアルは、上で作成したものを指定する let boxModel = ModelEntity(mesh: boxMesh, materials: [boxMaterial]) // 3つの値からなるベクトルにより、モデルの位置を「左0.2m」に指定する boxModel.position = SIMD3<Float>(-0.2, 0.0, 0.0) // アンカーにモデルを追加する anchor.addChild(boxModel) // メッシュリソースで球体を作成する。サイズは「0.1m」とする let sphereMesh = MeshResource.generateSphere(radius: 0.1) // マテリアルを作成する。環境マッピングは基本色「白」、表面の粗さは「0.0」、光の反射は「あり」とする let sphereMaterial = SimpleMaterial(color: UIColor.white, roughness: 0.0, isMetallic: true) // モデルを作成する。メッシュとマテリアルは、上で作成したものを指定する let sphereModel = ModelEntity(mesh: sphereMesh, materials: [sphereMaterial]) // 3つの値からなるベクトルにより、モデルの位置を「右0.4m」に指定する sphereModel.position = SIMD3<Float>(0.4, 0.0, 0.0) // アンカーにモデルを追加する anchor.addChild(sphereModel) // メッシュリソースでテキストを作成する let textMesh = MeshResource.generateText( "Hello, world!", // テキストは「Hello, world!」 extrusionDepth: 0.1, // 奥行きは「0.1m」 font: .systemFont(ofSize: 1.0), // フォントサイズは「1.0」 containerFrame: CGRect.zero, // コンテナフレームは「ゼロ」(その文字列の表示に必要な大きさに調整される) alignment: .left, // 左揃えで表示 lineBreakMode: .byTruncatingTail // テキストの折り返しは「末尾を切り捨て」 ) // マテリアルを作成する。環境マッピングは基本色「青」、表面の粗さは「0.0」、光の反射は「あり」とする let textMaterial = SimpleMaterial(color: UIColor.blue, roughness: 0.0, isMetallic: true) // モデルを作成する。メッシュとマテリアルは、上で作成したものを指定する let textModel = ModelEntity(mesh: textMesh, materials: [textMaterial]) // 3つの値からなるベクトルにより、モデルのサイズを「10分の1に縮小」に指定する textModel.scale = SIMD3<Float>(0.1, 0.1, 0.1) // 3つの値からなるベクトルにより、モデルの位置を「手前0.2m」に指定する textModel.position = SIMD3<Float>(0.0, 0.0, 0.2) // アンカーにモデルを追加する anchor.addChild(textModel) return arView } func updateUIView(_ uiView: ARView, context: Context) {} } #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { ContentView() } } #endif
RealityKit + ARKit 3 + SwiftUI で宙に浮く Hello World テキスト in 拡張現実 - Qiita https://qiita.com/niwasawa/items/3d1bd6af3ebbcadfb366 ■機能の検証 ※2023年3月に引き続き検証した内容 ※SwiftUIのプレビュー、「Updating took more than 5 seconds」というエラーで表示されない また、シミュレータの候補に「iPhone12」が無い(iPhone14からになっている) 動作確認は実機でしかできないようなので、それぞれいったん気にせずとする RealityKit の参考書 - Qiita https://qiita.com/john-rocky/items/77dd077a5778c7ca9369 以降は以下のうち、「ここに処理を書く」部分に書くコードのみを記載する
struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) /* ここに処理を書く */ return arView } func updateUIView(_ uiView: ARView, context: Context) {} }
ボックスの表示
// アンカーを作成する let anchor = AnchorEntity() // アンカーの位置はデバイス初期位置から下に「0.5m」、向こうに「1m」とする anchor.position = simd_make_float3(0, -0.5, -1) // ボックスのモデルを作成する。幅「0.3m」、高さ「0.1m」、奥行き「0.2m」、角の丸みの半径「0.03m」とする let boxModel = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.3, 0.1, 0.2), cornerRadius: 0.03)) // モデルをY軸で1ラジアン回転させる boxModel.transform = Transform(pitch: 0, yaw: 1, roll: 0) // アンカーにモデルを追加する anchor.addChild(boxModel) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor)
板の表示
// アンカーを作成する。アンカーの位置はデバイス初期位置から下に「0.5m」、向こうに「1m」とする let anchor = AnchorEntity(world: [0, -0.5, -1]) // 板のモデルを作成する。幅「0.2m」、高さ「0.3m」とする let plane = ModelEntity(mesh: .generatePlane(width: 0.2, height: 0.3)) // モデルをY軸で1ラジアン回転させる plane.transform = Transform(pitch: 0, yaw: 1, roll: 0) // アンカーにモデルを追加する anchor.addChild(plane) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor)
球体の表示
// アンカーを作成する let anchor = AnchorEntity() // アンカーの位置はデバイス初期位置から下に「0.5m」、向こうに「1m」とする anchor.position = simd_make_float3(0, -0.5, -1) // 球体のモデルを作成する。半径「0.1m」とする let sphere = ModelEntity(mesh: .generateSphere(radius: 0.1)) // アンカーにモデルを追加する anchor.addChild(sphere) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor)
テキストの表示
// アンカーを作成する let anchor = AnchorEntity() // アンカーの位置はデバイス初期位置から下に「0.5m」、向こうに「1m」とする anchor.position = simd_make_float3(0, -0.5, -1) // テキストのモデルを作成する let text = ModelEntity(mesh: .generateText( "Ciao!", // テキストは「Ciao!」 extrusionDepth: 0.03, // 奥行きは「3cm」 font: .systemFont(ofSize: 0.1, weight: .bold), // フォントサイズは「0.1」で太字 containerFrame: CGRect.zero, // コンテナフレームは「ゼロ」(その文字列の表示に必要な大きさに調整される) alignment: .center, // 中央揃えで表示 lineBreakMode: .byCharWrapping // テキストの折り返しは「末尾を折り返し」 )) // フォントサイズを30cmにする text.transform = Transform(pitch: 0, yaw: 0.3, roll: 0) // アンカーにモデルを追加する anchor.addChild(text) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor)
反射など現実の光に影響されるマテリアル
// アンカーを作成する let anchor = AnchorEntity() // アンカーの位置はデバイス初期位置から下に「0.5m」、向こうに「1m」とする anchor.position = simd_make_float3(0, -0.5, -1) // ボックスのモデルを作成する。幅「0.3m」、高さ「0.1m」、奥行き「0.2m」とする let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.3, 0.1, 0.2))) // マテリアルを作成する。環境マッピングは基本色「青」、表面の粗さは「0」、光の反射は「あり」とする var material = SimpleMaterial(color: .blue, roughness: 0, isMetallic: true) 光の反射量(最大値1に近づくと金属的な表面になる) material.metallic = 1 // モデルの表面に適用する box.model?.materials = [material] // アンカーにモデルを追加する anchor.addChild(box) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor)
物理レンダリングの影響を受けないマテリアル
// アンカーを作成する let anchor = AnchorEntity() // アンカーの位置はデバイス初期位置から下に「0.5m」、向こうに「1m」とする anchor.position = simd_make_float3(0, -0.5, -1) // ボックスのモデルを作成する。幅「0.3m」、高さ「0.1m」、奥行き「0.2m」とする let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.3, 0.1, 0.2))) // 光源を無視するマテリアルを作成する。色は単色の青とする let unlitMaterial = UnlitMaterial(color: .blue) // モデルの表面に適用する box.model?.materials = [unlitMaterial] // アンカーにモデルを追加する anchor.addChild(box) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor)
■サンプルの検証 以下のプログラムをもとに検証中 GitHub - john-rocky/RealityKit-Sampler: a sample collection of basic functions of Apple's AR framework for iOS. https://github.com/john-rocky/RealityKit-Sampler RealityKit の参考書 - Qiita https://qiita.com/john-rocky/items/77dd077a5778c7ca9369 SwiftUIにボックスを配置 ContentView.swift
import SwiftUI import RealityKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) // 水平面のアンカーを作成する let anchorEntity = AnchorEntity(plane: .horizontal) // ボックスのモデルを作成する。幅「0.1m」、高さ「0.1m」、奥行き「0.1m」、角の丸みの半径「0.02m」とする let boxEntity = ModelEntity(mesh: .generateBox(size: [0.1, 0.1, 0.1], cornerRadius: 0.02)) // マテリアルを作成する。環境マッピングは基本色「青」、光の反射は「あり」とする let material = SimpleMaterial(color: .blue, isMetallic: true) // モデルの表面に適用する boxEntity.model?.materials = [material] // アンカーにモデルを追加する anchorEntity.addChild(boxEntity) // 作成したアンカーを「arView」シーンに表示する arView.scene.addAnchor(anchorEntity) return arView } func updateUIView(_ uiView: ARView, context: Context) {} } #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { ContentView() } } #endif
SwiftUIではなくUIKitに配置する ContentView.swift
import SwiftUI import RealityKit struct ContentView: View { var body: some View { ARViewControllerContainer() .edgesIgnoringSafeArea(.all) } } // UIKitのUIViewControllerを扱うため、UIViewControllerRepresentableに準拠させる struct ARViewControllerContainer: UIViewControllerRepresentable { func makeUIViewController(context: UIViewControllerRepresentableContext<ARViewControllerContainer>) -> ARViewController { let viewController = ARViewController() return viewController } func updateUIViewController(_ uiViewController: ARViewController, context: UIViewControllerRepresentableContext<ARViewControllerContainer>) { } func makeCoordinator() -> ARViewControllerContainer.Coordinator { return Coordinator() } class Coordinator { } } #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { ContentView() } } #endif
ARViewController.swift
import UIKit import RealityKit class ARViewController: UIViewController { private var arView: ARView! override func viewDidLoad() { super.viewDidLoad() arView = ARView(frame: view.bounds) // アンカーを作成する let anchor = AnchorEntity() // アンカーの位置はデバイス初期位置から下に「0.5m」、向こうに「1m」とする anchor.position = simd_make_float3(0, -0.5, -1) // ボックスのモデルを作成する。幅「0.3m」、高さ「0.1m」、奥行き「0.2m」、角の丸みの半径「0.03m」とする let boxModel = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.3, 0.1, 0.2), cornerRadius: 0.03)) // モデルをY軸で1ラジアン回転させる boxModel.transform = Transform(pitch: 0, yaw: 1, roll: 0) // アンカーにモデルを追加する anchor.addChild(boxModel) // 作成したアンカーを「arView」シーンに表示する arView.scene.anchors.append(anchor) view.addSubview(arView) } }
画面内で物体に触れる(ContentView.swift は上と同じ) RealityKitで Tracking Configuration や ARSessionDelegate をつかう場合、明示的に ARKit をインポートしてセッションを構成・実行する必要がある ARをさわる【コンピュータ・ビジョン*AR】 - Qiita https://qiita.com/john-rocky/items/285b729a9ae86a0aea10 ARViewController.swift
import UIKit import Vision import RealityKit import ARKit // トラッキング情報やセッション状況の変化に対応するためARSessionDelegateプロトコルを使用する(Swiftでは多重継承ができないのでプロトコルを使用する) class ARViewController: UIViewController, ARSessionDelegate { private var arView:ARView! lazy var request:VNRequest = { // 手のポイントを取得するリクエスト。completionHandlerで指定したメソッド(handDetectionCompletionHandler)内で結果を処理する var handPoseRequest = VNDetectHumanHandPoseRequest(completionHandler: handDetectionCompletionHandler) // 検出する手は1つ handPoseRequest.maximumHandCount = 1 return handPoseRequest }() var viewWidth:Int = 0 var viewHeight:Int = 0 var box:ModelEntity! override func viewDidLoad() { super.viewDidLoad() arView = ARView(frame: view.bounds) arView.session.delegate = self view.addSubview(arView) // 平面アンカーを使用するため、ARWorldTrackingConfiguration でARセッションを実行する let config = ARWorldTrackingConfiguration() config.environmentTexturing = .automatic config.frameSemantics = [.personSegmentation] config.planeDetection = [.horizontal] // ビューの幅と高さを取得する arView.session.run(config, options: []) viewWidth = Int(arView.bounds.width) viewHeight = Int(arView.bounds.height) setupObject() } private func setupObject(){ // 水平面のアンカーを作成する let anchor = AnchorEntity(plane: .horizontal) // 板のモデルを作成する let plane = ModelEntity(mesh: .generatePlane(width: 2, depth: 2), materials: [OcclusionMaterial()]) // 衝突形状を付ける★もし動かなければ「アンカーにモデルを追加する」の後ろに戻す plane.generateCollisionShapes(recursive: false) plane.physicsBody = PhysicsBodyComponent(massProperties: .default, material: .default, mode: .static) // アンカーにモデルを追加する anchor.addChild(plane) // ボックスのモデルを作成する box = ModelEntity(mesh: .generateBox(size: 0.05), materials: [SimpleMaterial(color: .white, isMetallic: true)]) // 衝突形状を付ける box.generateCollisionShapes(recursive: false) box.physicsBody = PhysicsBodyComponent(massProperties: .default, material: .default, mode: .dynamic) // 位置を指定する box.position = [0,0.025,0] // アンカーにモデルを追加する anchor.addChild(box) arView.scene.addAnchor(anchor) } var recentIndexFingerPoint:CGPoint = .zero // 手のポイントを取得するリクエストを処理する func handDetectionCompletionHandler(request: VNRequest?, error: Error?) { // リクエストの結果から、人差し指の先の位置を取得する guard let observation = request?.results?.first as? VNHumanHandPoseObservation else { return } guard let indexFingerTip = try? observation.recognizedPoints(.all)[.indexTip], indexFingerTip.confidence > 0.3 else {return} // Visionの結果は0〜1に正規化されているので、ARViewの座標に変換する let normalizedIndexPoint = VNImagePointForNormalizedPoint(CGPoint(x: indexFingerTip.location.y, y: indexFingerTip.location.x), viewWidth, viewHeight) // 取得した指先の座標でヒットテストを実施(ヒットテストで検出するエンティティにはgenerateCollisionShapesが必要) if let entity = arView.entity(at: normalizedIndexPoint) as? ModelEntity, entity == box { // 見つけたボックス・オブジェクトに物理的な力を加える entity.addForce([0,40,0], relativeTo: nil) } recentIndexFingerPoint = normalizedIndexPoint } func session(_ session: ARSession, didUpdate frame: ARFrame) { let pixelBuffer = frame.capturedImage // ARSessionで取得したフレームでVisionリクエスト(VNImageRequestHandler)を実行 DispatchQueue.global(qos: .userInitiated).async { [weak self] in let handler = VNImageRequestHandler(cvPixelBuffer:pixelBuffer, orientation: .up, options: [:]) do { try handler.perform([(self?.request)!]) } catch let error { print(error) } } } }
■Blenderで作成したファイルを配置 RealityKit の参考書 #Swift - Qiita https://qiita.com/john-rocky/items/77dd077a5778c7ca9369 あらかじめ、BlenderからエクスポートしたUSDZファイルをツリーにドラッグ&ドロップしておく 必要に応じて、「Models」などのフォルダ(New Group)を作成するといい(フォルダ内に移動させても、参照は切れなかった) 以下のようなプログラムで、USDZファイルを画面に表示できた
import SwiftUI import RealityKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) /* 初期のコード */ /* // Create a cube model let mesh = MeshResource.generateBox(size: 0.1, cornerRadius: 0.005) let material = SimpleMaterial(color: .gray, roughness: 0.15, isMetallic: true) let model = ModelEntity(mesh: mesh, materials: [material]) // Create horizontal plane anchor for the content let anchor = AnchorEntity(.plane(.horizontal, classification: .any, minimumBounds: SIMD2<Float>(0.2, 0.2))) anchor.children.append(model) // Add the horizontal plane anchor to the scene arView.scene.anchors.append(anchor) */ /* ボックスメッシュを配置する場合 */ /* let anchor = AnchorEntity() // アンカー(ARモデルを固定する錨) anchor.position = simd_make_float3(0, -0.5, -1) // アンカーの位置は、デバイス初期位置から、0.5m下、1m向こう let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.3, 0.1, 0.2), cornerRadius: 0.03)) // 幅0.3m、高さ0.1m、奥行き0.2m、角の丸みの半径が0.03mのボックスメッシュからモデルをつくる。 box.transform = Transform(pitch: 0, yaw: 1, roll: 0) // ボックスモデルをY軸で1ラジアン回転させる anchor.addChild(box) // アンカーの子階層にボックスを加える arView.scene.anchors.append(anchor) // arViewにアンカーを加える */ /* USDZモデルを配置する場合 */ let anchor = AnchorEntity() anchor.position = simd_make_float3(0, -0.5, -1) if let usdzModel = try? Entity.load(named: "rocking-chair") { anchor.addChild(usdzModel) } arView.scene.anchors.append(anchor) return arView } func updateUIView(_ uiView: ARView, context: Context) {} }
以下も参考になるか 【iOS】USDZ形式の3DモデルをAR空間でアニメーションさせる方法 #Swift - Qiita https://qiita.com/Shota-Abe/items/88b8de761187b46fb1cb Webからのお手軽ARの手段(model-viewer)、glTF/usdによる表現力の違いについて #AR - Qiita https://qiita.com/ft-lab/items/a3ddc635c8d034137efa SwiftUIとARKitでUSDZ表示アプリ https://zenn.dev/soh92/articles/a2ad0dcfeafe80 [ARKit] 画像を認識してみる #Swift - Qiita https://qiita.com/katopan/items/1854f7cac3523f22a6a7 ■Blenderで作成したファイルを配置(平面を検出して配置)
import SwiftUI import RealityKit import ARKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) let config = ARWorldTrackingConfiguration() // 検出する面 //config.planeDetection = [.horizontal, .vertical] config.planeDetection = [.horizontal] // デバッグオプション //arView.debugOptions = [.showFeaturePoints, .showAnchorOrigins, .showAnchorGeometry] arView.session.run(config) // CoordinatorにARViewの参照を設定し、セッションデリゲートとして指定 let coordinator = context.coordinator coordinator.setARView(arView) arView.session.delegate = coordinator return arView } func updateUIView(_ uiView: ARView, context: Context) {} func makeCoordinator() -> Coordinator { return Coordinator() } class Coordinator: NSObject, ARSessionDelegate { weak var arView: ARView? func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { guard let arView = self.arView else { return } for anchor in anchors { if let planeAnchor = anchor as? ARPlaneAnchor { var modelEntity: ModelEntity? do { modelEntity = try Entity.loadModel(named: "rocking-chair") } catch { print("モデルのロードに失敗: \(error)") return } modelEntity?.generateCollisionShapes(recursive: true) let anchorEntity = AnchorEntity(anchor: planeAnchor) // モデルエンティティがnilでないことを確認 if let modelEntity = modelEntity { anchorEntity.addChild(modelEntity) arView.scene.addAnchor(anchorEntity) } } } } func setARView(_ view: ARView) { self.arView = view } } }
■Blenderで作成したファイルを配置(平面を検出してからタップで配置)
import SwiftUI import RealityKit import ARKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) let config = ARWorldTrackingConfiguration() // 検出する面 //config.planeDetection = [.horizontal, .vertical] config.planeDetection = [.horizontal] // デバッグオプション //arView.debugOptions = [.showFeaturePoints, .showAnchorOrigins, .showAnchorGeometry] arView.session.run(config) // CoordinatorにARViewの参照を設定し、セッションデリゲートとして指定 let coordinator = context.coordinator coordinator.setARView(arView) arView.session.delegate = coordinator // タップジェスチャの追加 let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:))) arView.addGestureRecognizer(tapGesture) return arView } func updateUIView(_ uiView: ARView, context: Context) {} func makeCoordinator() -> Coordinator { return Coordinator() } class Coordinator: NSObject, ARSessionDelegate { weak var arView: ARView? var lastPlaneAnchor: ARPlaneAnchor? func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { for anchor in anchors { if let planeAnchor = anchor as? ARPlaneAnchor { // 最後に検出された平面アンカーを保存 lastPlaneAnchor = planeAnchor } } } @objc func handleTap(_ sender: UITapGestureRecognizer) { guard let arView = arView, let planeAnchor = lastPlaneAnchor, let tapLocation = sender.view as? ARView else { return } let hitTestResults = tapLocation.hitTest(tapLocation.center, types: .existingPlaneUsingExtent) if let hitTestResult = hitTestResults.first { // モデルをロードし、タップされた位置に配置 var modelEntity: ModelEntity? do { modelEntity = try Entity.loadModel(named: "rocking-chair") } catch { print("モデルのロードに失敗: \(error)") return } modelEntity?.generateCollisionShapes(recursive: true) let anchorEntity = AnchorEntity(world: hitTestResult.worldTransform) if let modelEntity = modelEntity { anchorEntity.addChild(modelEntity) arView.scene.addAnchor(anchorEntity) } } } func setARView(_ view: ARView) { self.arView = view } } }
■Blenderで作成したファイルを配置(平面にマーカーを表示)
import SwiftUI import RealityKit import ARKit struct ContentView: View { var body: some View { ARViewContainer().edgesIgnoringSafeArea(.all) } } struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) let config = ARWorldTrackingConfiguration() // 検出する面 //config.planeDetection = [.horizontal, .vertical] config.planeDetection = [.horizontal] // デバッグオプション //arView.debugOptions = [.showFeaturePoints, .showAnchorOrigins, .showAnchorGeometry] arView.session.run(config) // CoordinatorにARViewの参照を設定し、セッションデリゲートとして指定 let coordinator = context.coordinator coordinator.setARView(arView) arView.session.delegate = coordinator // タップジェスチャの追加 let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:))) arView.addGestureRecognizer(tapGesture) return arView } func updateUIView(_ uiView: ARView, context: Context) {} func makeCoordinator() -> Coordinator { return Coordinator() } class Coordinator: NSObject, ARSessionDelegate { weak var arView: ARView? var planeAnchors = [ARPlaneAnchor]() func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { guard let arView = self.arView else { return } for anchor in anchors { if let planeAnchor = anchor as? ARPlaneAnchor { planeAnchors.append(planeAnchor) // マーカーの追加 let marker = createMarker() let anchorEntity = AnchorEntity(anchor: planeAnchor) anchorEntity.addChild(marker) arView.scene.addAnchor(anchorEntity) } } } @objc func handleTap(_ sender: UITapGestureRecognizer) { guard let arView = arView else { return } let location = sender.location(in: arView) let hitTestResults = arView.hitTest(location, types: .existingPlaneUsingExtent) if let hitTestResult = hitTestResults.first { // モデルをロードし、タップされた位置に配置 var modelEntity: ModelEntity? do { modelEntity = try Entity.loadModel(named: "rocking-chair") } catch { print("モデルのロードに失敗: \(error)") return } modelEntity?.generateCollisionShapes(recursive: true) let anchorEntity = AnchorEntity(world: hitTestResult.worldTransform) if let modelEntity = modelEntity { anchorEntity.addChild(modelEntity) arView.scene.addAnchor(anchorEntity) } } } func setARView(_ view: ARView) { self.arView = view } private func createMarker() -> Entity { let mesh = MeshResource.generatePlane(width: 0.05, depth: 0.05) let material = SimpleMaterial(color: .yellow, isMetallic: false) return ModelEntity(mesh: mesh, materials: [material]) } } }
■Blenderで作成したファイルを配置(配置済みのモデルをタップすると削除)
@objc func handleTap(_ sender: UITapGestureRecognizer) { guard let arView = arView else { return } let location = sender.location(in: arView) let hitTestResults = arView.hitTest(location) // タップされた場所に既にモデルが存在するかチェック if let firstHit = hitTestResults.first(where: { $0.entity is ModelEntity }) { // モデルがあれば削除 firstHit.entity.removeFromParent() } else { // 新しいモデルを配置 let planeHitTestResults = arView.hitTest(location, types: .existingPlaneUsingExtent) if let hitTestResult = planeHitTestResults.first { // モデルをロードし、タップされた位置に配置 var modelEntity: ModelEntity? do { modelEntity = try Entity.loadModel(named: "rocking-chair") } catch { print("モデルのロードに失敗: \(error)") return } modelEntity?.generateCollisionShapes(recursive: true) let anchorEntity = AnchorEntity(world: hitTestResult.worldTransform) if let modelEntity = modelEntity { anchorEntity.addChild(modelEntity) arView.scene.addAnchor(anchorEntity) } } } }
■Blenderで作成したファイルを配置(配置するモデルを選択)
import SwiftUI import RealityKit import ARKit struct ContentView: View { @State private var selectedModel: String = "rocking-chair" var body: some View { VStack { Picker("Select Model", selection: $selectedModel) { Text("Rocking Chair").tag("rocking-chair") Text("Chair").tag("chair") Text("Table").tag("table") } .pickerStyle(SegmentedPickerStyle()) .padding() ARViewContainer(selectedModel: selectedModel).edgesIgnoringSafeArea(.all) } } } struct ARViewContainer: UIViewRepresentable { var selectedModel: String func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) let config = ARWorldTrackingConfiguration() // 検出する面 //config.planeDetection = [.horizontal, .vertical] config.planeDetection = [.horizontal] // デバッグオプション //arView.debugOptions = [.showFeaturePoints, .showAnchorOrigins, .showAnchorGeometry] arView.session.run(config) // CoordinatorにARViewの参照を設定し、セッションデリゲートとして指定 let coordinator = context.coordinator coordinator.setARView(arView) arView.session.delegate = coordinator // タップジェスチャの追加 let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:))) arView.addGestureRecognizer(tapGesture) return arView } func updateUIView(_ uiView: ARView, context: Context) { // CoordinatorのselectedModelを更新 context.coordinator.selectedModel = selectedModel } func makeCoordinator() -> Coordinator { return Coordinator(selectedModel: selectedModel) } class Coordinator: NSObject, ARSessionDelegate { weak var arView: ARView? var planeAnchors = [ARPlaneAnchor]() var selectedModel: String init(selectedModel: String) { self.selectedModel = selectedModel } func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { guard let arView = self.arView else { return } for anchor in anchors { if let planeAnchor = anchor as? ARPlaneAnchor { planeAnchors.append(planeAnchor) // マーカーの追加 let marker = createMarker() let anchorEntity = AnchorEntity(anchor: planeAnchor) anchorEntity.addChild(marker) arView.scene.addAnchor(anchorEntity) } } } @objc func handleTap(_ sender: UITapGestureRecognizer) { guard let arView = arView else { return } let location = sender.location(in: arView) let hitTestResults = arView.hitTest(location) // タップされた場所に既にモデルが存在するかチェック if let firstHit = hitTestResults.first(where: { $0.entity is ModelEntity }) { // モデルがあれば削除 firstHit.entity.removeFromParent() } else { // 新しいモデルを配置 let planeHitTestResults = arView.hitTest(location, types: .existingPlaneUsingExtent) if let hitTestResult = planeHitTestResults.first { // モデルをロードし、タップされた位置に配置 var modelEntity: ModelEntity? do { modelEntity = try Entity.loadModel(named: selectedModel) //modelEntity = try Entity.loadModel(named: "rocking-chair") //modelEntity = try Entity.loadModel(named: "chair") //modelEntity = try Entity.loadModel(named: "table") } catch { print("モデルのロードに失敗: \(error)") return } modelEntity?.generateCollisionShapes(recursive: true) let anchorEntity = AnchorEntity(world: hitTestResult.worldTransform) if let modelEntity = modelEntity { anchorEntity.addChild(modelEntity) arView.scene.addAnchor(anchorEntity) } } } } func setARView(_ view: ARView) { self.arView = view } private func createMarker() -> Entity { let mesh = MeshResource.generatePlane(width: 0.05, depth: 0.05) let material = SimpleMaterial(color: .yellow, isMetallic: false) return ModelEntity(mesh: mesh, materials: [material]) } } }

Advertisement