Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

Metal で AR の描画をしたかったので調べた - ピクセルフォーマット編 -

こんにちは、iOSエンジニアの千吉良(ちぎら)です。

AVCaptureSession を使ってカメラからの入力を取得して画面に描画しようとした場合、 BGRA 形式のピクセルバッファを受け取って画面への描画を行うことになります。取得したピクセルバッファを用いて画像編集をした際に、ピクセルフォーマットの考慮が漏れていて景色が青くなってしまった、という経験をされた方も多いのではないでしょうか。

ARKit からも同じようにフレーム毎にピクセルバッファを取得することができますが、ARKit ではピクセルバッファは YCbCr(YUV) という形式になっており、描画部分に Metal を用いる場合にはピクセルフォーマットを意識してプログラムを書く必要があります。

今回は ARKit から取得したピクセルバッファを、Metal を用いて画面に描画するまでの処理をピクセルフォーマットに注目しながら追ってみます。

ARKit と Metal の連携

以下の Apple の公式ドキュメントには、ARKit アプリケーションの描画部分に Metal を用いる場合の説明が書かれています。

Displaying an AR Experience with Metal

ドキュメント内の説明に使われているコードは、Xcode で新規プロジェクトを作成する際に Augmented Reality App を選択し、Content Technology に Metal を指定すると生成されるプロジェクトに含まれるコードの一部です。

今回はこのプロジェクト内のコードを追って見ていきます。

生成されたプロジェクトの実行画面

f:id:timers-tech:20171106112215p:plain
画面をタップするとAR空間上に立方体が置かれます

プロジェクトの構成

このプロジェクトに含まれるコードが記述されたファイルは5つです。

  • ViewController.swift
    • MTKView の保持とデリゲート処理
    • ARSession の生成とデリゲート処理
    • 画面タップのハンドリング
  • Renderer.swift
    • 描画関連のクラスの生成や保持、描画処理の記述
  • Shaders.metal
    • バーテックスシェーダ、フラグメントシェーダ、シェーダで用いられる構造体の定義
  • ShaderTypes.swift
    • シェーダとSwiftプログラムとで共通で使われる構造体等の定義
  • AppDelegate.swift

ピクセルバッファの形式に関するコードが含まれているのは、Renderer.swiftShaders.metal です。

まずは ARSessionインスタンスから、ピクセルバッファを取得する部分を見てみましょう。

ピクセルバッファの取得とテクスチャの生成( Renderer.swift

ピクセルバッファの取得は、関数 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)

frame.capturedImage

capturedImage のドキュメントを option + click で確認してみましょう。ピクセルバッファの形式に関係した部分を抜粋します。

ARKit captures pixel buffers in a planar YCbCr format (also known as YUV) format. To render these images on a device display, you'll need to access the luma and chroma planes of the pixel buffer and convert pixel values to an RGB format.

(意訳) ARKit は YCbCr 形式でピクセルバッファをキャプチャします。デバイス上のディスプレイにそれらを描画する為には、輝度と彩度平面にアクセスしてピクセルの値を RGB に変換する必要があります。

ここで YCbCr という単語が出てきました。

YCbCr 形式とは

https://ja.wikipedia.org/wiki/YUV の記述を引用します。

YUVやYCbCrやYPbPrとは、輝度信号Yと、2つの色差信号を使って表現される色空間。

上記のリンク内に分かりやすい参考画像が参照されていますので、併せてご確認ください。

テクスチャの生成

より詳細に調べるために、再びコードを追ってみましょう。上記のコード内にある、テクスチャの生成に関する部分です。

capturedImageTextureY = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.r8Unorm, planeIndex:0)
capturedImageTextureCbCr = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.rg8Unorm, planeIndex:1)

ここでは、ARKit から取得したピクセルバッファからそれぞれインデックス 01 を指定して、2種類のテクスチャを生成しています。

1つめの capturedImageTextureY輝度信号Y にあたるテクスチャです。ピクセルフォーマットが .r8Unorm となっていますから、1ピクセルあたり 8bit の情報量で輝度情報が並べられています。

2つめの capturedImageTextureCbCr2つの色差信号 にあたるテクスチャです。こちらはピクセルフォーマットが .rg8Unorm となっていて、1ピクセルあたり 8bitx2 の情報量で色の情報が並べられています。

ここまでで、YCbCr 形式のピクセルバッファから2種類のテクスチャを生成することができました。

ピクセルバッファから2枚のテクスチャを生成

f:id:timers-tech:20171106112230p:plain
ピクセルバッファには輝度と色を表す情報が含まれていて、それを元にしてテクスチャを生成することができます

YCbCr から RGB への変換( Shaders.metal

バイスの画面に描画する為には、RGB 形式で色の指定をしなければいけません。その処理は Shaders.metal に定義されています。

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;
}

上のような関数はフラグメントシェーダと呼ばれ、デバイスの画面に表示する色の情報を返します。 返り値の型 float4float 4つ分のデータで、RGBA の形で色を表しています。

YCbCrRGB は相互変換することができて、ここでは変換行列を定義して計算結果を返しています。アルファ値は常に 1 に設定されています。

これで無事、ARKit のカメラからの入力を Metal で描画することができました。

他にも

サンプルプロジェクト内には Metal でAR空間上にものを置く処理など、他にもたくさんの情報があります。Metal に触れる機会としては、手頃で分量もちょうど良さそうです。皆さんもお暇な時に遊んでみてはいかがでしょうか。

積極採用中!!

子育て家族アプリFamm、カップル専用アプリPairyを運営するTimers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる