見出し画像

Vision Proアプリ開発入門:SwiftUIとRealityKitで作る跳ねる球体を作成する

ついにその日がやってきます!2024年6月28日、Vision Proが日本で発売開始。この日本発売を記念して、Vision Proアプリ開発入門を無料で公開します。

次のAnimation Gifはこれから作るアプリの起動時の画面です。

Vision Proシミュレーター

Vision Proの登場は、単なる新しいデバイスの登場にとどまらず、私たちのデジタル体験を根本から変える革命の始まりです。そして、開発者であるあなたも、その革命の波に乗り遅れるわけにはいきません。

ご安心ください。Appleシリコン搭載のMacと、ほんの少しのSwiftUIとRealityKitの知識さえあれば、誰でも簡単にvisionOSアプリを開発できます。

この記事では、Vision Proアプリ開発の第一歩として、跳ねる球体を使ったシンプルなジェネラティブアートアプリを作っていきます。複雑なコードは必要ありません。この記事を読み終える頃には、あなたもVision Proの世界に飛び込む準備が整っているはずです。さあ、一緒に未来を創造しましょう!


Vision Proアプリ開発を始めよう:跳ねる球体で作るジェネラティブアート

Apple Vision Pro,  Appleが満を持して送り出すこの革新的なデバイスは、私たちのデジタル体験を根底から覆す可能性を秘めています。それは、現実世界とデジタル世界を融合させる、空間コンピューティングの幕開けを告げるものです。

Vision Proが切り開く未来は、開発者であるあなたの手に委ねられています。想像してみてください。現実空間に3Dオブジェクトが浮かび上がり、インタラクティブなコンテンツが日常を彩る世界を。教育、医療、エンターテイメントなど、あらゆる分野でイノベーションが加速していくでしょう。

SwiftUIとRealityKit:アプリ開発の強力なツール

Appleは、開発者がVision Proの可能性を最大限に引き出せるよう、パワフルなツールを提供しています。それが、SwiftUIとRealityKitです。

SwiftUIは、宣言的な構文でユーザーインターフェースを構築できる、Appleが誇る最新のフレームワークです。直感的で簡潔なコードで、美しく、そして複雑なUIを構築できます。

一方、RealityKitは、拡張現実(AR)体験を構築するためのフレームワークです。リアルな3Dグラフィックスのレンダリング、アニメーション、物理シミュレーション、空間オーディオなど、ARに必要な機能を網羅しています。

SwiftUIとRealityKitを組み合わせることで、直感的な操作、美しいビジュアル、そして没入感のあるサウンドを備えた、革新的なVision Proアプリを開発できます。

目指すゴール:跳ねる球体で作るシンプルなジェネラティブアート

本記事では、Vision Proアプリ開発の第一歩として、跳ねる球体を使ったシンプルなジェネラティブアートアプリを開発します。SwiftUIとRealityKitの基本を学びながら、以下の機能を実装していきます。

  • 3D空間上に球体を表示する

  • 球体をランダムに動かす

  • 壁との衝突を検知して跳ね返る動きを実装する

  • 球体の移動軌跡を残像として表示する

  • 複数の球体を制御して、より複雑な表現に挑戦する

複雑なコードや高度な数学の知識は必要ありません。順を追って解説していくので、安心して読み進めてください。 次章からは、実際に手を動かしてアプリ開発を進めていきましょう。

開発環境の準備:XcodeとVision Proシミュレーター

いよいよVision Proアプリ開発するために、まずは開発環境を整えましょう。必要なのは、以下の2つです。

  • Xcode: Appleが提供する統合開発環境です。SwiftUIやRealityKitを使ったコーディング、UIデザイン、アプリのビルド、シミュレーターでの動作確認など、開発に必要な機能が全て揃っています。

  • Vision Proシミュレーター: あなたのMac上でVision Proの動作を再現するシミュレーターです。実機がなくても、アプリの動作確認やデバッグを行うことができます。

図1 App Store - Xcode

XcodeはApp Storeから無料でダウンロードできます。Vision Proシミュレーターは、Xcodeに付属しています。

Xcodeの準備とVision Proシミュレーターのインストール

すでにXcodeをお使いの方は、最新バージョンであることを確認してください。もし最新バージョンでない場合は、App Storeからアップデートしてください。原稿執筆時点の最新バージョンはXcode 15.4です。

図2 シミュレーターのインストール

