カメラ画像をレンダリングし、位置トラッキング情報を使ってオーバーレイコンテンツを表示するカスタムARビューを構築します。
概要
ARKitには、SceneKitまたはSpriteKitを使ってAR体験を簡単に表示するためのビュークラスが含まれています。しかしその代わりに、独自のレンダリングエンジンを構築する場合またはサードパーティ製のエンジンと統合する場合でも、ARKitは、カスタムビューによるAR体験の表示に必要なすべてのサポートを提供します。
![](https://docs-assets.developer.apple.com/published/ffb3831f78/c162c528-dc03-494d-a5da-c23a8691a98e.png)
どのAR体験の場合も、最初のステップとなるのは、カメラキャプチャとモーションの処理を管理するARSession(英語)オブジェクトを設定することです。セッションによって、デバイスが存在している現実の空間と、ARコンテンツのモデリングの場所であるバーチャル空間との対応が定義され、維持管理されます。AR体験をカスタムビューで表示するには、以下の作業が必要になります。
-
ビデオフレームとトラッキング情報をセッションから取得します。
-
取得したフレーム画像をビューの背景としてレンダリングします。
-
トラッキング情報を使って、カメラ画像の上にARコンテンツを配置し、描画します。
メモ
この記事で取り上げるのは、Xcodeのプロジェクトテンプレートに収められているコードです。完全なサンプルコードを参照するには、Augmented Realityテンプレートを使ってiOSアプリを新しく作成し、「Content Technology」ポップアップメニューの「Metal」を選択します。
セッションからのビデオフレームとトラッキングデータの取得
独自のARSession(英語)インスタンスを生成して維持管理し、対応させたいAR体験の種類に応じたセッション設定で実行します。このセッションは、カメラから映像をキャプチャし、モデリングされた3D空間でのデバイスの位置と向きをトラッキングして、ARFrame(英語)オブジェクトを提供します。これらの各オブジェクトは、ビデオフレームがキャプチャされた時点での、個々のビデオフレームの画像と位置トラッキング情報の両方を保持しています。
ARセッションによって生成されるARFrame(英語)オブジェクトにアクセスする方法は2種類あり、アプリがプルとプッシュのデザインパターンのどちらを好むかによって決まります。
フレームタイミングを制御したい場合(プルのデザインパターン)は、セッションの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)
}
}
別の方法として、アプリのデザインがプッシュパターンを好む場合は、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;
}
メモ
カメラの画像がビュー全体をカバーするようにするには、display
オーバーレイコンテンツのトラッキングとレンダリング
一般に、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
}
}
メモ
より複雑なAR体験では、現実世界の面の位置を見つけるのにヒットテストまたは平面検出を使用することができます。詳しくは、plane
リアルなライティングを用いたレンダリング
シーンに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
メモ
この例の完全なMetalの設定とレンダリングコマンドは、Xcodeテンプレートの全体を参照してください(Augmented Realityテンプレートを使ってiOSアプリを新しく作成し、「Content Technology」ポップアップメニューの「Metal」を選択します)。