WWDC Quick Look 💓 By SwiftGGTeam
Build a spatial drawing app with RealityKit

Build a spatial drawing app with RealityKit

元の動画を見る

Highlight

本セッションは空間描画アプリの構築を通じて、visionOS 2.0 の RealityKit 新機能を体系的に紹介します。アプリでは指のピンチで空間に描画でき、ソリッドブラシとスパークルブラシの 2 種類、色と太さの調整に対応しています。


主要内容

visionOS で空間描画アプリを作る最初のステップは、指の位置を取得することです。visionOS 1.0 では ARKit 経由で間接的にハンドアンカーデータを取得する必要があり、手順が煩雑で、ARKit session と RealityKit の Immersive Space は独立した 2 つのシステムでした。visionOS 2.0 では SpatialTrackingSession が導入され、RealityKit レイヤーで直接トラッキング権限をリクエスト。AnchorEntity の transform が自動的に手のポーズを更新し、コード量が減り、ロジックも明確になります。

手のデータを取得したら、ユーザーの筆跡をリアルタイムで 3D ジオメトリとしてレンダリングする必要があります。従来の RealityKit MeshResource は連続メモリレイアウト——すべての頂点位置をまとめ、すべての法線をまとめる——新しい頂点を追加するたびに大量のデータ移動が発生します。毎フレーム成長するストロークメッシュでは、このレイアウトがパフォーマンスのボトルネックです。visionOS 2.0 の新しい LowLevelMesh API では頂点バッファレイアウトをカスタマイズでき、インターリーブ配置、複数バッファ、triangle strip トポロジーに対応し、Metal compute shader から直接頂点データを書き込むことも可能です。既存の GPU ジオメトリ処理パイプラインをゼロコピーで RealityKit に接続できます。新しい LowLevelTexture API と組み合わせ、描画アプリ全体のメッシュ生成とテクスチャ更新を GPU で処理し、フレームレートを確保できます。


詳細

SpatialTrackingSession:ARKit に代わるトラッキング入口

ImmersiveSpace では AnchorEntity を手の関節にアンカーできます。しかし AnchorEntity の transform を実際に更新するには、トラッキング権限のリクエストが必要です。SpatialTrackingSession は visionOS 2.0 の新 API です(04:18):

// Retain the SpatialTrackingSession while your app needs access

let session = SpatialTrackingSession()

// Declare needed tracking capabilities
let configuration = SpatialTrackingSession.Configuration(tracking: [.hand])

// Request authorization for spatial tracking
let unapprovedCapabilities = await session.run(configuration)

if let unapprovedCapabilities, unapprovedCapabilities.anchor.contains(.hand) {
    // User has rejected hand data for your app.
    // AnchorEntities will continue to remain anchored and update visually
    // However, AnchorEntity.transform will not receive updates
} else {
    // User has approved hand data for your app.
    // AnchorEntity.transform will report hand anchor pose
}

キーポイント:

  • SpatialTrackingSession() でセッションインスタンスを作成——トラッキングが必要な間は参照を保持
  • Configuration(tracking: [.hand]) でハンドトラッキング能力を宣言
  • session.run(configuration) でシステムの権限ダイアログを表示し、未承認の能力リストを返す
  • 権限拒否時、AnchorEntity は視覚的には更新されるが transform はデータを受け取らない——戻り値を確認してグレースフルデグラデーション
  • SpatialTrackingSession 使用後、AnchorEntity は RealityKit 物理システムとも連携可能

MeshResource extruding:2D パスを 3D モデルに押し出し

アプリのキャンバス縁は SwiftUI Path で 2 つの同心円を定義し、3D に押し出します(07:07):

let path = SwiftUI.Path { path in
    path.addArc(center: .zero, radius: outerRadius,
        startAngle: .degrees(0), endAngle: .degrees(360),
        clockwise: true)
    path.addArc(center: .zero, radius: innerRadius,
        startAngle: .degrees(0), endAngle: .degrees(360),
        clockwise: true)
}.normalized(eoFill: true)
var options = MeshResource.ShapeExtrusionOptions()
options.boundaryResolution
    = .uniformSegmentsPerSpan(segmentCount: 64)
options.extrusionMethod = .linear(depth: extrusionDepth)

return try MeshResource(extruding: path,
                        extrusionOptions: extrusionOptions)

キーポイント:

  • normalized(eoFill: true) で even-odd 塗りつぶしルールを有効化——2 つの同心円の間がリング領域になる
  • boundaryResolution で弧の細分化精度を制御——64 セグメントでリングは十分滑らか
  • extrusionMethod = .linear(depth:) で Z 軸方向に指定深度で押し出し
  • 同じ API で AttributedString の 3D テキスト押し出しにも対応(27:01)。面ごとの material index とベベル半径を指定可能

注意:visionOS の foveated rendering では、細く高コントラストのエッジにちらつきが生じることがあります。セッションでは visionOS シーンで細線ジオメトリを避け、エッジを厚くして視覚的アーティファクトを除去することを明示的に推奨しています。

HoverEffectComponent:新しいハイライトとシェーダーのホバー効果

visionOS 2.0 では HoverEffectComponent に 2 つの新しいホバー効果が追加(09:33):