Xcodeの初回起動時に、開発するプラットフォームのシミュレーターをインストールするモーダルウインドウが表示されます。シミュレーターは容量が大きいため、必要なプラットフォームのみチェックを入れてください。ここでは、「iOS17.5」「visionOS1.2」にチェックを入れています。「Download & Install」を選びます。

新しいプロジェクトの作成

図3 新しいプロジェクトの作成

Xcodeを起動し、スタート画面から「Create a new Xcode project」を選択します。

図4 visionOS Appを選ぶ

次に、テンプレート選択画面で「visionOS > App」を選び、「Next」をクリックします。

図5 プロジェクトの設定

プロジェクトの設定画面が表示されるので、以下の項目を入力します。

  • Product Name: アプリの名前を入力します。ここでは「BouncingBalls」とします。

  • Team: Noneを選択します。(開発者登録済みの方はチーム名を選びます)

  • Organization Identifier: アプリを公開するときに使う識別子で、「com.example」と入力します。公開を予定しているときは、自分の持っているドメインを逆さにして「com.yourdomain」と登録します。

  • Initial Scene: アプリ起動時のシーンとして「Window」を選びます。

  • Immersive Space Renderer: Immersive Spaceとは、空間上にアプリをどの様に表示するかの表現方法のことで、そのレンダラーとして「RealityKit」を選択します。

  • Immersive Space: Immersive Spaceとして「Mixed」を選びます。これを選択すると、没入度を変更できるようになります。

入力が完了したら「Next」をクリックし、プロジェクトの保存先を指定します。これで、Vision Proアプリ開発の準備が整いました。

サンプルプロジェクトの実行とVision Proシミュレーターの使い方

図6 サンプルプロジェクトの起動

新しいプロジェクトが作成されると、デフォルトでサンプルコードが記述されています。このサンプルコードは、3D空間上にシンプルなオブジェクトを表示するものです。早速実行して、Vision Proシミュレーターで動作確認してみましょう。

Xcodeの画面左上にある再生ボタン(▶︎)をクリックするか、「Command + R」キーを押すと、ビルドが開始され、Vision Proシミュレーターが起動します。

図7 シミュレーターの使い方

Vision Proシミュレーターのウィンドウは、以下の3つの領域に分かれています。

  • シーンビュー: アプリで表示される3D空間が表示されます。マウスやトラックパッドを使って視点を操作できます。

  • ツールバー: 右上に、シミュレーターの操作やデバッグに使用するボタンが配置されています。

  • カメラバー: 左下に、カメラ操作のボタンが表示されています。

Vision Proシミュレーターの操作方法については、図7に説明文を見ながら、各種操作を行なってみてください。お好みで「背景シーンの変更」を行なって、リビングや書斎、図書館でのVision Proの使用状況をシミュレートできます。

さて、サンプルアプリを操作してみましょう。Windowには、グレーの3D球体、テキスト「Hello, world」、そして、「Show Immersive Space」のトグルスイッチが配置されています。トグルスイッチをオンにすると、Immersive Spaceがレンダリングされます。

図8 Immersive Spaceの2つの球体

Immersive Spaceには、2つの球体が表示されます。このサンプルプロジェクトは、静止したオブジェクトをレンダリングするだけの単純なものですが、Vision Proアプリに必要な要素を過不足なく含んでいます。このプロジェクトを改造して、思い通りのアプリを作成することができるのです。

この章では、XcodeとVision Proシミュレーターを使って、Vision Proアプリの開発環境を構築しました。次の章からは、いよいよSwiftUIとRealityKitを使って、跳ねる球体の実装に取り掛かります。

球体を動かす:アニメーションの実装

いよいよ、Vision Proアプリ開発を開始します。この章では、MeshResourceを使って赤い球体を表示し、RealityKitのアニメーション機能を使って球体を動的に動かします。そして、透明度を変化させた球体で、まるで残像のように見えるエフェクトを追加します。

プログラミングを始める前に、エンティティの親子関係と座標系について説明します。

RealityKitにおけるエンティティの親子関係と座標系

VisionOSのアプリ開発では、RealityKitのエンティティと、その親子関係、そして座標系を理解することが重要です。

  • エンティティ: RealityKitにおける3Dオブジェクトはエンティティと呼ばれ、`Entity`クラスで表されます。

  • 親子関係: エンティティは親子関係を持つことができ、親エンティティの座標系を基準に配置されます。

  • content: `RealityView`に渡される`content`は、シーンのルートとなるエンティティを表します。

  • add: `content.add(entity)`は、`entity`をシーンのルートに追加します。

  • addChild: `parentEntity.addChild(childEntity)`は、`childEntity`を`parentEntity`の子として追加します。

  • 座標系: 3次元直交座標系で表され、右がX軸、上がY軸、前方がZ軸として座標位置を指定します。原点は、デバイスの真下の床の位置になる点に注意が必要です。

