聴講メモ:visionOS TC 2025 – 備忘録編

昨日感想を投稿した visionOS TC 2025 の、手元でガーっとメモしたものを備忘録がてら整理してみた。聴くのに集中してメモしていないものもあり。アーカイブ動画配信いただけるようなので楽しみ。

イベントページ:https://visionos-tc.com


Transform your iOS app into an Immersive Experience

Masakaz Ozaki さん

  • iOSの既存アプリをどうイマーシブに落とし込んでいくか?
  • visionOS 開発では既存コードベースをほぼそのまま利用できる、せっかくvisionOS に対応するならイマーシブな体験を追加しよう
  • イマーシブとは?
    • Part of the content
    • Makes you want to reach out instinctively
    • Seamless transition
    • Experience that moves you
  • 東京「舞浜」
    • 流動的な体験フロー:京王線のドアが開いて、アトラクションに向かうまでの体験
  • どのように iOS アプリをいまー支部な体験に落とし込むか
    • iOSの複雑な画面遷移の構造を乗り越える:シーンの概念を使う
      • コアな機能だけを持って行って、そこから広げていくことも可能
    • Step 1:iPhoneのスクリーンを7台並べる(マルチウインドウ)
      • 横に並べるだけではなく、扇形に。眼精疲労を防止
    • Step 2:iPhone のかたちにとらわれないものを並べる(紙面をグリッド状に並べる)
      • iOS で使っている技術、SwiftUI の modifier だけで実現できる、RealityKit 使わなくて良い
    • Step 3:(震源地図)
    • Step 4:Metal を使ったビジュアライゼーション(衛星のリアルタイムレンダリング)
      • イマーシブっぽく見せる工夫、ウィンドウを斜めに表示する
  • ウィンドウの工夫、手を上げて操作する(ゴリラ腕問題)を解消するために、自動スクロールさせる
  • コンテンツの文字部分と大きな背景画像は分離、いっぱい動かすと画面酔いを引き起こすため

様々なジャンルの Apple Vision Pro 専用ゲームタイトル制作で直面した技術的課題と解決方法の紹介

Graffity株式会社 cova さん

“聴講メモ:visionOS TC 2025 – 備忘録編” の続きを読む

参加メモ:visionOS TC 2025 – 感想編

国内初の visionOS テックカンファレンス、visionOS TC がこの土日で開催されたので参加した。過去にプロポーザル提出したと書いたが採択にはならず、純粋にオーディエンスとして二日間楽しんだ。

イベントページ:https://visionos-tc.com

会場はアベマタワー。個人的に昼開始というのがとてもありがたく、休日午前のルーチンを崩さず余裕を持って会場に向かうことができた。

ビル入り口や会場フロアに、visionOS TC のパネルが掲げられていてテンションが上がった。ノベルティに肩掛けポーチをいただき、Vision Pro のバッテリー入れ説が会場では囁かれていた。

セッションのトークテーマのラインナップが素晴らしく、Vision Pro 向けのプロダクト開発から得られた超実践的な知見から、空間ビデオ撮影の心動かされるエピソードまで幅広く、加えて海外のゲストスピーカーからは体験設計のノウハウ、空間体験制作のワークフローにAIをフル活用するナレッジがそれぞれ講演された。さらに8人による LT では、開発にとどまらなない変化球的側面からも visionOS が考察され、こう列挙するだけでも半日とは思えないほど、死角なしの充実さだった。

クロージングトークでは、主催服部さんから「Apple Vision Proに未来はあるのか?」というドキッとする投げかけがありながらも、こうしたきっかけを起点に、日本から空間コンピューティングの面白い事例が創出されているという未来を作りたいというカンファレンスに掛ける想いを語られ、強く感銘を受けた。そしてその未来に向けて、微力ながらも加勢したいと刺激を受けたのだった。

懇親会では豪華な食事も。visionOS アプリのデモを見せていただいたり、空間ビデオの活用方法から日常業務の悩みにいたるまで、たくさんのトピックで会話できた。


また Day 2 は Apple Japan での開催で、パネルトークとネットワーキングが中心。visionOS の2025年を総括したり、2026年の展望を語ったり、会場からのQ&A含めてさまざまな切り口から国内外屈指のトップクリエイターたちの声が聞けたのは非常に貴重だった。

ネットワーキングでは、昨日に続いて、空間ビデオの意義や現状の性能限界についてや、3Dプリンタを用いた Vision Pro アクセサリーの制作について実体験を聞いたりし、つい先日購入した3Dプリンタの活用を見出す良い機会となった。

