ARKit + Metalで手をゆらゆらさせてみる


ARKitとMetalを使って手をゆらゆらさせる方法を紹介します。

仕上がりは、こんな感じです。

考え方

ARKitのPeople Occlusionを使用します。

People Occlusionを使用すると人型のマスクテクスチャが得られるので、そのマスクとカメラの画像を同時に歪ませて、歪んだ合成用画像を取得します。歪んだまま切り取る感じです。

こんな画像が取得できます(わかりやすくするために動画よりも強めに歪ませています)

この歪ませた手の映像と、元の映像を重ねることで上のような動画を作ることができます。

手順

1.Appleの公式サンプルを入手する

Appleの公式サンプルにPeople Occlusionを使用して人型のマスクテクスチャを得る処理が書かれているため、これをベースに書いていきます。

Effecting People Occlusion in Custom Renderers

2. シェーダーに経過時間を渡す

ゆらゆらさせるには、経過時間を変形式に与える必要があります。

シェーダーに渡すstructを宣言します。

Renderer.swift
struct Uniforms {
    var time: Float = 0
}

次に、経過時間を管理するための変数と、開始時間を宣言します。

Renderer.swift
class Renderer {
    var uniforms = Uniforms()
    private var startDate: Date = Date()
    var uniformsBuffer: MTLBuffer! // シェーダに渡すためのバッファ

そしてシェーダに情報を渡しているcompositeImagesWithEncoderメソッドの中で一緒に経過時間を渡してあげます。

Renderer.swift
uniforms.time = time
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.stride, options: [])
uniformsBuffer.label = "UniformsBuffer"

シェーダー側でもSwift側と同じstructを用意しておき、関数の引数として受け取ります。
引数名はmyUniformsとしています。

Shaders.metal
struct Uniforms {
    float time;
};

fragment half4 compositeImageFragmentShader(CompositeColorInOut in [[ stage_in ]],
                                    texture2d<float, access::sample> capturedImageTextureY [[ texture(0) ]],
                                    texture2d<float, access::sample> capturedImageTextureCbCr [[ texture(1) ]],
                                    texture2d<float, access::sample> sceneColorTexture [[ texture(2) ]],
                                    depth2d<float, access::sample> sceneDepthTexture [[ texture(3) ]],
                                    texture2d<float, access::sample> alphaTexture [[ texture(4) ]],
                                    texture2d<float, access::sample> dilatedDepthTexture [[ texture(5) ]],
                                    constant SharedUniforms &uniforms [[ buffer(kBufferIndexSharedUniforms) ]],
                                    constant Uniforms &myUniforms [[buffer(kBufferIndexMyUniforms)]])
{

3. シェーダーを書き換える

公式サンプルのうち、Shaders.metalのcompositeImageFragmentShader関数を次のように書き換えます。

Shaders.metal
@@ -219,8 +397,9 @@ fragment half4 compositeImageFragmentShader(CompositeColorInOut in [[ stage_in ]
     half4 sceneColor = half4(sceneColorTexture.sample(s, sceneTexCoord));
     float sceneDepth = sceneDepthTexture.sample(s, sceneTexCoord);

+    float2 modifier = float2(sin(cameraTexCoord.y + myUniforms.time*5)*0.2, 0); // 変形の式
     half4 cameraColor = half4(rgb);
-    half alpha = half(alphaTexture.sample(s, cameraTexCoord).r);
+    half alpha = half(alphaTexture.sample(s, cameraTexCoord + modifier).r); // 人型マスクを変形させる

     half showOccluder = 1.0;

@@ -233,8 +412,11 @@ fragment half4 compositeImageFragmentShader(CompositeColorInOut in [[ stage_in ]
         showOccluder = (half)step(dilatedDepth, sceneDepth); // forwardZ case
     }

+    float2 displacedUV = sceneTexCoord + modifier; // 画像を変形させる

-    half4 occluderResult = mix(sceneColor, cameraColor, alpha);
+    half4 displacedCol = half4(sceneColorTexture.sample(s, displacedUV)); // 変形した人型画像の取得
+    half4 occluderResult = mix(sceneColor, displacedCol, alpha); // 変形した画像と元の画像を合成
     half4 mattingResult = mix(sceneColor, occluderResult, showOccluder);
     return mattingResult;
 }

重要なのはここです。

Shaders.metal
float2 modifier = float2(sin(cameraTexCoord.y + myUniforms.time*5)*0.2, 0); // 変形の式

sin関数に入力画像のy座標と、経過時間を足したものを与え、これをmodifyerに代入しています。
modifierはカメラや人型マスクに加算するための変数で、今回はxだけに式が入っているので、x軸方向だけがゆらめくことになります。

なお、実際の動画は縦方向にゆらいでいるので、上の式と矛盾しますがこれは、iPhoneをランドスケープの状態で録画したものを、画像編集ソフトで縦に変換したためです。

図形の変形については、こちらの記事に書きましたので、こちらもご覧ください。

ARKitやSceneKitの図形をMetalシェーダーで変形させる方法

仕上がり

Youtubeにも動画をアップしています。
仕上がりの動画(Youtube)

ソースコードはこちらです。

最後に

NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでは簡単なtipsを発信しています。
https://twitter.com/jugemjugemjugem