📷

visionOSでMV-HEICの空間写真をステレオ表示する方法

2024/12/11に公開

はじめに

この記事はvisionOS Advent Calendar 2024 11日目の記事です。

この記事では、インターネット上にあるMV-HEICの空間写真(SpatialPhoto)をvisionOSでステレオ表示する方法を紹介します。

空間写真はAppleVisionProやiPhoneで撮影できる立体的な写真のことです。
基本はAppleVisionProのデフォルトにあるPhotoアプリで閲覧できるのですが、今回はインターネット上にある空間写真を取得して閲覧するアプリを作成したかったので、その方法を調査しました。

空間写真の規格はMV-HEICというもので保存されるようになっており、iPhoneで撮影した通常の写真とは少し異なります。
実際にSwiftUIのAsyncImageで表示しようとしましたが、立体的ではなく、平面的に表示されてしまいました。

そこで、ShaderGraphを使って右目と左目の画像をテクスチャとして使用し、PlaneMeshに貼り付ける方法を採用しました。
公式でもそのようなサンプルプロジェクトがありますので詳しくはそちらもご覧ください。

できるようになること

  1. 空間写真をvisionOSで表示する
  2. 空間写真のエッジをぼかして、より自然な見え方を実現する

対象読者

  • visionOSアプリケーション開発に興味がある開発者
  • 空間写真に関心がある開発者

Supabaseから画像を取得する

今回はSupabaseからMV-HEICを取得します。
Supabaseの使い方はSupabaseの公式ドキュメントをご覧ください。

まずは、Storageから署名付きURLを取得します。

/**
Signed URLを生成
- @Param bucketName: バケット名
- @Param filePath: ファイルパス
- @Returns: URL
*/
private func generateSignedURL(_ bucketName: String, _ filePath: String) async throws -> URL {
    let signedURLs = try await supabase.storage
      .from(bucketName)
      .createSignedURLs(paths: [filePath], expiresIn: 60)
    
    guard let url = signedURLs.first else {
      throw StorageClient.StorageError.networkError("Failed to generate signed URL")
    }
    return url
}

次に取得したURLから画像を取得します。

/**
URLから画像データを取得してCGImageSourceを生成
- @Param url: 画像データのURL
- @Returns: CGImageSource
*/
private func fetchImageSource(from url: URL) async throws -> CGImageSource {
    let (imageData, _) = try await URLSession.shared.data(from: url)
    guard let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
      throw MediaError.loadError("Failed to create image source")
    }
    return source
}

MV-HEICからModelEntityを構築する

取得した画像からModelEntityを構築します。

/**
空間写真用のModelEntityを構築
- @Param source: 画像データのソース
- @Returns: ModelEntity
*/
@MainActor
private func assembleSpatialPhotoEntity(source: CGImageSource) async throws -> ModelEntity
{
    guard let leftImage = CGImageSourceCreateImageAtIndex(source, 2, nil),
      let rightImage = CGImageSourceCreateImageAtIndex(source, 1, nil)
    else {
      throw MediaError.imageExtractionError
    }
    
    var spatialPhotoMaterial = try await ShaderGraphMaterial(
      named: "/Root/SpatialPhotoMaterial",
      from: "Scene.usda",
      in: realityKitContentBundle
    )
    
    let (leftTexture, rightTexture) = try await createTextures(
      leftImage: leftImage, rightImage: rightImage)
    
    try configureStereoMaterial(
      &spatialPhotoMaterial, leftTexture: leftTexture, rightTexture: rightTexture)
    
    return createPlaneMesh(
      withImage: leftImage,
      material: spatialPhotoMaterial
    )
}

それぞれの処理を見ていきましょう。

MV-HEICはダウンロードすると以下のような構成になっています。

1枚目は通常のHEIC。2枚目は右目用のHEIC。3枚目は左目用のHEIC。

そこで取得したMV-HEICからCGImageSourceCreateImageAtIndexの第2引数でindexを指定します。(左目が2、右目が1)

guard let leftImage = CGImageSourceCreateImageAtIndex(source, 2, nil),
  let rightImage = CGImageSourceCreateImageAtIndex(source, 1, nil)
else {
  throw MediaError.imageExtractionError
}