エンティティの親子関係と座標系について理解できたところで、赤い球体を表示するだけの簡単なコードを作成してみましょう。Xcodeのプロジェクトナビゲーターから「ImmersiveView.swift」を開き、デフォルトで記述されているコードを全て削除します。そして、以下のコードを記述してください。MeshResourceで赤い球体を表示する

MeshResourceで赤い球体を表示する

import SwiftUI
import RealityKit

struct ImmersiveView: View {
    // 球体の半径
    private let radius: Float = 0.05
    // 球体の位置
    private var position: SIMD3<Float> = [0, 0, 0]
    // 球体のエンティティ
    @State private var sphereEntity: ModelEntity!

    var body: some View {
        RealityView { content in
            // 基準のエンティティを配置
            let baseEntity = Entity() // ここから(1)
            baseEntity.position = [0, 1.2, -1.5]
            addAxes(to: baseEntity) // ここまで(1)
            content.add(baseEntity) // (2)
            
            // 赤い球体を作成する
            let sphereMesh = MeshResource.generateSphere(radius: radius) // ここから(3)
            let sphereMaterial = SimpleMaterial(color: UIColor(red: 1, green: 0, blue: 0, alpha: 1), isMetallic: false)
            sphereEntity = ModelEntity(mesh: sphereMesh, materials: [sphereMaterial])
            sphereEntity.position = position // ここまで(3)
            
            // 球体をシーンに追加
            baseEntity.addChild(sphereEntity) // (4)
        }
    }
    
    private func addAxes(to entity: Entity) {
        // X軸の色を赤に設定
        let xAxisMaterial = SimpleMaterial(color: .red, isMetallic: false) // (5)
        // Y軸の色を緑に設定
        let yAxisMaterial = SimpleMaterial(color: .green, isMetallic: false)
        // Z軸の色を青に設定
        let zAxisMaterial = SimpleMaterial(color: .blue, isMetallic: false)
        
        // X軸の線を作成
        let xAxis = MeshResource.generateBox(size: [0.5, 0.01, 0.01]) // ここから(6)
        let xAxisEntity = ModelEntity(mesh: xAxis, materials: [xAxisMaterial])
        xAxisEntity.position = [0.25, 0, 0] // ここまで(6)
        
        // Y軸の線を作成
        let yAxis = MeshResource.generateBox(size: [0.01, 0.5, 0.01])
        let yAxisEntity = ModelEntity(mesh: yAxis, materials: [yAxisMaterial])
        yAxisEntity.position = [0, 0.25, 0]
        
        // Z軸の線を作成
        let zAxis = MeshResource.generateBox(size: [0.01, 0.01, 0.5])
        let zAxisEntity = ModelEntity(mesh: zAxis, materials: [zAxisMaterial])
        zAxisEntity.position = [0, 0, 0.25]
        
        // 各軸のエンティティを追加
        entity.addChild(xAxisEntity) // (7)
        entity.addChild(yAxisEntity)
        entity.addChild(zAxisEntity)
    }
}

上記コードは、baseEntityという名前の空のエンティティを基準にして、赤い球体を配置します、baseEntityには、位置を確認するためにデバッグ用の座標軸XYZ(赤、緑、青の線)を配置しています。

(1) で、基準となる空のエンティティ baseEntity を作成します。

  • baseEntity.position = [0, 1.2, -1.5]で、baseEntityの位置をワールド座標系で(0, 1.2, -1.5)に設定します。VisionOSでは、デバイス真下の床が(0, 0, 0)となり、座標はメートル単位で指定します。

  • addAxes(to: baseEntity)で、baseEntityにXYZ軸を表示するための関数を呼び出しています。

(2) で、content.add(baseEntity)で、baseEntityをシーンのルートに追加します。これで、baseEntityと、その子エンティティがシーンに表示されるようになります。

(3)で、 赤い球体エンティティ sphereEntity を作成します。

  • MeshResource.generateSphere(radius:)で半径0.05メートルの球体メッシュを作成します。

  • SimpleMaterialで赤いマテリアルを作成します。

  • ModelEntity(mesh: sphereMesh, materials: [sphereMaterial])で、球体メッシュとマテリアルから球体エンティティを作成します。

  • sphereEntity.position = positionで座標 (0, 0, 0) に配置されます。

(4) で、sphereEntityをbaseEntityの子として追加(addChild)します。sphereEntityは、baseEntityの座標系を基準に配置されるため、baseEntityの原点に配置されることになります。

addAxes関数は、座標軸を配置します。以下は、AddAxes関数の説明です。

(5) で、XYZ軸それぞれに異なる色のSimpleMaterialを作成しています。

(6) で、XYZ軸の線を作成します。

  • MeshResource.generateBox(size:)で、幅0.5、高さ0.01、奥行き0.01メートルの直方体メッシュ(すなわち線)を作成します。

  • ModelEntity(mesh:materials:)で、直方体メッシュと赤いマテリアルからX軸のエンティティを作成します。

  • xAxisEntity.position = [0.25, 0, 0]で、X軸の位置をbaseEntityから見て(0.25, 0, 0)に設定します。

(7) で、addAxes関数に渡されたentity (ここではbaseEntity)の子として、XYZ軸のエンティティを追加します。

このように、contentを起点としてエンティティを親子関係で構築することで、複雑な3Dシーンを構築することができます。以上で、コードの解説は終了です。シミュレータを起動して、デバッグしてみましょう。

図9 赤い球体を表示する

コードをビルドして実行すると、Vision Proシミュレーター上に赤い球体が表示されます。座標軸は、X軸は赤、Y軸は緑、Z軸は青で表現されています。原点に赤い球体を正しく配置できました。

次は、アニメーションを実装します。RealityKitでアニメーションを実装する方法はいくつかありますが、今回はTimerを使って、位置を連続的に変化させる方法を採用します。この方法は、ランダムな動きを再現するのに適しています。

1メートルの箱の中を跳ね回るアニメーションを実装する

球体が1メートルの箱の中を跳ね回るアニメーションを実装しましょう。ImmersiveViewクラスのコメントで追記と書かれている2箇所を書き加えます。

struct ImmersiveView: View {
    // 球体の半径
    private let radius: Float = 0.05
    // 球体の位置
    @State private var position: SIMD3<Float> = [0, 0, 0]
    // 球体のエンティティ
    @State private var sphereEntity: ModelEntity!    // 箱のサイズを定義

    // 追記1
    // 箱のサイズ
    private let boxSize: Float = 1.0 // ここから(1)
    // アニメーションの時間間隔を定義
    private let animationDt: Float = 1.0 / 60.0
    // 赤い球体の速度を定義
    @State private var velocity = SIMD3<Float>(0.11, 0.2, 0.3)
    // タイマーを保持する変数を定義
    @State private var timer: Timer? // ここまで(1)
    // 追記1終わり

    var body: some View {
        RealityView { content in
            // 基準のエンティティを配置
            let baseEntity = Entity()
            baseEntity.position = [0, 1.2, -1.5]
            addAxes(to: baseEntity)
            content.add(baseEntity)
            
            // 赤い球体を作成する
            let sphereMesh = MeshResource.generateSphere(radius: 0.05)
            let sphereMaterial = SimpleMaterial(color: UIColor(red: 1, green: 0, blue: 0, alpha: 1), isMetallic: false)
            sphereEntity = ModelEntity(mesh: sphereMesh, materials: [sphereMaterial])
            sphereEntity.position = position
            
            // 球体をシーンに追加
            baseEntity.addChild(sphereEntity)
            
            // 追記2
            // タイマーを設定して定期的にアニメーションを実行
            self.timer = Timer.scheduledTimer(withTimeInterval: Double(animationDt), repeats: true) { _ in // (2)
                // 位置を更新
                position = position + velocity * animationDt // (3)
                
                // 壁との衝突をチェックし、反射する
                if position.x - radius < -boxSize / 2 { // ここから(4)
                    position.x = -boxSize / 2 + radius
                    velocity.x *= -1
                } else if position.x + radius > boxSize / 2 {
                    position.x = boxSize / 2 - radius
                    velocity.x *= -1
                }
                
                if position.y - radius < -boxSize / 2 {
                    position.y = -boxSize / 2 + radius
                    velocity.y *= -1
                } else if position.y + radius > boxSize / 2 {
                    position.y = boxSize / 2 - radius
                    velocity.y *= -1
                }
                
                if position.z - radius < -boxSize / 2 {
                    position.z = -boxSize / 2 + radius
                    velocity.z *= -1
                } else if position.z + radius > boxSize / 2 {
                    position.z = boxSize / 2 - radius
                    velocity.z *= -1
                } // ここまで(4)
                
                // メインスレッドで位置を更新
                DispatchQueue.main.async { // (5)
                    sphereEntity.position = position
                }
            }
            // 追記2終わり
        }
    }
    
