85
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

#まえがき
iOSでARアプリを作るならRealityKit。
この記事では基本から応用まで、RealityKitの使い方を知ることができます。
入門に、索引に、ご利用ください。

(もしもiOSアプリを作ったことがない場合は、「最初のアプリを作る」をよんでください。)

#もくじ

1.AR画面を表示する
AR画面
2.オブジェクトを表示する
ボックス テキスト
USDZモデル
3.表面を加工する
シンプル アンリット オクルージョン 画像
動画 USDZ USDZ+動画
4.位置や角度を調整する
5.現実のアンカー(目印)をつかって配置する
水平面 垂直面 画像 オブジェクト
[ 🐣
からだ AR

(画像アンカー・デモ)

6.アニメーション
移動 回転 拡大・縮小 USDZ
7.照明
ポイント ダイレクショナル スポット
8.物理作用
物理・衝突 衝突検知
9.よりリアルな現実世界との相互作用
10.オーディオ
11.タップとジェスチャー
タップ ジェスチャー
12.カスタム・コンポーネント
13.複数のデバイスで共有する
デバイス間共有 #デバイス・アンカー
14.RealityComposerで手軽にシーンをつくる
RealityComposer
15.イベントを検知する
イベント検知
16.ARKitとRealityKitの関係と注意点
17.RealityKitをつかうメリット
18.RealityKitのサンプルコード集

180.png

Put A Box Giant Robot 100inch-Monitor Building blocks
(https://github.com/john-rocky/RealityKit-Sampler)
Speech Balloon Special Move Face Cropper AR Hockey
Hand Interaction

1.AR画面を表示する

ARViewを置くことで、AR表示可能なカメラ画面が表示されます。

【実装方法】

※Info.PlistでCamera Usage Descriptionを追加

UIKit

import UIKit
import RealityKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let arView = ARView(frame: view.bounds)
        view.addSubView(arView)
        // ストーリーボードの@IBOutlet接続でも大丈夫です
    }
}

SwiftUI

import SwiftUI
import RealityKit

struct ContentView : View {
    var body: some View {
        return ARViewContainer()
            .edgesIgnoringSafeArea(.all) // 全画面表示
    }
}

struct ARViewContainer: UIViewRepresentable {
    
    func makeUIView(context: Context) -> ARView {
        
        let arView = ARView(frame: .zero)

        // ARView が読み込まれるときにしたい処理をかく書く
                
        return arView
        
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {}
    
}

ARViewのサブクラス化

SwiftUIでは、ARViewをUIViewRepresentableで表示するか、もしくはARViewを持つViewControllerを UIViewControllerRepresentableで表示すると便利です。
このようにARViewをサブクラス化することで、ARに関するコードを他から分離して書きやすくなります。

UIViewRepresentable

ARViewのサブクラスをViewとして表示する方法

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 customARView = CustomARView(frame: .zero)
         // ゼロの CGRect で初期化することで、 View の幅いっぱいの表示になります
        
        return customARView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {

    }
}
CustomARView.swift
import UIKit
import RealityKit

class CustomARView: ARView {
    required init(frame frameRect: CGRect) {
        super.init(frame: frameRect)
    }
    
    required init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
UIViewControllerRepresentable

ARViewControllerごとViewとして表示する方法

ContentView.swift
import SwiftUI
import RealityKit

struct ContentView: View {    
    var body: some View {
        ARViewControllerContainer()
            .edgesIgnoringSafeArea(.all) // 全画面表示
    }
}

struct ARViewControllerContainer: UIViewControllerRepresentable {
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<ARViewControllerContainer>) -> UIViewController {
        let viewController = ARViewController()
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ARViewControllerContainer>) {
    }
    
    func makeCoordinator() -> ARViewControllerContainer.Coordinator {
        return Coordinator()
    }
    
    class Coordinator {
        
    }
}
ARViewController
import UIKit
import RealityKit

class ARViewController: UIViewController {
    
    private var arView: ARView!

    override func viewDidLoad() {
        super.viewDidLoad()
        arView = ARView(frame: view.bounds)
        view.addSubview(arView)
    }

}

2.オブジェクトを表示する

RealityKitのシンプルなオブジェクト、もしくはUSDZを表示できます。

ボックス

【ボックスメッシュ】

IMG_5099のコピー.png
【実装方法】

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にアンカーを加える

【プレイン・メッシュ】

IMG_5100.PNG
【実装方法】

let anchor = AnchorEntity(world: [0, -0.5, -1])

let plane = ModelEntity(mesh: .generatePlane(width: 0.2, height: 0.3))
 // 幅0.2m、高さ0.3mの板メッシュからモデルをつくる。
plane.transform = Transform(pitch: 0, yaw: 1, roll: 0)

anchor.addChild(plane)
arView.scene.anchors.append(anchor)

球体

【球体メッシュ】

IMG_5101.PNG
【実装方法】

let anchor = AnchorEntity()
anchor.position = simd_make_float3(0, -0.5, -1)

let sphere = ModelEntity(mesh: .generateSphere(radius: 0.1))
 // 半径0.1mの球体メッシュからモデルをつくる。

anchor.addChild(sphere)
arView.scene.anchors.append(anchor)

テキスト

【テキスト・メッシュ】

IMG_5102.PNG
【実装方法】

let anchor = AnchorEntity()
anchor.position = simd_make_float3(0, -0.5, -1)

let text = ModelEntity(mesh: .generateText("Ciao!", extrusionDepth: 0.03, font: .systemFont(ofSize: 0.1, weight: .bold), containerFrame: CGRect.zero, alignment: .center, lineBreakMode: .byCharWrapping))
 // "Ciao!"という文字列、奥行き0.03m、システムフォント0.1m-bold、テキストの折り返しbyCharWrappingでモデルを作成。
 // containerFrameをゼロにすると、その文字列の表示に必要な大きさに文字列のコンテナが調整される。
 // コンテナフレームはモデルの原点(中心)にoriginが設定されるので、origin0だと少し右寄りになる。

 // フォントサイズは、m単位になるので、20にすると20mで表示されてしまうので注意。 
text.transform = Transform(pitch: 0, yaw: 0.3, roll: 0)
        
anchor.addChild(text)
arView.scene.anchors.append(anchor)

USDZモデルを表示する

3D の複雑な形状を表示できます。

【恐竜のUSDZ】
IMG_5110のコピー.png
【実装方法】

let anchor = AnchorEntity()
anchor.position = simd_make_float3(0, -0.5, -1)

if let usdzModel = try? Entity.load(named: "raptor") {
    anchor.addChild(usdzModel)
}

arView.scene.anchors.append(anchor)

非同期ロード

大量の高品質モデルのロードはアプリをブロックします。
非同期ロードが使えます。