そして、左右の目に合わせた画像をShaderGraphを使って切り替えるようにします。まずは、ShaderGraphMaterialをReality Composer Proから取得します。(ShaderGraphMaterialの作成方法は後述します。)

var spatialPhotoMaterial = try await ShaderGraphMaterial(
  named: "/Root/SpatialPhotoMaterial",
  from: "Scene.usda",
  in: realityKitContentBundle
)

そして、ImageからTextureを作成し、先ほどのShaderGraphMaterialを設定します。

let (leftTexture, rightTexture) = try await createTextures(
  leftImage: leftImage, rightImage: rightImage)

try configureStereoMaterial(
  &spatialPhotoMaterial, leftTexture: leftTexture, rightTexture: rightTexture)
/**
左目用・右目用のテクスチャを生成
- @Param leftImage: 左目用の画像データ
- @Param rightImage: 右目用の画像データ
- @Returns: TextureResource, TextureResource
*/
private static func createTextures(leftImage: CGImage, rightImage: CGImage) async throws -> (
TextureResource, TextureResource
) {
    async let leftTexture = TextureResource(
      image: leftImage,
      options: .init(semantic: .color)
    )
    async let rightTexture = TextureResource(
      image: rightImage,
      options: .init(semantic: .color)
    )
    return try await (leftTexture, rightTexture)
}
/**
ステレオ用シェーダーマテリアルの設定
- @Param material: シェーダーマテリアル
- @Param leftTexture: 左目用のテクスチャ
- @Param rightTexture: 右目用のテクスチャ
- @Returns: Void
*/
private static func configureStereoMaterial(
_ material: inout ShaderGraphMaterial,
leftTexture: TextureResource,
rightTexture: TextureResource
) throws {
    try material.setParameter(name: "LeftEye", value: .textureResource(leftTexture))
    try material.setParameter(name: "RightEye", value: .textureResource(rightTexture))
}

最後に表示するModelEntityを作成します。
(引数のwithImageは画像のサイズを参照するためなのでどちらでも構いません。)

createPlaneMesh(
  withImage: leftImage,
  material: spatialPhotoMaterial
)
/**
空間写真用の平面ModelEntityを生成
- @Param image: 画像データ
- @Param material: シェーダーマテリアル
- @Returns: ModelEntity
*/
@MainActor
private static func createPlaneMesh(
withImage image: CGImage,
material: ShaderGraphMaterial
) -> ModelEntity {
    let mesh = MeshResource.generatePlane(
      width: Float(image.width) / 5000,
      height: Float(image.height) / 5000,
      cornerRadius: 0.04
    )
    let entity = ModelEntity(mesh: mesh, materials: [material])
    entity.name = "SpatialPhoto"
    return entity
}

ModelEntityに設定するShaderGraphMaterialを作成する

ShaderGraphMaterialを作成するため、Xcodeから以下の手順でReality Composer Proを開きます。

左下の+ボタンからMaterial→ShaderGraphでシーンに追加します。

そして、ShaderGraphを以下のように作成します。

これで立体的な画像をvisionOSで表示することができました。

空間写真のエッジをぼかす

しかし、実機で再生するとエッジ部分の表示が乱れ、視認性が低下して目が疲れやすくなってしまいました。
そこで、デフォルトのPhotoアプリのようにエッジをぼかす処理を入れてみました。
具体的には、先ほどのShaderGraphにエッジを検出してぼかし(Blur)を適用するノードを追加したところ、エッジ部分の乱れが軽減され、見やすさが改善されました。
以下のように追加してください。

以上の実装により、visionOSで空間写真を表示することができました。

まとめ

この記事では、visionOSで空間写真を表示する方法について詳しく解説しました。
現在は空間写真を表示するためのSwiftUIは実装されておらず、どうやって表示するのか悩みましたが、RealityKitとShaderGraphを使って実現することができました。

今後も、visionOSの進化に合わせて、より高度な機能や最適化テクニックについて発信していく予定です。
この記事が皆様のvisionOS開発の一助となれば幸いです。もし参考になったと思われましたら、ぜひ「いいね」やフィードバックをお願いいたします。

参考記事

https://developer.apple.com/documentation/shadergraph
https://developer.apple.com/forums/thread/733813
https://www.finnvoorhees.com/words/reading-and-writing-spatial-photos-with-image-io#reading-spatial-photos

Discussion