実はこのカンファレンスでは visionOS アプリ開発のモチベーションを超えて、手元にある GoPro で撮りためた180°/360°動画をどう活用していくかであったり、3Dプリンタの活用や、そのためのモデリングをどう学んでいこうか、といった新たな興味関心を、他の参加者の方々と話す中で掘り起こされたのが面白かった。こうした、思ってもみなかった方向に発見があるのは、リアルな場こそのセレンディピティだと実感した。

またゲストスピーカーで来日されたTomさん、Oliverさんに話しかけるチャンスがあり、直接 Day 1 のセッションについて質問できたり、今自分がやっていることの展望をシェアすることができたのも嬉しいできごとだった。


この二日間で、たくさんの知見や発見、出会い、そして未来に向けた創造のモチベーションが得られたことはひとえに、素晴らしく設計運営された場があってのもの、、会場オペレーションは死角なしに素晴らしく、学びに交流にその時その時を楽しめた。主催の服部さんはじめ運営、スタッフ、サポーター、全ての関係者のみなさまに感謝!

来年開催を信じ、登壇か、サポーターか、あるいはスタッフか、どんな形であっても、国内 visionOS コミュニティを盛り上げる一助になりたいと、すでにワクワクが止まらない。

Day 2 会場を出た後の、ヒルズのイルミネーション

実装メモ:Vision Pro の向いている仰角/伏角(pitch)を求める

Vision Pro 装着時の頭部が、水平に対して何度上下に向いているか(仰/伏角、ピッチ)を知るには、頭部(カメラ)方向のベクトルについて、XZ平面に射影したベクトル長とY軸方向の成分とで atan を計算すれば、その角度(ラジアン)が求まる。

カメラ方向ベクトルの求め方は、SATOSHI さんのサンプルコードを参照。

let worldTracking = WorldTrackingProvider()

fun headPitchRadian() -> CGFloat {
    let matrix = worldTracking.deviceOriginFromAnchorTransform!
    // カメラ前方方向
    let cameraForward = simd_act(matrix.rotation, simd_float3(0, 0, -1))
    let front = SIMD3<Float>(x: cameraForward.x, y: cameraForward.y, z: cameraForward.z)
    // XZ平面に射影したベクトル長
    let horizontalLen = simd_length(SIMD2<Float>(front.x, front.z))
    return CGFloat(atan2(front.y, horizontalLen))
}

ここで、worldTracking.deviceOriginFromAnchorTransform に対して [0, 0, 1] によりZ軸方向に反転させているのは、もともとデバイス正面がZ軸の負方向を向いているため。

カメラ方向ベクトルの算出は以下でもOK。

// columns.2 がデバイスZ軸方向のワールド座標系におけるベクトル
let zAxis = SIMD3<Float>(
    matrix.columns.2.x,
    matrix.columns.2.y,
    matrix.columns.2.z
)
let cameraForward = simd_normalize(-zAxis) // 反転

実装メモ:デバイス座標系における Entity の座標を求める

以下実装のために、デバイス座標系における Entity の座標を求める必要があった。

trackingMode = .continuous とした AnchorEntity(.head) の座標系に変換すれば簡単にできるかと思ったが、デバイス位置にアンカーがうまく追従されず、原因は分からなかったが先に進めたかったので、座標変換して求めることにした。

デバイス→ワールド座標系の変換行列 の逆行列をとれば、ワールド→ワールド座標系の変換行列となり、ワールド座標系における任意の座標をデバイス座標系に変換することができる。

let worldTracking = WorldTrackingProvider()

func positionInDeviceSpace(of entity: Entity) -> SIMD3<Float> {
    guard let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: CACurrentMediaTime()) else { return .zero }
    let worldFromDevice = deviceAnchor.originFromAnchorTransform
    let deviceFromWorld = worldFromDevice.inverse
    let worldFromEntity = entity.transformMatrix(relativeTo: nil)
    let deviceFromEntity = deviceFromWorld * worldFromEntity
    return SIMD3<Float>(
        deviceFromEntity.columns.3.x,
        deviceFromEntity.columns.3.y,
        deviceFromEntity.columns.3.z
    )
}

WWDC23:Explore materials in Reality Composer Pro

ShaderGraphMaterial を用いた表現を試してみたく、ずっと避けてきた Reality Composer Pro でのノードベースのマテリアル設計を知るために、2年前(visionOS がまだセッション内で xrOS と呼ばれていた頃)のビデオを掘り起こし、久々にサマリしてみた(AIの手を借りて)。