_ = Entity.loadAsync(named: "model1")
    .append(Entity.loadAsync(named: "model2"))
    .append(Entity.loadAsync(named: "model3"))
    .collect()
    .sink { models in 
            // ロード完了時の処理
          }

3.表面を加工する

オブジェクトの表面の色・質感・模様をつけます。

シンプル・マテリアル

反射など現実の光に影響されるマテリアル

【ブルー・メタリックのシンプル・マテリアル】

IMG_5107のコピー.png

【実装方法】

let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.3, 0.1, 0.2)))
let material = SimpleMaterial(color: .blue,
                              roughness: 0, // 表面の粗さ
                              isMetallic: true) // 光を反射するか
 // 青色、粗さ0、メタリックのシンプルなマテリアルをつくる

material.metallic = 1 // 光を反射する度合い。 最大値1に近づくと金属的な表面になる

box.model?.materials = [material] // ボックスの表面に適用する

アンリット・マテリアル

物理レンダリングの影響を受けないマテリアル。明るさに影響されないので、暗い場所でもはっきり見える。

【ブルーのアンリット・マテリアル】

IMG_5397のコピー.png

【実装方法】

let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.03, 0.01, 0.02)))
let unlitMaterial = UnlitMaterial(color: .blue)
box.model?.materials = [unlitMaterial]

オクルージョン・マテリアル

ARオブジェクトを透過するマテリアル。

【ロボットの手前にオクルージョン・ボックスを配置】

IMG_5394のコピー.png

【実装方法】

let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.03, 0.01, 0.02)))
let occlusionMaterial = OcclusionMaterial()
box.model?.materials = [occlusionMaterial]

画像テクスチャ

【画像をボックスに貼り付け】
IMG_5617のコピー.jpg

【実装方法】

let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.5, 0.5, 0.2)))

if let texture = try? TextureResource.load(named: "dolphin") { // テクスチャ・リソースとして画像を読み込む
    var imageMaterial = UnlitMaterial()
    imageMaterial.baseColor = MaterialColorParameter.texture(texture) // アンリット・マテリアルのテクスチャに設定する 
    box.model?.materials = [imageMaterial] // ボックスのマテリアルに設定する
}

【Tips1 画像の明るさのバグ】
上記のように画像をXcode のアセット・フォルダ( Assets.xcassets )から画像名で読み込むと、オリジナルの画像よりも暗くなるバグが発生することがあります
その場合、同じ画像をURLで読み込むと、改善されます。
アセット・フォルダではなく、バンドルのファイルの階層に画像を置き、 URLで読み込みます。

if let url = Bundle.main.url(forResource: "dolphin", withExtension: "png"), // メインバンドルの画像URLを取得 
   let texture = try? TextureResource.load(contentsOf:url, withName:nil) { // 画像URLからテクスチャリソースとして読み込む

   var imageMaterial = UnlitMaterial()
   imageMaterial.baseColor = MaterialColorParameter.texture(texture)
   box.model?.materials = [imageMaterial]

}

【Tips2 UIImageやリモート画像の適用】
UIImage やリモート URL は直接 TextureResource で読み込めないので、一度ローカルに保存してから URL で読み込みます。

let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("temp.png")

if let uiImage = UIImage(named: "dolphin"),
   let pngData = uiImage.pngData(),
   ((try? pngData.write(to: url)) != nil), // 一度ローカルに書き込む
   let texture = try? TextureResource.load(contentsOf: url) { // 保存した画像のローカルURLからテクスチャリソースとして読み込む
   var imageMaterial = UnlitMaterial()
   imageMaterial.baseColor = MaterialColorParameter.texture(texture)
   box.model?.materials = [imageMaterial]
}

【Tips3 画像の縦横比】
長方形などでは、メッシュの縦横サイズによって画像の縦横比がゆがみます。
ボックスの横幅を基準として、画像の縦横比にあわせてサイズを決定するには以下のようにできます。
長方形ボックスの場合は側面のセットのどちらかか、上下面のいずれかのアスペクトを諦める必要があります。
以下のコードは上下面を諦めています。

func getBoxSizeForImage(image: UIImage, boxWidth: Float) -> SIMD3<Float> {
    let imageSize = image.size
    if imageSize.width > imageSize.height {
        let aspect = imageSize.width / imageSize.height
        return [Float(aspect) * boxWidth, boxWidth, boxWidth]
    } else {
        let aspect = imageSize.height / imageSize.width
        return [boxWidth, Float(aspect) * boxWidth, boxWidth]
    }
}
let boxSize = getBoxSizeForImage(image: uiimage!, boxWidth: 0.1) // ボックスの横幅0.1mに合わせて、画像が歪まない縦幅と奥行きを取得する
let box = ModelEntity(mesh: .generateBox(size: boxSize))

【Tips4 Reality Composerのバグ】
トリビアですが、 Reality Composerで作成したメッシュにコードからテクスチャを貼ろうとすると、画像の向きが逆になったり、画像の一部分しか貼れなかったりします。

動画マテリアル

【ボックス・メッシュに動画を貼り付け】
Jun-09-2021 11-18-40.gif
【実装方法】

*メインバンドルに動画ファイルを置き、CopyBundleResouseで動画ファイルをリソースとしてリンクしておきます。

import AVFoundation // 動画再生用にAVFoundationをインポート
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.5, 0.5, 0.2))) // ボックスを作る

if let videoURL = Bundle.main.url(forResource: "rat", withExtension: "mp4") { // バンドルのmp4ファイルのURLを取得
 // バンドルに動画ファイルを入れる場合は、Copy Bundle Resources に動画ファイルを設定しないと nil が返ってくるので注意
    let asset = AVURLAsset(url: videoURL) // URLからAVAssetを作る
    let playerItem = AVPlayerItem(asset: asset) // AVAssetでAVPlayerItemを作る
    let player = AVPlayer(playerItem: playerItem) // AVPlayerItemでAVPlayerを初期化
    let videoMaterial = VideoMaterial(avPlayer: player) // AVPlayer から VideoMaterial をつくる
    box.model?.materials = [videoMaterial] // ボックスのマテリアルに適用
    player.play() // AVPlayerを再生する
}

【Tips 動画の縦横比】
動画のアスペクトに合わせて、横幅基準でメッシュのサイズを取得するには、以下のような方法が使えます。

func getSizeForVideo(baseWidth: Float) -> (Float, Float) {
    guard let url = model.videoURL else { return (baseWidth, baseWidth) }
    let resolution = resolutionForVideo(url: url)
    let width = resolution.0!.width
    let height = resolution.0!.height
        
    guard resolution.1!.b == 0 else {
        if width > height {
            let aspect = Float(width / height)
            return (baseWidth, Float(aspect) * baseWidth)
        } else {
            let aspect = Float(height / width )
            return (Float(aspect) * baseWidth, baseWidth)
        }
    }
        
    if width > height {
        let aspect = Float(width / height)
        return (Float(aspect) * baseWidth, baseWidth)
    } else {
        let aspect = Float(height / width )
        return (0.05, Float(aspect) * baseWidth)
    }
}
    