    // 以下略

このコードは、赤い球体を一定速度で移動させ、仮想空間に配置した目に見えない箱の壁に当たると反射するアニメーションを実装しています。

(1) で、アニメーション用の変数を定義しています。

  • boxSize: 球体が跳ね回る箱のサイズ(一辺の長さ)を定義します。

  • animationDt: アニメーションのフレームレートを制御します。ここでは1/60秒、つまり60FPSを指定しています。

  • velocity: 球体の速度ベクトルを定義します。SIMD3<Float>は、3次元ベクトルを表す型です。

  • timer: アニメーションを定期的に実行するためのタイマーを保持する変数です。

(2) で、Timer.scheduledTimer(withTimeInterval:repeats:block:)を使って、指定した時間間隔で繰り返し実行されるタイマーを作成します。animationDtで指定した時間間隔で、クロージャ内の処理が繰り返し実行されます。

(3) で、球体の現在の位置(position)に、速度(velocity)と時間間隔(animationDt)を掛け合わせた値を加算することで、球体の位置を更新します。

(4) で、壁との衝突をチェックし、反射処理を行います。もし壁を超えていたら、球体の位置を壁の表面に修正し、速度ベクトルの該当成分の符号を反転させることで反射処理を行います。

(5) は、メインスレッドで球体のエンティティのpositionプロパティを更新します。RealityKitのエンティティは、メインスレッドからのみ更新可能です。タイマーのクロージャはバックグラウンドスレッドで実行されるため、DispatchQueue.main.asyncを使ってメインスレッドに処理をdispatchし、sphereEntity.positionを更新します。

これで、赤い球体が跳ね回りながら、その軌跡を残像として表示するアニメーションが完成しました。ビルドしてアプリを実行します。

図10 跳ね回る球体

座標軸を基準に1メートルの箱の中を跳ね回る球体を実現できました。箱の大きさ(boxSize)、球体の速度(velocity)の値を変えると、アニメーション表現が変化するので試してみてください。以上で、跳ね回る球体は実装できました。

次の章は、これまでのコードを改造して、球体の数を50個に増やし、カラフルな球体が箱の中で跳ね回るジェネラティブアートに変化させます。

ジェネラティブアートの完成:球体の数を増やして表現を豊かに

ここまでで、単一の球体を動かす基本を学びました。本章では、いよいよ球体の数を増やし、より複雑で美しいジェネラティブアートを創造していきます。複数の球体を効率的に管理するため、球体をクラスとして定義し、それぞれの球体が独立して動作するように実装しましょう。

球体を表す Ball クラスを定義

球体を表現する Ball クラスを定義します。 Ball クラスは、球体の半径radius、位置position、速度velocity、そしてRealityKitのモデルエンティティ modelEntityをプロパティとして持ちます。

図11 新しいファイルを作成

まず、Ballクラスを記述するための新しいSwiftファイルを作成します。プロジェクトナビゲーターで右クリックから「New File…」を選びます。

図12 ファイルテンプレート

ファイルのテンプレートが表示されるので、「visionOS > Swift File」を選びます。

図13 ファイル名を決める

画面が切り替わったら、ファイル名を「Ball」と名前をつけて、「Create」を選びます。これで、新しいファイルが作成されるので、次のコードを記述します。

import SwiftUI
import RealityKit

class Ball {
    var radius: Float
    var position: SIMD3<Float>
    var velocity: SIMD3<Float>
    var modelEntity: ModelEntity // モデルエンティティを保持
    
    init(radius: Float, color: [Float], position: SIMD3<Float>) { // (1)
        self.radius = radius
        self.position = position
        self.velocity = Ball.randomNormalizedVector() / radius / 100 // (2)
        
        // 球体を作成
        let alpha = 1.0 // ここから(3)
        let sphereMesh = MeshResource.generateSphere(radius: radius)
        let sphereMaterial = SimpleMaterial(color: UIColor(red: CGFloat(color[0]), green: CGFloat(color[1]), blue: CGFloat(color[2]), alpha: CGFloat(alpha)), isMetallic: false)
        let sphereEntity = ModelEntity(mesh: sphereMesh, materials: [sphereMaterial])
        sphereEntity.position = position
        self.modelEntity = sphereEntity // ここまで(3)
    }
    
