メモ > 技術 > 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])
}
}
}