let hover = HoverEffectComponent(
    .highlight(.init(
        color: UIColor(/* ... */),
        strength: 5.0)
    )
)
placementEntity.components.set(hover)

キーポイント:

  • .highlight で entity に均一なハイライト色を適用;strength で鮮やかさを制御
  • .shader(.default) で ShaderGraphMaterial によるカスタムホバー表現が可能
  • ShaderGraph の Hover State ノードは Time Since Hover StartIntensity プロパティを提供し、ストロークに沿った光晕効果が可能(13:45

LowLevelMesh:カスタム頂点レイアウト、GPU パイプラインへのゼロコピー接続

ソリッドブラシの頂点構造体(16:56):

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

LowLevelMesh.Attribute で各フィールドのセマンティクスとオフセットを記述(19:27):

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)!)
        ]
    }
}

キーポイント:

  • semantic で RealityKit に属性の解釈方法を指示;カスタム属性は UV チャンネルにマッピング(最大 8 個)
  • format は Metal 頂点フォーマットに対応。半精度・圧縮フォーマットをサポート
  • offsetMemoryLayout.offset(of:) で自動計算し、手計算ミスを回避
  • layoutIndex は頂点レイアウトリストのエントリを指し、どのバッファにデータがあるかを決定

LowLevelMesh を作成し、バッファ容量とレイアウトを指定(21:14):

private static func makeLowLevelMesh(vertexBufferSize: Int, indexBufferSize: Int,
                                     meshBounds: BoundingBox) throws -> LowLevelMesh
{
    var descriptor = LowLevelMesh.Descriptor()
    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
}

キーポイント:

  • vertexCapacityindexCapacity でバッファサイズを事前割り当て
  • vertexLayouts でインターリーブレイアウトを宣言——すべての属性が 1 バッファ内、頂点ごとに配置
  • topology: .triangleStrip は triangle list よりインデックス数を節約——管状ジオメトリに最適
  • 各 Part で異なる materialIndex を指定し、マルチマテリアルに対応

withUnsafeMutableBytes で頂点・インデックスデータを更新(22:37):

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

GPU 駆動パーティクルブラシ:LowLevelMesh + Metal Compute

スパークルブラシは毎フレームパーティクル位置を更新。Metal compute shader が LowLevelMesh バッファに直接書き込み(25:28):

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()

キーポイント:

  • lowLevelMesh.replace(bufferIndex:using:) が MTLBuffer を返し、通常の Metal バッファと同様に compute shader にバインド可能
  • コマンドバッファ送信後、RealityKit が更新された頂点データで自動レンダリング——手動同期不要
  • 入力はパーティクルシミュレーションバッファ、出力は LowLevelMesh 頂点バッファに直接書き込み——中間ゼロコピー

LowLevelTexture:GPU 動的テクスチャ

起動画面の背景テクスチャを LowLevelTexture で生成(29:44):

let descriptor = LowLevelTexture.Descriptor(pixelFormat: .rg16Float,
                                            width: textureResolution,
                                            height: textureResolution,
                                            textureUsage: [.shaderWrite, .shaderRead])

let lowLevelTexture = try LowLevelTexture(descriptor: descriptor)
var textureResource = try TextureResource(from: lowLevelTexture)

var material = UnlitMaterial()
material.color = .init(tint: .white, texture: .init(textureResource))

キーポイント:

  • pixelFormat は compressed フォーマットをサポート——visionOS 2.0 で新規
  • textureUsage で shader 読み書き権限を宣言
  • TextureResource を作成し、material の color.texture に直接割り当て
  • GPU 側更新は lowLevelTexture.replace(using: commandBuffer) で MTLTexture を取得——LowLevelMesh と完全に対称

重要ポイント

  • やること: LowLevelMesh で既存の Metal ジオメトリ処理パイプラインを RealityKit に接続。価値: Metal ベースのメッシュ生成コード(CAD、ゲームエンジンエクスポーターなど)があれば、ゼロコピー接続で変換レイヤーが不要になり、フレームレートに直接恩恵。始め方: 頂点構造体を LowLevelMesh.Attribute で記述、layoutIndex と offset を指定、LowLevelMesh.Descriptor を作成、withUnsafeMutableBytes でデータを填充。

  • やること: ShaderGraph Hover State ノードでパスに沿ったホバー光效を実現。価値: 静的ハイライトより空間感があり、ユーザーがどの 3D 要素を見ているか直感的に把握でき、インタラクション品質が目に見えて向上。始め方: Reality Composer Pro で ShaderGraphMaterial を作成、Hover State ノードを追加、Time Since Hover Start で curveDistance に沿った光晕位置を駆動、コードで HoverEffectComponent(.shader(.default)) を有効化。

  • やること: MeshResource extruding で SwiftUI Path と AttributedString を 3D UI 要素に素早く変換。価値: 2D デザインのベクターアイコンとテキスト見出しを空間の实体に直接変換でき、モデリング工程を省略。深度、ベベル、マテリアルゾーンなど動的パラメータに対応。始め方: SwiftUI.Path または AttributedString を定義、ShapeExtrusionOptions(深度、ベベル半径、マテリアル割り当て)を設定、MeshResource(extruding:) 1 行でメッシュ生成。


関連セッション

コメント

GitHub Issues · utterances