    static func randomNormalizedVector() -> SIMD3<Float> { // (4)
        let randomAngleX = Float.random(in: 0...(2 * .pi))
        let randomAngleY = Float.random(in: 0...(2 * .pi))

        let x = cos(randomAngleX) * sin(randomAngleY)
        let y = sin(randomAngleX) * sin(randomAngleY)
        let z = cos(randomAngleY)

        return SIMD3<Float>(x, y, z)
    }
    
    func updatePosition(boxSize: Float, dt: Float) { // (5)
        // 位置を更新
        var newPosition = self.position + self.velocity * dt
        
        // 壁との衝突をチェックし、反射する
        if newPosition.x - self.radius < -boxSize / 2 {
            newPosition.x = -boxSize / 2 + self.radius
            self.velocity.x *= -1
        } else if newPosition.x + self.radius > boxSize / 2 {
            newPosition.x = boxSize / 2 - self.radius
            self.velocity.x *= -1
        }
        
        if newPosition.y - self.radius < -boxSize / 2 {
            newPosition.y = -boxSize / 2 + self.radius
            self.velocity.y *= -1
        } else if newPosition.y + self.radius > boxSize / 2 {
            newPosition.y = boxSize / 2 - self.radius
            self.velocity.y *= -1
        }
        
        if newPosition.z - self.radius < -boxSize / 2 {
            newPosition.z = -boxSize / 2 + self.radius
            self.velocity.z *= -1
        } else if newPosition.z + self.radius > boxSize / 2 {
            newPosition.z = boxSize / 2 - self.radius
            self.velocity.z *= -1
        }
        
        // 位置を更新
        self.position = newPosition
        
        // モデルエンティティの位置を更新
        self.modelEntity.position = self.position // (6)
    }
}

このコードは、3D空間内を跳ね回る球体を表す Ball クラスを定義し、その球体の動きを制御するメソッドを提供しています。

(1) は、Ball クラスのインスタンスを初期化する際に呼ばれるイニシャライザです。引数として、球体の半径(radius)、色(color)、初期位置(position)を受け取ります。受け取った値を使って、球体のプロパティを初期化します。

(2) で、球体の初期速度を設定しています。静的メソッドであるandomNormalizedVector()を呼び出してランダムな単位ベクトルを取得し、半径(radius)と100で割ることで、球体の大きさによって速度を調整しています。この処理により、小さい球体は早く、大きい球体は遅く動く挙動を実現しています。

(3) で、球体のModelEntityを作成します。

  • MeshResource.generateSphereを使って球体メッシュを生成します。

  • SimpleMaterialを使って球体のマテリアルを生成します。色は初期化時に渡されたcolor配列の値を使用します。

  • ModelEntityを使って球体エンティティを生成します。

  • 生成した球体エンティティの位置を初期位置(position)に設定します。

  • 生成した球体エンティティをmodelEntityプロパティに保持します。

(4) は、3次元空間におけるランダムな単位ベクトルを生成する静的メソッドです。ランダムな角度(randomAngleX, randomAngleY)を生成し、三角関数を使ってx, y, z成分を計算します。生成したベクトルは、長さが1に正規化されています。

(5) で、球体の位置を更新し、壁との衝突を処理するメソッドです。引数として、箱のサイズ(boxSize)と時間間隔(dt)を受け取ります。

(6) で、衝突処理後の球体の位置(self.position)を、ModelEntityのpositionプロパティに反映させています。これにより、実際に3D空間内で球体が移動します。

このBallクラスは、複数の球体を生成し、それぞれ独立して動きを制御するための基礎となります。

次に、ImmersiveViewクラスからBallクラスを参照して、複数の球体が跳ね回るアニメーションを実現します。ImmersiveViewクラスを次のように修正します。

ImmersiveViewの修正

import SwiftUI
import RealityKit

struct ImmersiveView: View {
    // 箱のサイズ
    private let boxSize: Float = 1.0
    // アニメーションの時間間隔を定義
    private let animationDt: Float = 1.0 / 60.0
    // タイマーを保持する変数を定義
    @State private var timer: Timer?
    //球体エンティティを管理する配列
    @State private var balls: [Ball] = []
    // 球体の数
    private let ballCount = 50