こう見ているとなるほど簡単に思えるが、そもそもどんな表現をしたいかが頭に思い描けていないと、ただ闇雲にエディタをいじくる羽目になりそう。そして Reality Composer Pro での表現の可能性を知るためには、各ノードごとの Input/Output を把握する必要もある。


0:00 – Introduction

  • Reality Composer Pro と visionOS の material 基礎を俯瞰し MaterialX ベースの ShaderGraphMaterial と Reality Composer Pro の Shader Graph エディタでの制作フローを提示

0:55 – Materials in xrOS

  • material が色や画像アニメーション、geometry の変形が可能、 PBR による物理特性パラメータで外観を制御
  • RealityKit 2 の CustomMaterial と visionOS 限定の ShaderGraphMaterial、Physically Based と Custom の二種類の shader が選択可能
  • MaterialX というオープンスタンダードに基づくグラフ構成で material を設計

3:38 – Material editing

  • 地形に等高線を描く TopographyMaterial を例に Reality Composer Pro の Shader Graph 操作の実演
  • Position ノードから高さを取得し Separate で Y 値を抽出後 Modulo と ifgreater で高さ帯を判定しラインと地形色を切り替えるロジックを構築
  • inspector でノード引数を設定し Diffuse Color に接続することでカスタム shader が実際のモデルへ即時反映されるワークフローを実演

10:26 – Node graphs

  • 複雑なノード群を Compose Node Graph でまとめ Lines ノードとして再利用し SecondaryLines インスタンスでラインの間隔や色を差別化
  • Spacing と Color の入力を追加してノードグラフ間のパラメータ共有を可能にし Multiply ノードで複数ラインセットを合成す

13:16 – Geometry modifiers

  • Geometry Modifier サーフェスを追加し高さマップやカラーラスターデータを Image ノード経由で読み込み Model Position Offset と surface normal を同時に更新してフラットなディスクを地形へ変換
  • Remap ノードで法線値を -1 から 1 に補正し Combine 3 で Y 成分のみを操作
  • 新しい地形用のデータセットを追加し Mix ノードと 0 から 1 の制御値で二つの地形間を補間し Promote で Swift コードから進行度を操作できる入力として公開可能

実装メモ:Vision Pro のハンドトラッキング検出可能な角度を実験してみた

HandTrackingProvider を用いたトラッキングで、どこまで広域に検出可能なのか実験してみた。

デバイスに対して水平(X軸)方向を0度とし、正面(Z軸)方向を90度とした時、試してみると-10度強くらいまでは継続的・安定的にトラッキングできた。デバイスに対してやや後ろまでの検出はあてにして良さそうだ。

数値上は-20度近くまで到達できたが、ここまでくるとトラッキングが一時的に失われることもあった。


検証環境:Vision Pro (2024) / visionOS 26.0.1

実装メモ:ParticleEmitterComponent でパーティクルを Entity 自身に吸収させる

パーティクル演出は基本的に放出されるようなイメージがあるが、visionOS の ParticleEmitterComponent では内向きに集約させることもできるのでメモ。

mainEmitter.attractionCenter に吸収先の座標を指定すればよく、emitterShapeSize を Entity のサイズよりも大きく指定した上で、エミッターを持つ Entity 中心座標を指定すれば、自分自身にパーティクルを吸収させることができる。

var p = entity.components[ParticleEmitterComponent.self]!
p.emitterShape = .sphere
p.emitterShapeSize = .init(x: 0.1, y: 0.1, z: 0.1)
p.fieldSimulationSpace = .local
p.mainEmitter.attractionCenter = entity.position(relativeTo: entity)

entity.components[ParticleEmitterComponent.self] = p

検証環境:visionOS 26.0.1

実装メモ:visionOS でパーティクルのプロパティをランタイムで変更する

Reality Composer Pro で定義したパーティクル(前回記事参照)を、Entity に対し ParticleEmitterComponent で設定したあと、その属性をアプリ動作中に動的に変更することができる。

単純に、ParticleEmitterComponent を取り出して値を変更し、再度入れ直せば良い。

struct ImmersiveView: View {
    @State var model: Entity?
    @ObservedObject var someTracking: SomeTracking

