記事

Metalを利用したAR体験の表示

カメラ画像をレンダリングし、位置トラッキング情報を使ってオーバーレイコンテンツを表示するカスタムARビューを構築します。

概要

ARKitには、SceneKitまたはSpriteKitを使ってAR体験を簡単に表示するためのビュークラスが含まれています。しかしその代わりに、独自のレンダリングエンジンを構築する場合またはサードパーティ製のエンジンと統合する場合でも、ARKitは、カスタムビューによるAR体験の表示に必要なすべてのサポートを提供します。

どのAR体験の場合も、最初のステップとなるのは、カメラキャプチャとモーションの処理を管理するARSession(英語)オブジェクトを設定することです。セッションによって、デバイスが存在している現実の空間と、ARコンテンツのモデリングの場所であるバーチャル空間との対応が定義され、維持管理されます。AR体験をカスタムビューで表示するには、以下の作業が必要になります。

  1. ビデオフレームとトラッキング情報をセッションから取得します。

  2. 取得したフレーム画像をビューの背景としてレンダリングします。

  3. トラッキング情報を使って、カメラ画像の上にARコンテンツを配置し、描画します。

セッションからのビデオフレームとトラッキングデータの取得

独自のARSession(英語)インスタンスを生成して維持管理し、対応させたいAR体験の種類に応じたセッション設定で実行します。このセッションは、カメラから映像をキャプチャし、モデリングされた3D空間でのデバイスの位置と向きをトラッキングして、ARFrame(英語)オブジェクトを提供します。これらの各オブジェクトは、ビデオフレームがキャプチャされた時点での、個々のビデオフレームの画像と位置トラッキング情報の両方を保持しています。

ARセッションによって生成されるARFrame(英語)オブジェクトにアクセスする方法は2種類あり、Appがプルとプッシュのデザインパターンのどちらを好むかによって決まります。

フレームタイミングを制御したい場合(プルのデザインパターン)は、セッションのcurrentFrame(英語)プロパティを使って、ビューのコンテンツを描画し直すたびに現在のフレーム画像とトラッキング情報を取得します。ARKit Xcodeテンプレートは、このアプローチを採用しています。

  
// in Renderer class, called from MTKViewDelegate.draw(in:) via Renderer.update()
func updateGameState() {        
    guard let currentFrame = session.currentFrame else {
        return
    }
    
    updateSharedUniforms(frame: currentFrame)
    updateAnchors(frame: currentFrame)
    updateCapturedImageTextures(frame: currentFrame)
    
    if viewportSizeDidChange {
        viewportSizeDidChange = false
        
        updateImagePlane(frame: currentFrame)
    }
}

別の方法として、Appのデザインがプッシュパターンを好む場合は、session(_:didUpdate:)(英語)デリゲートメソッドを実装します。この方法では、セッションが、キャプチャするビデオフレームごとに1回呼び出します(デフォルトで1秒あたり60フレーム)。

フレームを取得した後は、カメラ画像を描画し、AR体験に含まれるオーバーレイコンテンツがあれば更新し、レンダリングする必要があります。

カメラ画像の描画

ARFrame(英語)オブジェクトのcapturedImage(英語)プロパティは、デバイスのカメラでキャプチャされたピクセルバッファを保持します。この画像をカスタムビューの背景として描画するには、画像コンテンツからテクスチャを生成し、それらのテクスチャを使うGPUレンダリングコマンドをサブミットする必要があります。

ピクセルバッファの内容は2平面のYCbCr(別称YUV)データフォーマットでエンコードされます。画像をレンダリングするには、このピクセルデータを描画可能なRGBフォーマットに変換する必要があります。Metalでのレンダリングでは、GPUシェーダコードでこの変換を最も効率的に実行することができます。ピクセルバッファから、(バッファの輝度(Y)平面と彩度(CbCr)平面用に1つずつ)2つのMetalテクスチャを生成するには、CVMetalTextureCache(英語)のAPIを使います。

func updateCapturedImageTextures(frame: ARFrame) {
    // Create two textures (Y and CbCr) from the provided frame's captured image
    let pixelBuffer = frame.capturedImage
    if (CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
        return
    }
    capturedImageTextureY = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.r8Unorm, planeIndex:0)!
    capturedImageTextureCbCr = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.rg8Unorm, planeIndex:1)!
}

func createTexture(fromPixelBuffer pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) -> MTLTexture? {
    var mtlTexture: MTLTexture? = nil
    let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
    let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)
    
    var texture: CVMetalTexture? = nil
    let status = CVMetalTextureCacheCreateTextureFromImage(nil, capturedImageTextureCache, pixelBuffer, nil, pixelFormat, width, height, planeIndex, &texture)
    if status == kCVReturnSuccess {
        mtlTexture = CVMetalTextureGetTexture(texture!)
    }
    
    return mtlTexture
}