private func resolutionForVideo(url: URL) -> (CGSize?,CGAffineTransform?) {
    guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { return (nil,nil) }
    let size = track.naturalSize.applying(track.preferredTransform)
    return (CGSize(width: abs(size.width), height: abs(size.height)),track.preferredTransform)
}

USDZの表面を加工

【USDZに青色のシンプル・マテリアルを貼り付け】
IMG_5131.PNG
【実装方法】

let usdzModel = try! Entity.loadModel(named: "raptor") // USDZを読み込む
for index in 0 ..< usdzModel.model!.mesh.expectedMaterialCount { // USDZのマテリアルの数だけ貼り付ける
    let material = SimpleMaterial(color: .blue, roughness: 0, isMetallic: true)
    usdzModel.model?.materials[index] = material
}

USDZ+動画

【USDZに動画マテリアルを貼り付け】
Jun-11-2021 13-15-45.gif
【実装方法】

let usdzModel = try! Entity.loadModel(named: "raptor")
var videoMaterials:[VideoMaterial] = []
guard let videoURL = Bundle.main.url(forResource: "fire", withExtension: "mp4") else {return}
let asset = AVURLAsset(url: videoURL)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
let videoMaterial = VideoMaterial(avPlayer: player)

for _ in 0 ..< usdzModel.model!.mesh.expectedMaterialCount { // USDZのマテリアルの数だけ動画マテリアルを作る
    videoMaterials.append(videoMaterial)
}
usdzModel.model?.materials = videoMaterials // マテリアルを適用
player.play()

4.オブジェクトの位置や角度を調整する

位置

ローカル座標の中の位置を設定(原点は親エンティティの原点[0,0,0])
entity.position = [-1,1,-2]
 // 親エンティティの原点から1m左、1m上、2m向こうに配置

【ローカル座標】

スクリーンショット 2021-06-19 12.19.19.png
*ローカル座標の原点はワールド座標の原点とは異なる(親エンティティがルートシーンの場合は一致する)

ワールド座標の中の位置を設定(アプリ起動時のカメラ位置が原点)
let worldOriginAnchor = AnchorEntity(world: [0,0,0])
arView.scene.addAnchor(worldOriginAnchor)

entity.position = worldOriginAnchor.convert(position: [-1,1,-2], to: entity)
 // ワールド座標の原点(デバイスの初期位置)から1m左、1m上、2m向こうに配置

【ワールド座標】
スクリーンショット 2021-06-19 12.09.51.png

角度

let degree:Float = 40 * 180 / .pi
entity.orientation = simd_quatf(angle: degree, axis: [1,0,0])
 // x軸で40度回転

スケール

entity.setScale([0.5,0.5,0.5], relativeTo: entity)
 // 半分のスケールに

entity.scale = [0.5, 0.5, 0.5]
 // 親エンティティに対してのスケールを半分に

SIMD3

座標やスケールはSIMD3で指定します。
simd_make_float3 ( ) もしくは配列で初期化できます。

let xyz:SIMD3<Float> = simd_make_float3(0.3, 0.1, 0.2) 
let xyz:SIMD3<Float> = [0.3, 0.1, 0.2]
 // どちらでも同じ
 // 単位はm

5.現実のアンカー(目印)をつかって配置する

現実の特徴点にARを配置できます。
カメラが動いても、オブジェクトはアンカーにとどまります。
アンカーが動くと、オブジェクトは追従します。
アンカーを検出しだい、モデル・エンティティが出現します。

水平面アンカー

机や床などをアンカーにできます。

【水平面アンカーで机を検出してボックスを配置】
IMG_5135.PNG
【実装方法】

平面アンカーを使用する場合は、 ARWorldTrackingConfiguration で AR セッションを実行します。

let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal, .vertical]
arView.session.run(config, options: [])
let anchor = AnchorEntity(plane: .horizontal, // 水平面のアンカーエンティティを作る
                          classification: .table, // 平面分類は、 .floor や .any など設定可能
                          minimumBounds: [0.2,0.2])  // 検出最小面積(省略可)
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.1, 0.03, 0.05)))
anchor.addChild(box) // ボックスをアンカーエンティティの子にする
arView.scene.anchors.append(anchor)

垂直面アンカー

壁やドアなどをアンカーにできます。

【ディスプレイに垂直面アンカーでボックスを配置】
IMG_5136.PNG
【実装方法】

let anchor = AnchorEntity(plane: .vertical) // 垂直面のアンカーエンティティを作る
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.1, 0.03, 0.05)))
anchor.addChild(box) // ボックスをアンカーエンティティの子にする
arView.scene.anchors.append(anchor)

画像アンカー

ポスターや雑誌の表紙などをアンカーにできます。

【ディスプレイに映った画像を認識してボックスを配置】
IMG_5156.jpeg

【実装方法】

アンカーにしたい画像を登録します。

1、「Assets.xcassets」 → 左下の「+」ボタン → 「AR and Textures」 → 「AR Resource Group」 で ARリソースのフォルダをつくり、アンカーにしたい画像を ドラッグ & ドロップする。
2、アンカー画像をクリックし、右側のペインでアンカー画像の幅と高さを登録する。
スクリーンショット 2021-06-12 9.25.21.png
3、ARリソース・フォルダ名と画像名からアンカーをつくる。

let anchor = AnchorEntity(.image(group: "AR Resources", name: "girlImage"))
 // ARリソースのグループ名と画像名を指定して画像アンカーをつくる
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.1, 0.03, 0.05)))
anchor.addChild(box)
arView.scene.anchors.append(anchor)

【Demo パソコン画面から飛び出すビデオ】

Jun-12-2021 12-54-44

動画の最初のフレームを画像アンカーとして設定して、アンカー画像と同じサイズのボックス・エンティティに動画を貼り付けて再生しています。

【実装方法】

1、動画ボックスの準備
let anchor = AnchorEntity(.image(group: "AR Resources", name: "girlImage"))
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.595, 0.001, 0.325)))
 // オリジナルのアンカー画像と同じサイズのボックスをつくる(iMacのディスプレイサイズ)

// 動画マテリアルをつくってはりつける
guard let videoURL = Bundle.main.url(forResource: "girl", withExtension: "mp4") else {return}
let asset = AVURLAsset(url: videoURL)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
let videoMaterial = VideoMaterial(avPlayer: player)
box.model?.materials = [videoMaterial]

// デリゲートメソッドで操作できるようにクラス変数として保持しておく
videoBoxEntity = box
avPlayer = player
anchor.addChild(box)
arView.scene.anchors.append(anchor)
2、アンカーを見つけたときに再生する
var videoBoxEntity:ModelEntity?
var avPlayer:AVPlayer?

func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
 // セッションに新しいアンカーが発見されたとき実行されるメソッド
    for anchor in anchors {
        guard let imageAnchor = anchor as? ARImageAnchor else { continue }
         // イメージアンカーが見つかった場合
        guard let videoBox = videoBoxEntity else { continue }
        if imageAnchor.name == "girlImage" { // 見つかったアンカーの名前を確認する
             guard let videoMaterial = videoBox.model?.materials.first as? VideoMaterial else {return}
             // 動画のボックスを拡大、移動
             videoBox.move(to: Transform(scale: simd_make_float3(1, 100, 1),
                                         rotation: videoBox.orientation,
                                         translation: simd_make_float3(videoBox.transform.translation.x,
                                                                       videoBox.transform.translation.y+0.7,
                                                                       videoBox.transform.translation.z)),
                          relativeTo: videoBox.parent,
                          duration: 3)
              

            avPlayer?.play() // 動画を再生
       }
   }
}

オブジェクト・アンカー

事前にスキャンしたオブジェクトをアンカーにできます。

【植物のポットをアンカーにしてボックスを配置】
IMG_5164.PNG

【実装方法】

1、ARKitのスキャン機能でアンカーにしたいオブジェクトをスキャンする

let configuration = ARObjectScanningConfiguration()
configuration.planeDetection = .horizontal
sceneView.session.run(configuration, options: .resetTracking)
sceneView.session.createReferenceObject(
    transform: boundingBox.simdWorldTransform,
    center: float3(), extent: boundingBox.extent,
    completionHandler: { object, error in
        // 生成された参照オブジェクトをここで処理する
    }
)

[アップルのサンプル・アプリ] (https://developer.apple.com/documentation/arkit/content_anchors/scanning_and_detecting_3d_objects)を実行すると、 .arobject ファイルをつくることができます。

Jun-12-2021 15-20-00

2、生成された .arobject ファイルを AR and Textures のリソース・グループに登録する(手順は画像アンカーと同じ)

スクリーンショット 2021-06-12 15.10.32.png

3、登録したリソース・グループ名と .arobject の名前でオブジェクト・アンカーをつくる

let anchor = AnchorEntity(.object(group: "AR Resources", name: "pot"))
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.1, 0.03, 0.05)))
anchor.addChild(box)
arView.scene.anchors.append(anchor)

半透明の球体で包んでみました。

IMG_5170のコピー.png

let anchor = AnchorEntity(.object(group: "AR Resources", name: "pot"))
let sphere = ModelEntity(mesh: .generateSphere(radius: 0.1))
let material = SimpleMaterial(color: .green.withAlphaComponent(0.3), roughness: .float(0), isMetallic: true)
sphere.model?.materials = [material]
anchor.addChild(sphere)
arView.scene.anchors.append(anchor)

顔アンカー

【顔アンカーにガイコツを配置】
Jun-13-2021 15-23-18

【実装方法】

顔がターゲットのアンカー・エンティティをつかうには、ARFaceTrackingConfiguration で ARView のセッションを実行します。

let config = ARFaceTrackingConfiguration() 
arView.session.run(config, options: [])
// この構成でセッションを実行すると、フロントカメラに切り替わります。

let anchor = AnchorEntity(.face)
let usdzModel = try! Entity.loadModel(named: "skull")
anchor.addChild(usdzModel)
arView.scene.anchors.append(anchor)

デフォルトでは顔のジオメトリがオクルージョンされ、アンカーが顔に追従します。

からだアンカー

【からだアンカーの1m上に飛行機のモデルを配置】
// Jun-13-2021 21-46-28

【実装方法】

体アンカーをつかうには、ARBodyTrackingConfiguration で ARView のセッションを実行します。

let config = ARBodyTrackingConfiguration()
arView.session.run(config, options: [])

let anchor = AnchorEntity(.body)

let usdzModel = try! Entity.load(named: "toy_biplane")
usdzModel.position = simd_make_float3(0, 1, 0)
anchor.addChild(usdzModel)
arView.scene.anchors.append(anchor)

ARアンカー

ARKit の AR アンカーからアンカー・エンティティをつくれます。
体アンカーに相対する頭や手の位置、顔アンカーの表情の動きなど、AR アンカーのプロパティにアクセスすることもできます。
AR アンカーからつくったアンカー・エンティティは、 AR アンカーの位置のアップデートに追従します。

let arAnchorEntity = AnchorEntity(anchor: arAnchor)

6.アニメーション

移動、回転、拡大縮小をアニメーションできます。
USDZに組み込まれたアニメーションを再生することもできます。

移動

【横に移動】
Jun-13-2021 13-06-33

【実装方法】

let anchor = AnchorEntity(world: simd_make_float3(0, 0, -0.2))
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.02, 0.01, 0.01),cornerRadius: 0.001))
anchor.addChild(box)
arView.scene.anchors.append(anchor)

let newTranslation = simd_make_float3(anchor.transform.translation.x+0.05,
                                      anchor.transform.translation.y,
                                      anchor.transform.translation.z)
 // 移動先は、アンカーの元の位置から5cm右に移動した位置
let moveController = anchor.move(to: Transform(scale: simd_make_float3(1, 1, 1), // スケールは変えない
                          rotation: anchor.orientation, // 角度も変えない
                          translation: newTranslation), // 位置だけ置き換える
            relativeTo: anchor, // アンカーが属する座標系において動かす
            duration: 3, // 3秒で動かす
            timingFunction: .linear) // 線形アニメーション(.easeInOut などのカーブも設定可能)

 // relativeTo: を nil にすると、ワールド座標系の変換になります。
 // *アンカー・エンティティを動かしていますが、box のモデル・エンティティを .move することもできます。
 // *scaleやrotationは変えないのであれば、省略することもできます。

回転

【150度回転】
Jun-13-2021 13-10-48

【実装方法】

let rotation:Float = 150 * .pi / 180
anchor.move(to: Transform(pitch: 0, yaw: 0, roll: rotation), // z軸を中心に、反時計回りに150度
            relativeTo: anchor,
            duration: 1)

拡大・縮小

【3倍に拡大】
Jun-13-2021 22-18-33.gif

【実装方法】

let scale = simd_make_float3(3, 3, 3) // 3倍のスケールにする
anchor.move(to: Transform(scale: scale, rotation: anchor.orientation, translation: anchor.transform.translation),
            relativeTo: anchor,
            duration: 1)

*RealityKit2 から .move メソッドは .moveCharacter メソッドにかわるようです。

USDZアニメーション

【USDZ ファイルにベイクされたアニメーションを再生】
Jun-13-2021 21-39-29

Entity.load() で アニメーションごとUSDZ をよみこみ、シーンにエンティティを追加したあとにアニメーションを再生します。

【実装方法】

let anchor = AnchorEntity()
anchor.position = simd_make_float3(0, 0, -0.5)
let usdzModel = try! Entity.load(named: "toy_biplane")

 // Entity.loadModel() ではアニメーションがロードされないので注意!

anchor.addChild(usdzModel)
arView.scene.anchors.append(anchor)

 // シーンにエンティティを加えたあとに再生しないと、再生されないので注意!

for animation in usdzModel.availableAnimations {
    usdzModel.playAnimation(animation.repeat())
}

GIF なのでカクついていますが、実際はなめらかに再生されます。

7.照明

RealityKit はデフォルトで環境の明るさを反映しますが、3種類のライト・エンティティ(ライト・コンポーネント)を加えることもできます。

ポイント・ライト

全方向におなじ光を放つライト。
家の照明みたいなもの。

【箱の少し手前にポイント・ライトを配置】
point.png

【実装方法】

let lightEntity = PointLight()
// 以下 .light でライト・コンポーネントの設定ができる
lightEntity.light.color = .red 
lightEntity.light.intensity = 1000 // ルーメン単位でライトの強さを設定
lightEntity.light.attenuationRadius = 0.5 // ライトが及ぶ半径
lightEntity.look(at: [0,0,0], from: [0,0,0.1], relativeTo: lightAnchor)
 // ライトの位置を10cm後ろに

let lightAnchor = AnchorEntity(plane: .horizontal)
lightAnchor.addChild(lightEntity)
arView.scene.anchors.append(lightAnchor)

ダイレクショナル・ライト

指向性ライト。ある方向に均一な光を放つ。

【箱の少し手前に指向性ライトを配置】
directional.png

【実装方法】

let lightEntity = DirectionalLight()
lightEntity.light.color = .red
lightEntity.light.intensity = 1000
lightEntity.light.isRealWorldProxy = true
lightEntity.shadow?.maximumDistance = 10
lightEntity.shadow?.depthBias = 5

let lightAnchor = AnchorEntity(plane: .horizontal)
lightAnchor.addChild(lightEntity)
arView.scene.anchors.append(lightAnchor)

スポット・ライト

円錐形に照らすライト。
舞台照明でよくあるやつ。

【箱の少し手前にスポット・ライトを配置】
spot.png

【実装方法】

let lightEntity = SpotLight()
lightEntity.light.color = .red
lightEntity.light.intensity = 3000
lightEntity.look(at: [0,0,0], from: [0,0.05,0.3], relativeTo: lightAnchor)
lightEntity.shadow = SpotLightComponent.Shadow() // デフォルトではシャドウがない
// 以下デフォルト値
lightEntity.light.innerAngleInDegrees = 45
lightEntity.light.outerAngleInDegrees = 60 // これら2つでスポットライトの円の広さと集中度が決まるはず
lightEntity.light.attenuationRadius = 10

let lightAnchor = AnchorEntity(plane: .horizontal)
lightAnchor.addChild(lightEntity)
arView.scene.anchors.append(lightAnchor)

8.物理作用

ぶつかって跳ね返ったり、重力を受けたり、といった物理作用を表現できます。

【物理作用をつけた板と球体】
Jun-15-2021 02-53-42

【実装方法】

エンティティに物理ボディと衝突形状をつけます。


let horizontalPlane = ModelEntity(mesh: .generateBox(size: [0.2,0.003,0.2]),
                                             materials: [SimpleMaterial(color: .white,
                                             isMetallic: true)])

horizontalPlane.physicsBody = PhysicsBodyComponent(massProperties: .default, // 質量
                                                   material: .generate(friction: 0.1, // 摩擦係数
                                                                       restitution: 0.1), // 衝突の運動エネルギーの保存率
                                                   mode: .kinematic)     
 // .kinematic モードで物理ボディをつける

horizontalPlane.generateCollisionShapes(recursive: false)
 // 衝突形状をつける。子ノードまで recursive につけることも可能

worldAnchor.addChild(physicalSphere)

// 添付Gif の verticalPlane も同様です

let colors:[UIColor] = [.red,.orange,.magenta,.yellow,.purple,.white]
for index in 0...5 {
    let physicalSphere = ModelEntity(mesh: .generateSphere(radius: 0.01),
                                     materials: [SimpleMaterial(color: colors[index],
                                     isMetallic: true)])
    physicalSphere.physicsBody = PhysicsBodyComponent(massProperties: .default,
                                                   material: .generate(),
                                                   mode: .dynamic) 
     // .dynamic モードで物理ボディをつける

    physicalSphere.generateCollisionShapes(recursive: false)
    // 衝突形状をつける
    worldAnchor.addChild(physicalSphere)
}
        
arView.scene.anchors.append(model.worldAnchor)

PhysicsBodyMode

.dynamic : 動いて他の dynamic ボディに力をあたえることができる。自身も受けた力による運動をする。
.kinematic : 動いて他の dynamic ボディに力をあたえることができる。自身は受けた力による運動をしない。
.static : 動かない。他の dynamic ボディに力をあたえるのは衝突された時。自身は受けた力による運動をしない。

※ .dynamic ボディを持つエンティティは、支えがないと重力を受けて落ちていきます。

衝突検知

CollisionComponent をつけたエンティティどうしの衝突を検知できます。

【球体と板の衝突をトリガーにビデオ・マテリアルを再生】
Jun-21-2021 05-38-29

【実装方法】

Combine で CollisionEvents をリッスンします。

import Combine

var collisionSubscribing:Cancellable? // イベントをリッスンするために参照保持が必要だそうです
collisionSubscribing = arView.scene.subscribe(to: CollisionEvents.Began.self) { event in
    print("collision !")
    let entityA = event.entityA as? ModelEntity
    let entityB = event.entityB as? ModelEntity 
     // 衝突したエンティティ
}

特定のエンティティの衝突を検知する場合は、

collisionSubscribing = arView.scene.subscribe(to: CollisionEvents.Began.self,
                                              on: specifiedEntity) { event in
    print("collision of the specified entity ! ")
}

※エンティティが PhysicsBodyComponent を持っている場合は、衝突するエンティティのどちらかが.dynamic タイプのボディを持っていないと検知しません。PhysicsBodyComponent を持っていない場合は CollisionComponent のみで検知できます。

ちなみに、 ARView のセッションを終了して他の View に遷移するときは、 Cancellable をキャンセルしておかないと、ポインタが解放されずメモリ使用量が増えてしまうので、明示的にキャンセルします。

collisionSubscribing.cancel()

衝突フィルター

9.よりリアルな現実世界との相互作用

Lidar 搭載のデバイスでは、現実の物理形状が細かくとれるので、以下の効果をつけて AR をよりリアルに見せられます。

オクルージョン

現実の物体の背後に ARオブジェクトが隠れます。

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

AR オブジェクトが現実の床に影を落とします

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

物理演算

AR オブジェクトが、現実のオブジェクトと物理的にインタラクションします。

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

衝突

現実の物体との衝突を検知します。

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

10.オーディオ

音声の再生

【実装方法】

AudioFileResourceで音源を読み込み、エンティティのprepareAudioメソッドに渡すと、AudioPlaybackControllerを返すので、AudioPlaybackControllerで再生します。

do {
    let audioResource = try AudioFileResource.load(named: "audio.mp3", // バンドル・メインから読み込む
                                                   in: nil,
                                                   inputMode: .spatial, // デバイスの位置によって聞こえ方が変わる
                                                   loadingStrategy: .preload, // .stream でリアルタイムで読み込むことも可能
                                                   shouldLoop: true) // ループさせる

    let audioPlaybackController = entity.prepareAudio(audioResource)
        audioPlaybackController.play()            
} catch {
    print("Error loading audio file")
}

AudioFileResource.load(contentOf:URL ...) にすることで、URL からも音源を読み込めます。

AudioFileResource.InputMode
モード 説明
nonSpatial  位置に関係なく同じ聞こえ方
spatial  デバイスとエンティティの距離と方向の関係によって聞こえ方が変わる
ambient  デバイスとエンティティの方向の関係によって聞こえ方が変わる

11.タップとジェスチャー

タップ

ARView はユーザーのタップの先にあるエンティティを検出できます。

【エンティティをタップしたときに、力を加える】
Jun-17-2021 11-11-04.gif

【実装方法】

1、UITapGestureRecognizer で ARView へのユーザーのタップを検出し、その先にあるエンティティを取得する。

@IBAction func onTap(_ sender: UITapGestureRecognizer) {
    let tapLocation = sender.location(in: arView)

    if let tappedEntity = arView.entity(at: tapLocation) as? ModelEntity {
        tappedEntity.addForce([0,100,-100], relativeTo: nil)
         // y軸、z軸方向に100ニュートンの力をタップしたエンティティに加える
    }
}

2、ヒットテストで検出するエンティティには物理シェイプが必要です。

let tappableEntity = ModelEntity(mesh: .generateBox(size: 0.1, cornerRadius: 0.02))
tappableEntity.generateCollisionShapes(recursive: true)
 // recursive に子エンティティにも物理シェイプをつけられる

Ray Cast による平面との交点検出

タップした場所の先にある平面との交点を検出できます。

@objc func tapped(sender: UITapGestureRecognizer){
    let location = sender.location(in: arView)
    let results = self.raycast(from: location, allowing: .estimatedPlane, alignment: .any)
     // 水平・垂直両方の平面を検出する設定で、ARView上のタップした場所からレイ・キャストする 

    if let firstResult = results.first {
        let anchor = ARAnchor(name: "Anchor for object placement", transform: firstResult.worldTransform)
         // rayCastQuery の結果の交点座標で AR アンカーをつくる
        arView.session.add(anchor: anchor) // シーンに AR アンカーを加える

        let anchorEntity = AnchorEntity(anchor: anchor)  // AR アンカーからアンカー・エンティティをつくる
        let modelEntity = ModelEntity(mesh: generateBox(size: 0.05))
        modelEntity.model?.materials = [SimpleMaterial(color: .white, isMetallic: true)]
        anchorEntity.addChild(modelEntity)
        arView.scene.addAnchor(anchorEntity)
    }
}

// エンティティを平面に平行に配置したい場合は、 allowing を existingPlaneGeometry にする
// let results = arView.raycast(from: location, allowing: .existingPlaneGeometry, alignment: .any)

ジェスチャーで移動、回転、拡大・縮小

ARView に組み込まれている エンティティ用のジェスチャ・リコナイザー( UIGestureRecognizer のサブクラス)をエンティティごとに ARView にインストールすることで、エンティティを

パン(ドラッグ)・ジェスチャーで 移動( X*Z 平面 )
2本指で円を描くジェスチャーで 回転( Y軸 )、
ピンチ・ジェスチャーで 拡大・縮小

ができます。

【ジェスチャーで移動・回転・拡大・縮小するボックス】
Jun-27-2021 22-38-42.gif

【実装方法】

モデル・エンティティに衝突形状をつけて、ジェスチャをインストールします。

modelEntity.generateCollisionShapes(recursive: true)
arView.installGestures(.all, for: modelEntity)

※エンティティに .dynamic タイプの PhysicsBodyComponent をつけると、移動と回転ジェスチャーが機能しなくなります(スケールのみ機能)。

上記のインストール手順のみで、移動、回転、拡大・縮小がモデル・エンティティに反映されますが、 他の GestureRecognizer 同様、 @objc func でメソッドを追加して、エンティティの移動距離やスケール量などを取れます。

arView.installGestures(.all, for: modelEntity).forEach { recognizer in
    recognizer.addTarget(self, action: #selector(handleGesture(sender:)))
}

@objc func handleGesture(sender: UIGestureRecognizer){
    if let panGesture = sender as? EntityTranslationGestureRecognizer {
        // panGesture.state
        // panGesture.entity
        // panGesture.translation(in: Entity?) -> SIMD3<Float>?
        // などがとれる。
    }
}

12.カスタム・コンポーネント

コンポーネント・プロトコルに準拠したストラクトをつくって、Entity にロジックを持たせることができます。

【実装方法】

struct CardComponent: Component, Codable {
    var flipUp = false 
}

let cardEntity = try! Entity.loadMode(named: "card")
cardEntity.components[CardComponent.self] = CardComponent()
cardEntity.components[CardComponent.self]?.flipUp = true

13.複数のデバイスで共有する

複数のデバイスで AR 画面を見たときに、同じ位置にエンティティがあったり、同じアクションをしたりして、AR 世界を共有できます。

【iPod Touch の操作を iPhone に反映】
Jun-19-2021 09-30-36

Collaborative Session

アンカーの位置やエンティティ・コンポーネントの状態、物理状態などを複数デバイスで即時共有できます。

共有サービスにはいろんなネットワークが使えますが、ここではアップルの MultiPeerConnectivity フレームワークをつかいます。

MultiPeerConnectivity についてはこちら。

【実装方法】

1、Local Network (Bonjour services)をアプリに追加

Info.plistにLocal Network Usage DescriprtionとBonjour servicesを追加します。

スクリーンショット 2021-05-27 4.04.33.png

Info.plist のBonjour services に自分のサービス名をStringで記述します。
このサービス名が、そのアプリのやりとりの識別子になります。
Bonjourサービス名はASCII小文字、数字、およびハイフン15文字以内である必要があります。
仮に、"my-mc-service"とします。

Info.plist
<key>NSBonjourServices</key>
    <array>
        <string>_my-mc-service._tcp</string> // サービス名とtcpの前に_(アンダーバー)を入れます
    </array>
2、他のピアと接続する
import MultipeerConnectivity

class ViewController: UIViewController, MCSessionDelegate, MCNearbyServiceAdvertiserDelegate, MCNearbyServiceBrowserDelegate {

    static let serviceType = "my-mc-service"
    var session: MCSession!
    let myPeerID = MCPeerID(displayName: UIDevice.current.name)
    var serviceAdvertiser: MCNearbyServiceAdvertiser!
    var serviceBrowser: MCNearbyServiceBrowser!
    var connectedPeers: [MCPeerID] {
        return session.connectedPeers
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required)
        session.delegate = self
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: ViewController.serviceType)
        serviceAdvertiser.delegate = self
        serviceAdvertiser.startAdvertisingPeer()
        
        serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: ViewController.serviceType)
        serviceBrowser.delegate = self
        serviceBrowser.startBrowsingForPeers()

        arView.scene.synchronizationService = try? MultipeerConnectivityService(session: session)
    }

    //MARK:- delegate

    func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
        
    }
    
    func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
        
    }
    
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, self.session)
        print(peerID)

    }
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
        print(peerID)
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        
    }
    
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
    }
    
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {

    }
    
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
    }
    
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
    }
    
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
    }
}
3、Collaborative Session を開始
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let config = ARWorldTrackingConfiguration()
    config.isCollaborationEnabled = true
    arView.session.run(config, options: [])
}

これで複数のデバイスでアンカーやエンティティが共有されます。

Collaborative Session をなるべく早く有効に、また正確に位置を共有するコツ

・デバイス同士をなるべく近く、似た角度に向けて同じ風景を見られるようにする
・デバイスを動かして、周囲を認識させ、 ARFrame.WorldMappingStatus を .mapped にする
・アンカーの原点と子エンティティのローカル距離を近くしておく(ずれが少なくなる)
・なるべくエンティティごとにこまめにアンカーを作成する
・ただし、エンティティ同士の相対距離をなるべく正確に保ちたい場合は、一つのアンカーにアタッチする
・デリゲートメソッド内でARアンカー位置のアップデートを受け取り、アンカーエンティティの位置を更新する

エンティティの変更権限

エンティティの変更を他のデバイスに反映させられるのは、エンティティのオーナーだけです。
エンティティの所有者とは、エンティティを作成したデバイスを意味します。
オーナーシップを譲渡することにより、元のオーナーでないデバイスからもエンティティの変更を全体に反映できます。
所有権を引き継ぐために、元の所有者ではないデバイスが所有権要求を送信します。
【実装方法】

entity.requestOwnership { result in
    if result == .granted { // 許可

    } else { // 拒否

    }
}

エンティティのオーナーは、オーナーシップを要求されたときに許可するかどうかを設定できます。

entity.synchronization?.ownershipTransferMode = .autoAccept // 許可
entity.synchronization?.ownershipTransferMode = .manual // 拒否

共有セッション中に、特定のエンティティを共有したくない場合

hidingEntity.synchronization = nil

これでこのエンティティは自分のデバイスにのみ表示されます

他のデバイスの位置アンカー

ARParticipantAnchorは、他のデバイスの位置と、 そのデバイス固有のAR セッションの ID を取得できます。

【iPod Touch の ParticipantAchor にテキスト・エンティティをアタッチ】
Jun-20-2021 08-54-51

【実装方法】

func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
    for anchor in anchors {
        if let participantAnchor = anchor as? ARParticipantAnchor {
            let anchorEntity = AnchorEntity(anchor: participantAnchor)
            arView.scene.addAnchor(anchorEntity)
        }
    }
}

MultipeerConnectivityでAR セッションの ID を交換して peerID(ディスプレイ・ネームをプロパティとしてもつ) にひもづけておくと、アンカーのデバイスの ディスプレイ・ネームが識別できます。

14.RealityComposerで手軽にシーンをつくる

グラフィック・ユーザー・インターフェイスで AR のシーンをつくって、アプリに組み込むことができます。
オブジェクト、アンカー、アニメーション、音声、などをシーンに追加し、.rcproject ファイルとして xcode プロジェクトで読み込めます。

【Reality Composer 内でシーンを再生】
Jun-17-2021 10-14-14.gif

【Reality Composer でつくったシーンをARView で再生】
Jun-19-2021 13-42-08.gif

Reality Composer でシーンをつくる

Reality Composer プロジェクトは、Xcode 右クリック → Open Developer Tool
もしくは、Xcode の New → File から作成できます。
また、Xcode にバンドルした .rcproject ファイル画面で Open in Reality Composer をクリックするとReality Composer でファイルが開け、Reality Composer での編集内容が Xcode のプロジェクトにも即時反映されます。

コンテンツを配置するアンカーを選択できます。
(シーンごとに選択できるアンカーは一つ)

【Reality Composer で選べるアンカー】
スクリーンショット 2021-06-15 15.38.13.png

プリセットのモデルを追加できます。
USDZ モデルも、ドラッグ&ドロップで追加できます。

【Reality Composer で選べるプリセットのシェイプ】
スクリーンショット 2021-06-15 15.39.28.png

モデルの位置や大きさ、角度、表面の色、質感、物理特性、衝突特性を設定できます。

【Reality Composer のオブジェクトの設定ペイン】
スクリーンショット 2021-06-17 9.46.50.png

アニメーションなど、オブジェクトの挙動をシーケンスで設定できます。
以下の挙動を設定できます。

強調(宙返りなどコミカルなアニメーション)
表示(フェードインなど設定可能)
隠す(フェードアウトなど設定可能)
移動/回転/拡大・縮小(絶対・相対)
力を加える(物理作用を計算してくれる)
オービット(他のオブジェクトの周りをまわる)
シーンを変更
サウンドを再生(プリセット・サウンドがダウンロードできます)
環境音を再生
ミュージックを再生
待機
USDZ アニメーション再生
Xcode で通知アクションをする

各ビヘイビア・シーケンスに以下の開始トリガーを設定できます。

タップ
シーン開始
カメラが近づいたとき
オブジェクトの衝突
コードからの通知

【Reality Composer のビヘイビア設定】
スクリーンショット 2021-06-17 10.00.18.png