    var body: some View {
        RealityView { content in
            model = ModelEntity(...
            await attachParticleEffect(to: model!) // https://p0dee.com/blog/2025/11/03/define-particle-on-reality-composer-pro-and-attach-to-entity/
        } update: { content in
            guard let model else { return }
            model.position = someTracking.location
            updateParticleEmitterProperties(for: model, value: someTracking.strength)
        }
    }

    func updateParticleEmitterProperties(for model: Entity, value: Float) {
        guard var particleEmitterComp = centerMarker.components[ParticleEmitterComponent.self] else {
            return
        }
        let birthRate = Float(value * 100)
        particleEmitterComp.mainEmitter.birthRate = birthRate
        model.components[ParticleEmitterComponent.self] = particleEmitterComp
    }
}

他にも、RealityKit.System を使って、シーンの毎更新でパーティクルを自動的に更新する方法もありそう(出来た)。この手法は、例えば Apple のサンプルだと惑星や飛行機などの物体を周回軌道に乗せて動かし続ける、といった表現に用いられている。以下のコードだと、ParticleEmitterComponent を持ったすべての Entity を更新対象としいる。雑なので、Entity Component System に乗っ取り、カスタム Component を定義して、属性変更に必要な値もこれを経由して受け渡す設計にすると良いかもしれない。

// https://developer.apple.com/videos/play/wwdc2023/10081/
public class ParticleTransitionSystem: RealityKit.System {
    public required init(scene: RealityKit.Scene) { }
    
    private static let query = EntityQuery(where: .has(ParticleEmitterComponent.self))

    public func update(context: SceneUpdateContext) {
        let entities = context.scene.performQuery(Self.query)
        for entity in entities {
            updateParticles(entity: entity)
        }
    }
    
    public func updateParticles(entity: Entity) {
        guard var particleEmitterComp = entity.components[ParticleEmitterComponent.self] else {
            return
        }
        // スケールに応じてパーティクルの放出量を変化させる
        let scale = entity.scale(relativeTo: nil).x
        let birthRate = Float(scale * 100)
        particleEmitterComp.mainEmitter.birthRate = birthRate
        entity.components[ParticleEmitterComponent.self] = particleEmitterComp
    }
}

実装メモ:Reality Composer Pro で定義したパーティクルを Reality View のエンティティにアタッチする

visionOS/App プロジェクトを作成したら初期状態で存在する Packages/RealityKitContent/Package を Reality Composer Pro で開き、以下のブログに従ってパーティクルを追加する。

How to make a fire effect using particles and Reality Composer Pro

これを実装で抽出し、任意のエンティティに追加する。

func attachParticleEffect(to entity: Entity) async throws {
    let particlesRoot = try await Entity(
        named: "ParticleEffect",
        in: realityKitContentBundle // already defined in Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift
    )
    guard
        let emitterSourceEntity = particlesRoot.findEntity(named: "ParticleEmitter"),
        let emitterComponent = emitterSourceEntity.components[ParticleEmitterComponent.self]
    else {
        assertionFailure("Emitter entity or ParticleEmitterComponent not found")
        return
    }
    entity.components.set(emitterComponent)
}

検証環境 Xcode 26.0.1 (17A400)

visionOS TC 2025 にプロポーザルを提出した

昨夜こんな投稿をしたのだが、

ふと、Vision Pro で長らくやりたかったことを思い出したので、プロポーザルを出してみた。

空間コンピューティングでOne more repを切り拓けるか

筋トレやってますか?
人類は、トレーニングの質を日々追い求め、対内外、五感、科学的/非科学的を問わずあらゆる工夫を編み出してきました。
食事、サプリ、シューズやウェア、モチベーションソング・・・
しかし未踏の領域は、まだ我々に残されているように思います。それは視覚です。

Vision Pro を手にした今こそ、ジムの退屈な風景を疑ってみませんか?
空間コンピューティングを活かし、ポージングと連動して視覚・聴覚を演出強化することで、パフォーマンスは増強するのか。限界突破(One more rep)に与するのか。

人体実験を通して、身体の可能性とインターフェースの未来に迫ります。

書いてある通り、visionOS のポージングやハンドトラッキング技術を使った視覚演出により、精神面を補助し、トレーニングの限界を突破できるのではないか?というアイデアがある。これを実証するために実は昨年末、自宅用のダンベル・ベンチセットを購入したのだが、ただのタンスの肥やしになってしまっていた。

技術的な具体性が一切ないので受けは良くない気がしている。具体で書いたら、やりたいことが簡単にイメージできそうなのであえて避けたのと、そもそも現時点まだ何も作っていないので、方針変えた時に逃げられないのもある。

昨日からプロポーザル投稿が増えてなかったのと、現状オモシロ系がなかったので、空気を変えようと若干ネタに走ってみたが、、夜中のテンションで書き上げたので、明日朝読みなおしたら普通に後悔してそう泣