こんにちは、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
を指定すると生成されるプロジェクトに含まれるコードの一部です。
今回はこのプロジェクト内のコードを追って見ていきます。
生成されたプロジェクトの実行画面
プロジェクトの構成
このプロジェクトに含まれるコードが記述されたファイルは5つです。
ViewController.swift
MTKView
の保持とデリゲート処理ARSession
の生成とデリゲート処理- 画面タップのハンドリング
Renderer.swift
- 描画関連のクラスの生成や保持、描画処理の記述
Shaders.metal
- バーテックスシェーダ、フラグメントシェーダ、シェーダで用いられる構造体の定義
ShaderTypes.swift
- シェーダとSwiftプログラムとで共通で使われる構造体等の定義
AppDelegate.swift
ピクセルバッファの形式に関するコードが含まれているのは、Renderer.swift
と Shaders.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 から取得したピクセルバッファからそれぞれインデックス 0
と 1
を指定して、2種類のテクスチャを生成しています。
1つめの capturedImageTextureY
は 輝度信号Y
にあたるテクスチャです。ピクセルフォーマットが .r8Unorm
となっていますから、1ピクセルあたり 8bit の情報量で輝度情報が並べられています。
2つめの capturedImageTextureCbCr
は 2つの色差信号
にあたるテクスチャです。こちらはピクセルフォーマットが .rg8Unorm
となっていて、1ピクセルあたり 8bitx2 の情報量で色の情報が並べられています。
ここまでで、YCbCr 形式のピクセルバッファから2種類のテクスチャを生成することができました。
ピクセルバッファから2枚のテクスチャを生成
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; }
上のような関数はフラグメントシェーダと呼ばれ、デバイスの画面に表示する色の情報を返します。 返り値の型 float4
は float
4つ分のデータで、RGBA
の形で色を表しています。
YCbCr
と RGB
は相互変換することができて、ここでは変換行列を定義して計算結果を返しています。アルファ値は常に 1
に設定されています。
これで無事、ARKit のカメラからの入力を Metal で描画することができました。
他にも
サンプルプロジェクト内には Metal でAR空間上にものを置く処理など、他にもたくさんの情報があります。Metal に触れる機会としては、手頃で分量もちょうど良さそうです。皆さんもお暇な時に遊んでみてはいかがでしょうか。
積極採用中!!
子育て家族アプリFamm、カップル専用アプリPairyを運営するTimers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings