Apple Vision Pro(visionOS 2)RealityKit LowLevelMesh APIについて  | 技術ブログ | 株式会社OnePlanet 読み込まれました

Apple Vision Pro(visionOS 2)RealityKit LowLevelMesh APIについて

Appleの新しい空間コンピューターデバイス『Apple Vision Proは何ができる?』

日本の発売日が2024年6月28日(金)に決定したAR、VR、XR技術を搭載の新しい空間コンピューターデバイス『Apple Vision Pro(アップル ビジョン プロ)』(値段は税込599,800円〜)について以下をご覧ください。

https://ar-marketing.jp/apple-glass-release/


日本初のApple Vision Pro (アップル ビジョン プロ)アプリ開発を手掛けました

第96回アカデミー賞 4部受賞した映画『哀れなるものたち』(配給:ウォルト・ディズニー・ジャパン株式会社様)の世界に没入体験できるApple Vision Proのアプリの開発から渋谷PARCOでの展示サポートまで株式会社OnePlanetが手掛けました。

詳細:https://1planet.co.jp/news/Twd0qMjt

Apple Vision Pro(アップル ビジョン プロ)やAR関するご依頼・ご相談など、お気軽にお問い合わせください。


はじめに

今回、Apple Vision ProのvisionOS 2の新機能AP、LowLevelMesh API ついて説明します。前回の記事は以下のリンク先ページにあります。

Apple Vision Pro(visionOS2)RealityKit HoverEffectComponent の Spotlight効果とシェーダーの適用について

RealityKitのメッシュ構造について

メッシュは頂点と三角形などのプリミティブの集合体です。これらの頂点には位置やテクスチャ座標などの属性が関連付けられ、それらの属性はデータで記述されます。例えば、各頂点位置は3次元ベクトルです。

頂点データはGPUに送信できるようバッファは整理されている必要があります。ほとんどのRealityKitメッシュのデータは連続してメモリに配置されます。つまり、頂点位置0の後に頂点位置1が続き、頂点2へと続くように配置されます。これ以外の頂点の属性に同じです。インデックスバッファは個別に配置され、メッシュ内の各三角形の頂点インデックスを含まれます。

標準でないメッシュレイアウト

RealityKitの標準のメッシュレイアウトは非常に汎用性が高く、多くのユースケースに適しています。ただし、特定の用途において、その用途にあった手法の方が効率的な場合もあります。例えばお絵描きアプリの場合、ユーザーのブラシストロークをメッシュに変換するためにカスタムジオメトリ処理パイプラインを使用しています。具体的には、各ブラシストロークが滑らかに曲がるような表現を行いたい場合、メッシュの曲率を向上させることでスムーズに表現されます。

このアルゴリズムは最適化されており、ブラシストロークの端にポイントを追加する速度が最大化されています。この場合、レイテンシを最小限に抑えることが重要になります。

ブラシストロークメッシュには、単一のバッファが使用されます。しかし、標準のメッシュレイアウトとは異なり、各頂点はその属性がすべて記述された後に次の頂点に移ります。つまり、属性が交互に配置されます。最初の頂点の位置の後にその頂点の法線が続き、接線が続きます。すべての属性が記述された後に次の頂点に移ります。

このバッファとは対照的に、Standard vertex bufferは各属性のデータを連続して配置します。Brush vertex buffer レイアウトは、お絵描きアプリに便利です。

ブラシストロークを生成する際、アプリはvertex bufferの末尾に頂点を追加し続けます。Brush vertex bufferは新しい頂点を追加する際、古いデータの位置を変更することなく追加できます。しかし、Standard vertex bufferを使用すると、バッファが大きくなるについて、ほとんどのデータを移動させる必要があります。

Brush vertexには、標準レイアウトで見られるものとは異なる属性も含まれます。位置、法線、接線などの標準的な属性の他に、色やマテリアルのプロパティ、曲線距離などのカスタム属性があります。

struct SolidBrushVertex {
    packed_float3 position;
    packed_float3 normal;
    packed_float3 bitangent;
    packed_float2 materialProperties;
    float curveDistance;
    packed_half3 color;
};

アプリのコードでは、ブラシ頂点はMetal Shading Languageのこの構造体として表されます。構造体の各エントリは頂点の属性に対応します。

問題について

ここで問題に直面しています。一方では、高性能ジオメトリエンジンの頂点レイアウトを保持し、不要な変換やコピーを避けたい。しかし、ジオメトリエンジンのレイアウトはRealityKitの標準レイアウトと互換性がありません。GPUバッファをそのままRealityKitに持ち込み、データを読み取る手段が必要になってきます。

LowLevelMesh API

この問題を解決するために登場したのが、LowLevelMesh APIになります。LowLevelMesh API を使用すると、頂点データをさまざまな方法で配置することができます。

頂点データ用に4つの異なるMetalバッファを使用できます。したがって、RealityKitの標準のレイアウトと同様のレイアウトを使用することもできます。

しかし、複数のバッファを持つことが便利な場合もあります。例えば、他の属性よりも頻繁にテクスチャ座標を更新する必要がある場合、この動的データを独自のバッファに移す方が効率的です。

頂点バッファを交互に配置したり、交互でない配置をしたりすることもできます。

また、三角形ストリップなどの任意のMetalプリミティブタイプを使用できます。

LowLevelMesh API の活用について

メッシュデータが独自のカスタムレイアウトを持つバイナリファイルから供給される場合、このデータをそのままRealityKitに転送できます。

デジタルコンテンツ作成ツールやCADアプリケーションなどの既存のメッシュ処理パイプラインをRealityKitに橋渡しする場合にも効率的です。さらには、ゲームエンジンからRealityKitにメッシュデータを効率的に橋渡しする方法としてもLowLevelMesh API を使用することができます。

LowLevelMesh APIは、RealityKitにメッシュデータを提供する方法の可能性を広げます。

LowLevelMesh API の実装

アプリは追加の変換や不要なコピーを行わず、そのまま頂点バッファをLowLevelMeshに渡すことができるようになりました。

LowLevelMesh属性を使用して、頂点の配置方法を記述します。SolidBrushVertex構造体へのextensionで属性リストを設定します。まず、位置の属性を宣言します。

extension SolidBrushVertex {
    static var vertexAttributes: [LowLevelMesh.Attribute] {
        typealias Attribute = LowLevelMesh.Attribute
        return [
            Attribute(semantic: .position, format: MTLVertexFormat.float3, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.position)!),
            Attribute(semantic: .normal, format: MTLVertexFormat.float3, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.normal)!),
            Attribute(semantic: .bitangent, format: MTLVertexFormat.float3, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.bitangent)!),
            Attribute(semantic: .color, format: MTLVertexFormat.half3, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.color)!),
            Attribute(semantic: .uv1, format: MTLVertexFormat.float, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.curveDistance)!),
            Attribute(semantic: .uv3, format: MTLVertexFormat.float2, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.materialProperties)!)
        ]
    }
}

最初のステップはセマンティックを定義することです。これはLowLevelMeshに属性の解釈方法を指示します。

この場合、属性は位置なので、そのセマンティックを使用します。次に、この属性のMetal頂点フォーマットを定義します。この場合、SolidBrushVertexの定義に一致するようにfloat3を選択する必要があります。

次に、属性のバイトオフセットを提供します。

最後に、レイアウトインデックスを提供します。これは後で説明する頂点レイアウトのリストにインデックスを付けます。お絵描きアプリは単一のレイアウトしか使用しないため、インデックス0を使用します。

次に、他のメッシュ属性を宣言します。法線と接線の属性は位置と似ていますが、異なるメモリオフセットとセマンティックが使用されます。

色属性は半精度浮動小数点値を使用します。visionOS 2から、LowLevelMeshで圧縮頂点フォーマットを含む任意のMetal頂点フォーマットを使用することができるようになりました。

残りの2つのパラメータには、セマンティクスUV1とUV3を使用します。今年新たに、LowLevelMeshで使用できるUVチャンネルが最大8つになりました。Shader Graph Material はこれらの値にアクセスできます。

private static func makeLowLevelMesh(vertexBufferSize: Int, indexBufferSize: Int, 
                                     meshBounds: BoundingBox) throws -> LowLevelMesh
{
    var descriptor = LowLevelMesh.Descriptor() // Similar to MTLVertexDescriptor
    
    descriptor.vertexCapacity = vertexBufferSize
    descriptor.indexCapacity = indexBufferSize
    descriptor.vertexAttributes = SolidBrushVertex.vertexAttributes
        
    let stride = MemoryLayout<SolidBrushVertex>.stride
    descriptor.vertexLayouts = [LowLevelMesh.Layout(bufferIndex: 0, 
                                                    bufferOffset: 0, bufferStride: stride)]
   
    let mesh = try LowLevelMesh(descriptor: descriptor)
    
    mesh.parts.append(LowLevelMesh.Part(indexOffset: 0, indexCount: indexBufferSize,
                                        topology: .triangleStrip, materialIndex: 0,
                                        bounds: meshBounds))
    return mesh
}

LowLevelMesh API のオブジェクトを生成できます。これを行うには、LowLevelMesh Descriptorを作成します。LowLevelMesh記述子は概念的にはMetalのMTLVertexDescriptorに似ていますが、RealityKitがメッシュを取り込むために必要な情報も含まれています。最初に、頂点バッファとインデックスバッファの必要な容量を宣言します。次に、頂点属性のリストを渡します。

それから、頂点レイアウトのリストを作成します。各頂点属性はレイアウトの1つを使用します。LowLevelMesh API は頂点データ用に最大4つのMetalバッファを提供します。バッファインデックスはバッファのどれを使用するかを宣言します。

次に、バッファオフセットと各頂点のストライドを提供します。ほとんどの場合、ここで行ったように1つのバッファしか使用しません。これで、LowLevelMeshを初期化することができます。

最後のステップは、パーツのリストを作成することです。各パーツはインデックスバッファの領域をカバーします。各メッシュパーツに異なるRealityKitマテリアルインデックスを割り当てることができます。そして、アプリはメモリ効率を向上させるために三角形ストリップトポロジーを使用します。

let mesh: LowLevelMesh

let resource = try MeshResource(from: mesh)

entity.components[ModelComponent.self] = ModelComponent(mesh: resource, materials: [...])

最後に、LowLevelMeshからMeshResourceを作成し、それをEntityのModelComponentに割り当てることができます。

let mesh: LowLevelMesh

mesh.withUnsafeMutableBytes(bufferIndex: 0) { buffer in
    let vertices: UnsafeMutableBufferPointer<SolidBrushVertex>
        = buffer.bindMemory(to: SolidBrushVertex.self)

    // Write to vertex buffer `vertices`
}

LowLevelMeshの頂点データを更新する必要がある場合、withUnsafeMutableBytes APIを使用できます。このAPIは、メッシュデータを更新する際にGPUに送信される実際のメモリ領域に直接アクセスするため、オーバーヘッドが最小限に抑えられます。例えば、メッシュのメモリレイアウトを事前にわかっているため、送信された生のポインタをバッファポインタに変換するためにbindMemoryを使用することができます。インデックスバッファデータについても同様です。LowLevelMeshインデックスバッファをwithUnsafeMutableIndicesで更新できます。

LowLevelMeshは、アプリのメッシュ処理パイプラインを強力に加速するツールです。さらに、GPUを使用して頂点やインデックスバッファの更新を行うことができます。上の動画はお絵描きアプリのスパークルブラシです。これはブラシストロークに沿って動くパーティクルフィールドを生成します。このーティクルフィールドは毎フレーム動的に更新されるため、ソリッドブラシで見た更新処理とは異なります。メッシュ更新の頻度と複雑さから、GPUを使用するのが理にかなっています。

スパークルブラシには位置や色などのパーティクルごとの属性リストがあります。先ほどのように、曲線距離パラメータやパーティクルのサイズも含まれます。

GPUのパーティクルシミュレーションは、各パーティクルの属性と速度を追跡するためにSparkleBrushParticleタイプを使用します。アプリはシミュレーションのためにSparkleBrushParticlesの補助バッファを使用します。

構造体のSparkleBrushVertexはメッシュの頂点データに使用されます。各頂点のUV座標を含み、シェーダーが3D空間でパーティクルをどのように向けるかを理解できるようにします。各パーティクルには4つの頂点を持つ平面が作成されます。そのため、アプリはスパークルブラシメッシュを更新するために、パーティクルシミュレーションバッファ(SparkleBrushParticlesで満たされたもの)とLowLevelMesh頂点バッファ(SparkleBrushVerticesを含む)を維持する必要があります。

上記の図の2つのソースコード。

struct SparkleBrushAttributes {
    packed_float3 position;
    packed_half3 color;
    float curveDistance;
    float size;
};

// Describes a particle in the simulation
struct SparkleBrushParticle {
    struct SparkleBrushAttributes attributes;
    packed_float3 velocity;
};