    var body: some View {
        RealityView { content in
            // 基準のエンティティを配置
            let baseEntity = Entity()
            baseEntity.position = [0, 1.2, -1.5]
            content.add(baseEntity)
            
            for _ in 0..<ballCount { // (1)
                // Ballオブジェクトを作成
                let randomRadius = Float.random(in: 0.005...0.03)
                let randomColor = [Float.random(in: 0...1), Float.random(in: 0...1), Float.random(in: 0...1)]
                let randomPosition = SIMD3<Float>(Float.random(in: -0.4...0.4), Float.random(in: -0.4...0.4), Float.random(in: -0.4...0.4))
                let ball = Ball(radius: randomRadius, color: randomColor, position: randomPosition) // (2)
                balls.append(ball) // (3)
                
                // 球体をシーンに追加
                baseEntity.addChild(ball.modelEntity) // (4)
            }

            // タイマーを設定して定期的にアニメーションを実行
            self.timer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { _ in
                for ball in balls { // (5)
                    ball.updatePosition(boxSize: boxSize, dt: animationDt)
                }
            }

        }
    }
}

このコードは、ImmersiveView 構造体の中で、複数の球体をランダムな位置、半径、色で生成し、それらを箱型の空間に閉じ込めて跳ね回らせるアニメーションを実現しています。

(1) は、球体の生成ループです。ballCountで指定された数(この場合は50個)の球体を生成するループです。_(アンダースコア)は、ループカウンタ変数を使わない場合のプレースホルダです。

(2) は、Ball オブジェクトの生成処理です。

  • 各球体のプロパティ(半径、色、初期位置)をランダムに生成しています。

  • randomRadiusは0.005から0.03の間でランダムな半径を生成します。

  • randomColorはRGBの各要素を0から1の間でランダムに生成し、配列として保持します。

  • randomPositionはx, y, z座標をそれぞれ-0.4から0.4の間でランダムに生成し、SIMD3<Float>型のベクトルとして保持します。

  • 生成したプロパティを使ってBallクラスのインスタンスを生成します。

(3) で、生成したBallオブジェクトを、balls配列に追加します。

(4) で、生成した Ball オブジェクトが持つ modelEntity (球体エンティティ) を、baseEntityの子として追加します。これにより、球体エンティティがシーンに表示されるようになります。

(5) は、タイマー処理によるアニメーションを実行します。

  • 1秒間に60回(60FPS)の頻度でタイマー処理を実行します。

  • タイマー処理の中では、balls配列に格納されている全てのBallオブジェクトに対して、updatePositionメソッドを呼び出します。

  • updatePositionメソッドは、球体の位置を更新し、壁との衝突を処理します。

このコードによって、複数の球体がランダムな動きで箱の中を跳ね回る、シンプルなジェネラティブアートが実現します。ビルドして、アプリを実行します。

図14 跳ね回るボールのアニメーション

50個のカラフルな球体が透明な箱の中を跳ね回るアニメーションが実現できました。お使いのMacの性能にもよりますが、ボールの数(ballCount)をもっと増やすこともできるでしょう。箱の大きさ(boxSize)もお好みで変更して、ジェネラティブアートを楽しんでください。

最後の課題は、球体に残像をつけて、移動する軌跡が見えるようにすることで、、軌跡を付けることで球体のスピードが明確になり、ジェネラティブアートとして完成させることができます。

残像による軌跡の表示を実現する

軌跡を追加するには、Ballクラス、ImmersiveViewクラスに小規模の修正が必要です。まずは、Ballクラスの修正箇所を示します。

class Ball {
    var radius: Float
    var position: SIMD3<Float>
    var velocity: SIMD3<Float>
//    var modelEntity: ModelEntity // モデルエンティティを保持 // ここから(1)
    var modelEntitys: [ModelEntity] // 複数のモデルエンティティを保持 // ここまで(1)
    
    init(radius: Float, color: [Float], position: SIMD3<Float>) {
        // 省略
//        self.modelEntity = sphereEntity // (2)
        
        // 残像用の透明度の異なる球体を作成
        self.modelEntitys = [] // ここから(3)
        for i in 0..<10 {
            let alpha = Float(0.1) * Float(10 - i)
            let sphereMesh = MeshResource.generateSphere(radius: radius)
            let sphereMaterial = SimpleMaterial(color: UIColor(red: CGFloat(color[0]), green: CGFloat(color[1]), blue: CGFloat(color[2]), alpha: CGFloat(alpha)), isMetallic: false)
            let sphereEntity = ModelEntity(mesh: sphereMesh, materials: [sphereMaterial])
            sphereEntity.position = position
            self.modelEntitys.append(sphereEntity)
        } // ここまで(3)
    }
    