【ビヘイビア・シーケンスを再生して確認】
Jun-17-2021 10-14-14.gif

Xcode で Reality Composer プロジェクトファイルを読み込む

以下でシーンが再生されます。

let sceneElement = try! RealityComposerProject.loadScene()
arView.scene.anchors.append(sceneElement)
 // .rcproject ファイル名: RealityComposerProject
 // Scene: シーン名

【Reality Composer で作ったシーンを ARView で再生】
Jun-19-2021 13-42-08.gif

Reality Composer シーンのエンティティにアクセスする

Reality Composer のプロパティ設定でエンティティの name を設定してアクセスします。

let sceneElement = try! RealityComposerProject.loadScene()
let box:Entity = sceneElement.box
let robot:Entity = sceneElement.robot

コードからビヘイビアをトリガーする

Reality Composer のビヘイビア設定から通知を選択します。
コードからビヘイビアの name でアクセスします。

let sceneElement = try! RealityComposerProject.loadScene()
sceneElement.notifications.behavior.post()
 // "behavior"という名前のビヘイビアを発火する

15.イベントを検知する

あらかじめ ARView.Scene で特定のイベントをサブスクライブ(購読)することで、イベント発生時にコードを実行できます。
たとえば、アンカーがシーンに固定されたことを受信するには以下。

import Combine

var cancellables: [Cancellable] = []

let isActiveSub = arView.scene.subscribe(to: SceneEvents.AnchoredStateChanged.self, on: anchorEntity, { event in
    if event.isAnchored {
          print("anchorEntity is active!")
    }
})
cancellables.append(isActiveSub)

RealityKit においては以下のイベントが受けとれます。

Scene Event

種類 説明
SceneEvents.Update   フレーム間隔ごとに1回トリガーされるイベントで、フレームごとにカスタムロジックを実行するために使用できます。
SceneEvents.AnchoredStateChanged   アンカーエンティティのアンカー状態が変更されたときにトリガーされるイベント。
AnimationEvents.PlaybackCompleted   アニメーションがその期間の終わりに達したときに発生するイベント。
AnimationEvents.PlaybackLooped  アニメーションがループしたときに発生するイベント。
AnimationEvents.PlaybackTerminated  イベントが完了したかどうかに関係なく、イベントが終了したときに発生するイベント。
AudioEvents.PlaybackCompleted  オーディオの再生が完了したときに発生するイベント。
CollisionEvents.Began   2つのオブジェクトが衝突したときに発生するイベント。
CollisionEvents.Updated  2つのオブジェクトが接触しているときに、すべてのフレームで発生するイベント。
CollisionEvents.Ended   以前に接触していた2つのオブジェクトが分離したときに発生するイベント。

【画像アンカーがアクティブになったら、ポップアップする絵本】

Jul-06-2021 11-33-48

Combine をつかって、ARView でイベントを受信できます。

【実装例】

import Combine

var sub:Cancellable! // クラス変数として参照する必要がある
var animated = false
        
sub = arView.scene.subscribe(to: SceneEvents.AnchoredStateChanged.self, on: sceneElement, { event in
    if event.anchor.isActive, !self.animated {
     // アンカーがアクティブになったら少し待ってポップアップする
       self.animated = true
        Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { timer in
          popUpBox.move(to: Transform(translation: [0, 0.2, 0]), relativeTo: popUpBox, duration: 3, timingFunction: .easeInOut)
      }
   }
})

Cancellable に当てたイベント検知は明示的にキャンセルするまで、参照が残ってメモリを圧迫するので、使用後はキャンセルします。

sub.cancel()

16.ARKitとRealityKitの関係と注意点

RealityKit は ARKit をベースに構築されており、 RealityKit 単体でも利用できますが、 各種の Tracking Configuration や ARSessionDelegate をつかう場合は、明示的に ARKit をインポートしてセッションを構成・実行する必要があります

その際、いくつか注意すべきことがあります。

テクスチャの光の反射

ARKit の WorldTrackingConfiguration をデフォルトの構成でつかうと、RealityKit のマテリアルの反射が暗くなります。

【RealityKit 単体】
IMG_5648.PNG

【WorldTrackingConfigration でセッションを実行】
IMG_5649.PNG

これを防ぐには、 ARWorldTrackingConfiguration の environmentTexturing を .automatic に設定します。

let config = ARWorldTrackingConfiguration()
config.environmentTexturing = .automatic

平面検出の設定

ARKit で明示的に WorldTrackingConfiguration をつかって、 RealityKit で ターゲット plane のアンカー・エンティティをつかう場合、 WorldTrackingConfiguration の planeDetection を設定しないと、平面にアンカー・エンティティを設置できません。RealityKit 単体使用の場合はこの設定は不要ですが、 ARKit も併用して設定する場合は忘れず設定する必要があります。

let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal

17.RealityKitをつかうメリット

iOS でARアプリを作るにはいくつか選択肢がありますが、RealityKitには以下のメリットがあります。

・アップルの公式かつ最新のフレームワークである
・アップデートとサポートが手厚い
・あつかいやすい
・レンダリングがきれいで、リッチなARコンテンツを表示できる

ぼくはこれまではARKit+SceneKitでARアプリを作っていましたが、SceneKit は新しい機能追加は止まっているので、この機会にRealityKitをひととおり試してみました。
RealityKitはSceneKitに比べ細かい融通がきかないようなイメージがありましたが、やってみるとぜんぜんいろいろできました。
iOSでARアプリを作る場合の第一候補になると思います。

18.RealityKitのサンプルコード集

RealityKit のサンプルコード集 「RealityKit-Sampler」 をオープンソースとして公開しました。
GitHub でソースコードを手に入れられます。Xcode でビルドできます。

RealityKit の機能をわかりやすく盛り込んだサンプルコードの集まりです。

RealityKit-Sampler

Put A Box Giant Robot 100inch-Monitor Building blocks
(https://github.com/john-rocky/RealityKit-Sampler)
Speech Balloon Special Move Face Cropper AR Hockey
Hand Interaction

学べること

コンテンツ 技術要素
Put the box SwiftUIのARView, Scene, Entity, Anchor, MeshResource, Material.
Big Robots USDZ, Animation
Big Monitor VideoMaterial, SceneEvent
Building Block Ray Cast, Hit Test, Handle Gestures, Physics, Collision, TextureResource
Speech Balloon Face Anchor, ARSessionDelegate, RealityComposer
Special Move Body Anchor
Face Cropper Image Anchor
AR Hockey Collaborative Session
Hand Interaction addForce, Vision

🐣


こんにちは、だいすけです。
ぼくはフリーランス・エンジニアで、 AR や 機械学習のアプリの実装をしています。
お仕事のご相談こちらまで。
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

85
70
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
85
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?