// One quad (4 vertices) is created per particle
struct SparkleBrushVertex {
    struct SparkleBrushAttributes attributes;
    simd_half2 uv;
};

extension SparkleBrushVertex {
    static var vertexAttributes: [LowLevelMesh.Attribute] {
        typealias Attribute = LowLevelMesh.Attribute
        return [
            Attribute(semantic: .position, format: .float3, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.attributes.position)!),

            Attribute(semantic: .color, format: .half3, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.attributes.color)!),
            
            Attribute(semantic: .uv0, format: .half2, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.uv)!),
            
            Attribute(semantic: .uv1, format: .float, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.attributes.curveDistance)!),
            
            Attribute(semantic: .uv2, format: .float, layoutIndex: 0,
                      offset: MemoryLayout.offset(of: \Self.attributes.size)!)
        ]
    }
}

ソリッドブラシと同様に、頂点バッファの仕様をLowLevelMesh属性のリストで提供します。属性のリストはSparkleBrushVertexのメンバーに対応します。

LowLevelMeshをGPUで構築する時が来たら、Metalコマンドバッファとコンピュートコマンドエンコーダを使用します。バッファの作業が完了すると、RealityKitは自動的に変更を適用します。コードでの実装方法は以下の通りです。

let inputParticleBuffer: MTLBuffer
let lowLevelMesh: LowLevelMesh

let commandBuffer: MTLCommandBuffer
let encoder: MTLComputeCommandEncoder
let populatePipeline: MTLComputePipelineState

commandBuffer.enqueue()
encoder.setComputePipelineState(populatePipeline)

let vertexBuffer: MTLBuffer = lowLevelMesh.replace(bufferIndex: 0, using: commandBuffer)

encoder.setBuffer(inputParticleBuffer, offset: 0, index: 0)
encoder.setBuffer(vertexBuffer, offset: 0, index: 1)
encoder.dispatchThreadgroups(/* ... */)

// ...
encoder.endEncoding()
commandBuffer.commit()

先ほど述べたように、アプリはパーティクルシミュレーション用にMetalバッファを、頂点バッファ用にLowLevelMeshを使用します。Metalコマンドバッファとコンピュートコマンドエンコーダを設定します。これにより、アプリはGPUコンピュートカーネルを実行してメッシュを構築できます。LowLevelMeshのreplaceを呼び出し、コマンドバッファを提供します。これにより、RealityKitがレンダリングに直接使用するMetalバッファが返されます。シミュレーションをGPUにディスパッチした後、コマンドバッファをコミットします。コマンドバッファが完了すると、RealityKitは自動的に更新された頂点データを使用し始めます。

まとめ

今回はLowLevelMesh APIについて説明しました。RealityKitのLowLevelMesh APIは、アプリケーションのメッシュ処理パイプラインを強力にするAPIです。GPUコンピュートを活用した頂点やインデックスバッファの更新をサポートし、オーバーヘッドを最小限に抑えた高性能なパフォーマンスを提供します。これにより、既存のメッシュパイプラインを効率的に統合し、カスタムレイアウトを作成することが可能となり、RealityKitにおけるメッシュデータ処理の可能性をさらに広げています。今後もこのような機能を活用したApple Vision Pro(アップル ビジョン プロ)のアプリ開発を行い、より革新的で魅力的なアプリケーションの開発に積極的に取り組んでまいります。

参考

Creating a spatial drawing app with RealityKit(サンプルコード)

https://developer.apple.com/documentation/RealityKit/creating-a-spatial-drawing-app-with-realitykit?ref=elkraneo.com

今回、この動画を元に記事を作成しました。

Apple Vision Pro(アップル ビジョン プロ)やAR関するご依頼・ご相談など、お気軽にお問い合わせください。

ARに関するご依頼・ご相談など、お気軽にお問い合わせください。

https://1planet.co.jp/contact

XR エンジニア

徳山 禎男

SIerとして金融や飲料系など様々な大規模プロジェクトに参画後、2020年にOnePlanetに入社。ARグラスを中心とした最先端のAR技術のR&Dや、法人顧客への技術提供を担当。過去にMagic Leap 公式アンバサダーを歴任。

View More

お問い合わせ・ご相談

ARでやってみたいことやお困りごとなど
お気軽にお問い合わせください。

お問い合わせ