    // 省略
    
    func updatePosition(boxSize: Float, dt: Float) {
        // 省略
        
        // モデルエンティティの位置を更新
//        self.modelEntity.position = self.position // (5)
        
        // 残像の位置をシフト
        for i in stride(from: modelEntitys.count - 1, through: 1, by: -1) { // (6)
            modelEntitys[i].position = modelEntitys[i - 1].position
        }
        
        // モデルエンティティの位置を更新
        modelEntitys[0].position = self.position // (7)
    }
}

このコードは、Ball クラスに複数の ModelEntity を持たせることで、球体の移動軌跡を残像として表現する仕組みを実装しています。

(1)は、単一の ModelEntity を保持していた modelEntity プロパティを、複数の ModelEntity を保持できる modelEntities 配列に変更しています。

(2) は、modelEntity への代入をコメントにして無効にしています。modelEntity プロパティは削除されたため、この行は不要になり削除されます。

(3) は、残像用の球体エンティティの生成と追加処理です。

  • init メソッド内で、透明度の異なる10個の球体エンティティを生成し、modelEntitys 配列に追加しています。

  • alpha 値をループ変数 i によって変化させることで、残像が徐々に薄くなる効果を実現しています。

(5) は、modelEntity.position の更新をコメントにして無効化しています。

(6) は、残像の位置をシフト処理です。updatePosition メソッド内で、配列の最後尾から2番目の要素から順に、1つ前の要素の位置をコピーしています。これにより、残像が球体の移動軌跡に沿って追従する効果が生まれます。

(7) で、modelEntitys 配列の先頭にある球体エンティティの位置を、現在の球体の位置 (self.position) に更新します。このエンティティが透明度0の球体の実体を表しています。

これらの変更によって、Ball クラスは単一の球体だけでなく、その残像も表現できるようになり、より視覚的に豊かな表現が可能になります。

最後に、ImmersiveViewクラスの2箇所を修正し、球体の軌跡を描くように改造します。

import SwiftUI
import RealityKit

struct ImmersiveView: View {
    // 省略

    var body: some View {
        RealityView { content in
            // 省略
                
                // 球体をシーンに追加
//                baseEntity.addChild(ball.modelEntity) // (1)

                // 球体をシーンに追加
                for entity in ball.modelEntitys { // (2)
                    baseEntity.addChild(entity)
                }
            }

// 省略

このコードは、Ballクラスが複数の透明度の違う球体エンティティを持つようになったことから、すべての球体エンティティをシーンに表示するように改造しています。

(1)は、球体エンティティが単一であった時のコードで、不要になったのでコメントにします。

(2)は、Ball オブジェクトの modelEntitys 配列に格納されている全ての ModelEntity (球体エンティティと残像) を、baseEntity の子として追加しています。これにより、球体本体だけでなく、透明度の異なる複数の残像もシーンに表示されるようになります。

以上で、全てのプログラミングが完了しました。ビルドして、アプリを起動します。

図15 球体の軌跡を表示できた

球体の移動する軌跡に残像のように透明度の違う球体を配置することに成功しました。スピードが速い球体には長い軌跡が、遅い球体には短い軌跡が描かれています。ジェネラティブアートとしての品質も向上できました。

以上で、Vision Proアプリ入門講座を終了します。

おわりに

本記事では、SwiftUIとRealityKitを用いて、Vision Proで動作するシンプルなジェネラティブアートアプリを開発しました。跳ねる球体を通して、基本的な3Dオブジェクトの作成、アニメーション、衝突判定、そして残像の実装方法を学びました。

しかし、これはほんの始まりに過ぎません。Vision Proと空間コンピューティングの可能性は無限に広がっており、あなたの創造力次第で、さらに革新的で魅力的なアプリを生み出すことができます。

次のステップへ

  • インタラクション: ユーザーが視線やハンドジェスチャーで球体とインタラクトできるようにしてみましょう。

  • サウンド: 球体の動きに合わせて効果音を追加し、より臨場感のある体験を作り出しましょう。

  • 空間認識: Vision Proの空間認識機能を活用し、現実空間の物体と球体が相互作用するアプリに挑戦してみましょう。

あなたのアイデアを形に

Vision Proは、私たちに全く新しいキャンバスを提供してくれます。あなたのアイデアを形にし、世界を変えるようなアプリを創造しましょう。


この記事が気に入ったらサポートをしてみませんか?