次に、色変換行列でYCbCrからRGBへの変換を実行するfragment関数を使って、それらの2つのテクスチャを描画するレンダリングコマンドをエンコードします。

fragment float4 capturedImageFragmentShader(ImageColorInOut in [[stage_in]],
                                            texture2d<float, access::sample> capturedImageTextureY [[ texture(kTextureIndexY) ]],
                                            texture2d<float, access::sample> capturedImageTextureCbCr [[ texture(kTextureIndexCbCr) ]]) {
    
    constexpr sampler colorSampler(mip_filter::linear,
                                   mag_filter::linear,
                                   min_filter::linear);
    
    const float4x4 ycbcrToRGBTransform = float4x4(
        float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
        float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
        float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
        float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
    );
    
    // Sample Y and CbCr textures to get the YCbCr color at the given texture coordinate
    float4 ycbcr = float4(capturedImageTextureY.sample(colorSampler, in.texCoord).r,
                          capturedImageTextureCbCr.sample(colorSampler, in.texCoord).rg, 1.0);
    
    // Return converted RGB color
    return ycbcrToRGBTransform * ycbcr;
}

オーバーレイコンテンツのトラッキングとレンダリング

一般に、AR体験の焦点となるのは3Dオーバーレイコンテンツのレンダリングであり、コンテンツがまるでカメラ画像の中で見る現実の一部であるかのように現れます。この錯視の世界を実現するには、ARAnchor(英語)クラスを使って、現実の空間に対応する3Dコンテンツの位置と向きをモデリングします。レンダリング中に参照できるトランスフォームを提供するのが、アンカーです。

たとえば、以下のXcodeテンプレートでは、ユーザーが画面をタップしたとき、デバイスの約20cm手前の位置にアンカーが生成されます。

func handleTap(gestureRecognize: UITapGestureRecognizer) {
    // Create anchor using the camera's current position
    if let currentFrame = session.currentFrame {
        
        // Create a transform with a translation of 0.2 meters in front of the camera
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.2
        let transform = simd_mul(currentFrame.camera.transform, translation)
        
        // Add a new anchor to the session
        let anchor = ARAnchor(transform: transform)
        session.add(anchor: anchor)
    }
}

レンダリングエンジンで、各ARAnchor(英語)オブジェクトのtransform(英語)プロパティを使ってビジュアルコンテンツを配置します。Xcodeテンプレートでは、handleTapメソッドでセッションに追加された各アンカーを使って、シンプルなキューブメッシュを配置します。

func updateAnchors(frame: ARFrame) {
    // Update the anchor uniform buffer with transforms of the current frame's anchors
    anchorInstanceCount = min(frame.anchors.count, kMaxAnchorInstanceCount)
    
    var anchorOffset: Int = 0
    if anchorInstanceCount == kMaxAnchorInstanceCount {
        anchorOffset = max(frame.anchors.count - kMaxAnchorInstanceCount, 0)
    }
    
    for index in 0..<anchorInstanceCount {
        let anchor = frame.anchors[index + anchorOffset]
        
        // Flip Z axis to convert geometry from right handed to left handed
        var coordinateSpaceTransform = matrix_identity_float4x4
        coordinateSpaceTransform.columns.2.z = -1.0
        
        let modelMatrix = simd_mul(anchor.transform, coordinateSpaceTransform)
        
        let anchorUniforms = anchorUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)
        anchorUniforms.pointee.modelMatrix = modelMatrix
    }
}

リアルなライティングを用いたレンダリング

シーンに3Dコンテンツを描画するためのシェーダを設定するときは、各ARFrame(英語)オブジェクトの推定ライティング情報を使うと、いっそうリアルなシェーディングを得られます。

// in Renderer.updateSharedUniforms(frame:):
// Set up lighting for the scene using the ambient intensity if provided
var ambientIntensity: Float = 1.0
if let lightEstimate = frame.lightEstimate {
    ambientIntensity = Float(lightEstimate.ambientIntensity) / 1000.0
}
let ambientLightColor: vector_float3 = vector3(0.5, 0.5, 0.5)
uniforms.pointee.ambientLightColor = ambientLightColor * ambientIntensity

関連項目

表示

class ARSCNView(英語)

3DのSceneKitコンテンツを使って、カメラビューを拡張したAR体験を表示するためのビュー。

class ARSKView(英語)

2DのSpriteKitコンテンツを使って、カメラビューを拡張したAR体験を表示するためのビュー。