#まえがき
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のサンプルコード集
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として表示する方法
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) {
}
}
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として表示する方法
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 {
}
}
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を表示できます。
ボックス
【ボックスメッシュ】
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にアンカーを加える
板
【プレイン・メッシュ】
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)
球体
【球体メッシュ】
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)
テキスト
【テキスト・メッシュ】
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 の複雑な形状を表示できます。
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.表面を加工する
オブジェクトの表面の色・質感・模様をつけます。
シンプル・マテリアル
反射など現実の光に影響されるマテリアル
【ブルー・メタリックのシンプル・マテリアル】
【実装方法】
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] // ボックスの表面に適用する
アンリット・マテリアル
物理レンダリングの影響を受けないマテリアル。明るさに影響されないので、暗い場所でもはっきり見える。
【ブルーのアンリット・マテリアル】
【実装方法】
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オブジェクトを透過するマテリアル。
【ロボットの手前にオクルージョン・ボックスを配置】
【実装方法】
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.03, 0.01, 0.02)))
let occlusionMaterial = OcclusionMaterial()
box.model?.materials = [occlusionMaterial]
画像テクスチャ
【実装方法】
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で作成したメッシュにコードからテクスチャを貼ろうとすると、画像の向きが逆になったり、画像の一部分しか貼れなかったりします。
動画マテリアル
*メインバンドルに動画ファイルを置き、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に青色のシンプル・マテリアルを貼り付け】
【実装方法】
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+動画
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向こうに配置
【ローカル座標】
*ローカル座標の原点はワールド座標の原点とは異なる(親エンティティがルートシーンの場合は一致する)
ワールド座標の中の位置を設定(アプリ起動時のカメラ位置が原点)
let worldOriginAnchor = AnchorEntity(world: [0,0,0])
arView.scene.addAnchor(worldOriginAnchor)
entity.position = worldOriginAnchor.convert(position: [-1,1,-2], to: entity)
// ワールド座標の原点(デバイスの初期位置)から1m左、1m上、2m向こうに配置
角度
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を配置できます。
カメラが動いても、オブジェクトはアンカーにとどまります。
アンカーが動くと、オブジェクトは追従します。
アンカーを検出しだい、モデル・エンティティが出現します。
水平面アンカー
机や床などをアンカーにできます。
【水平面アンカーで机を検出してボックスを配置】
【実装方法】
平面アンカーを使用する場合は、 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)
垂直面アンカー
壁やドアなどをアンカーにできます。
【ディスプレイに垂直面アンカーでボックスを配置】
【実装方法】
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)
画像アンカー
ポスターや雑誌の表紙などをアンカーにできます。
【実装方法】
アンカーにしたい画像を登録します。
1、「Assets.xcassets」 → 左下の「+」ボタン → 「AR and Textures」 → 「AR Resource Group」 で ARリソースのフォルダをつくり、アンカーにしたい画像を ドラッグ & ドロップする。
2、アンカー画像をクリックし、右側のペインでアンカー画像の幅と高さを登録する。
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 パソコン画面から飛び出すビデオ】
動画の最初のフレームを画像アンカーとして設定して、アンカー画像と同じサイズのボックス・エンティティに動画を貼り付けて再生しています。
【実装方法】
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() // 動画を再生
}
}
}
オブジェクト・アンカー
事前にスキャンしたオブジェクトをアンカーにできます。
【実装方法】
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 ファイルをつくることができます。
2、生成された .arobject ファイルを AR and Textures のリソース・グループに登録する(手順は画像アンカーと同じ)
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)
半透明の球体で包んでみました。
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)
顔アンカー
【実装方法】
顔がターゲットのアンカー・エンティティをつかうには、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)
デフォルトでは顔のジオメトリがオクルージョンされ、アンカーが顔に追従します。
からだアンカー
【実装方法】
体アンカーをつかうには、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に組み込まれたアニメーションを再生することもできます。
移動
【実装方法】
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は変えないのであれば、省略することもできます。
回転
【実装方法】
let rotation:Float = 150 * .pi / 180
anchor.move(to: Transform(pitch: 0, yaw: 0, roll: rotation), // z軸を中心に、反時計回りに150度
relativeTo: anchor,
duration: 1)
拡大・縮小
【実装方法】
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アニメーション
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種類のライト・エンティティ(ライト・コンポーネント)を加えることもできます。
ポイント・ライト
全方向におなじ光を放つライト。
家の照明みたいなもの。
【実装方法】
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)
ダイレクショナル・ライト
指向性ライト。ある方向に均一な光を放つ。
【実装方法】
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)
スポット・ライト
円錐形に照らすライト。
舞台照明でよくあるやつ。
【実装方法】
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.物理作用
ぶつかって跳ね返ったり、重力を受けたり、といった物理作用を表現できます。
【実装方法】
エンティティに物理ボディと衝突形状をつけます。
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 をつけたエンティティどうしの衝突を検知できます。
【実装方法】
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 はユーザーのタップの先にあるエンティティを検出できます。
【実装方法】
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軸 )、
ピンチ・ジェスチャーで 拡大・縮小
ができます。
【実装方法】
モデル・エンティティに衝突形状をつけて、ジェスチャをインストールします。
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 世界を共有できます。
Collaborative Session
アンカーの位置やエンティティ・コンポーネントの状態、物理状態などを複数デバイスで即時共有できます。
共有サービスにはいろんなネットワークが使えますが、ここではアップルの MultiPeerConnectivity フレームワークをつかいます。
MultiPeerConnectivity についてはこちら。
【実装方法】
1、Local Network (Bonjour services)をアプリに追加
Info.plistにLocal Network Usage DescriprtionとBonjour servicesを追加します。
Info.plist のBonjour services に自分のサービス名をStringで記述します。
このサービス名が、そのアプリのやりとりの識別子になります。
Bonjourサービス名はASCII小文字、数字、およびハイフン15文字以内である必要があります。
仮に、"my-mc-service"とします。
<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 にテキスト・エンティティをアタッチ】
【実装方法】
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 でつくったシーンをARView で再生】
Reality Composer でシーンをつくる
Reality Composer プロジェクトは、Xcode 右クリック → Open Developer Tool
もしくは、Xcode の New → File から作成できます。
また、Xcode にバンドルした .rcproject ファイル画面で Open in Reality Composer をクリックするとReality Composer でファイルが開け、Reality Composer での編集内容が Xcode のプロジェクトにも即時反映されます。
コンテンツを配置するアンカーを選択できます。
(シーンごとに選択できるアンカーは一つ)
プリセットのモデルを追加できます。
USDZ モデルも、ドラッグ&ドロップで追加できます。
【Reality Composer で選べるプリセットのシェイプ】
モデルの位置や大きさ、角度、表面の色、質感、物理特性、衝突特性を設定できます。
【Reality Composer のオブジェクトの設定ペイン】
アニメーションなど、オブジェクトの挙動をシーケンスで設定できます。
以下の挙動を設定できます。
強調(宙返りなどコミカルなアニメーション)
表示(フェードインなど設定可能)
隠す(フェードアウトなど設定可能)
移動/回転/拡大・縮小(絶対・相対)
力を加える(物理作用を計算してくれる)
オービット(他のオブジェクトの周りをまわる)
シーンを変更
サウンドを再生(プリセット・サウンドがダウンロードできます)
環境音を再生
ミュージックを再生
待機
USDZ アニメーション再生
Xcode で通知アクションをする
各ビヘイビア・シーケンスに以下の開始トリガーを設定できます。
タップ
シーン開始
カメラが近づいたとき
オブジェクトの衝突
コードからの通知
Xcode で Reality Composer プロジェクトファイルを読み込む
以下でシーンが再生されます。
let sceneElement = try! RealityComposerProject.loadScene()
arView.scene.anchors.append(sceneElement)
// .rcproject ファイル名: RealityComposerProject
// Scene: シーン名
【Reality Composer で作ったシーンを ARView で再生】
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つのオブジェクトが分離したときに発生するイベント。 |
【画像アンカーがアクティブになったら、ポップアップする絵本】
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 のマテリアルの反射が暗くなります。
【WorldTrackingConfigration でセッションを実行】
これを防ぐには、 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を使ったアプリを作っています。
機械学習関連の情報を発信しています。