見出し画像

ARフィルターMEISAIの光学迷彩フィルターをMetalで動作させる

ARフィルターとして人気アプリにMEISAIというものがあります。

カッコいいですね。初めて見たときは衝撃でした。

どうやって作っているんだろう?と思っていたところ、作っているご本人が作り方をブログに書いていることを教えていただきました。しかもソースコードも公開されています。

もともとのソースコードはUnityで作られたものですが、これをiOSのMetalで動作させてみます。

まずは本物をUnityでビルドしてみる

こちらにリポジトリがありました。

Unityで読み込みます。
Unityのバージョンは最新版にコンバートすると沢山エラーが出るので、素直にこのリポジトリが使っているバージョンである「2019.2.1f1」を使います。

画像1

プロジェクトがロードできたら、下のように選択していきます。

1. Project > Assets > MEISAI > scene > MeisaiSampleを選択する
2. Hierarchy > AR Session Origin > AR Cameraを選択する
3. Post Effect(script)にチェックを入れる

画像4

ここまでやったら、FileメニューからBuildします。
(Buildの方法については他のサイトに色々と書かれているので省略します)

ビルドし結果、うまく動作しました。

画像5

People Occlusionについて

光学迷彩フィルターは、ARKitのPeople Occlusion機能を利用しています。これは、AR上に配置した物体の前に人が立っている時、人の後ろに物体がくるように表示する機能です。

画像2

この機能を使うと、人の部分の領域がどこにあるかという情報を取得することができます。
(手の部分だけを赤くした例)

画像6


光学迷彩フィルターは、人の部分だけに金属のように見えるフィルターをかけることで、あのような効果を実現しています。

この人の部分の領域(マスク)を得る処理は、Appleがサンプルを用意してくれているので、今回はこれをベースに処理を作っていきます。

処理を作るにあたっては、こちらの記事が参考になりました。


Shaders.metalのcompositeImageFragmentShaderの最後のところに、Occlusionの処理が書かれています。

(元のコード)

half4 occluderResult = mix(sceneColor, cameraColor, alpha);
half4 mattingResult = mix(sceneColor, occluderResult, showOccluder);
return mattingResult;

詳しく見ていきます。

このalphaという変数には、人の部分のマスク画像のaplha値が入っています。また、showOccluderには深さにもとづいた仮想オブジェクトと人の前後関係を示した値が入っています。

人のマスク画像と深さ画像はARMatteGeneratorというクラスがARKitに用意されており、これを使用して取得しています。ARMatteGeneratorを用いることで、人と仮想コンテンツのどちらが手前にあるかによって、どちらを見せるかを決定するためのマスクテクスチャが得られます。

Appleのサンプルではこのあたりです。

Renderer.swift: 521

// マスク用のアルファテクスチャを得る
alphaTexture = matteGenerator.generateMatte(from: currentFrame, commandBuffer: commandBuffer)
// 深度情報のテクスチャを得る
dilatedDepthTexture = matteGenerator.generateDilatedDepth(from: currentFrame, commandBuffer: commandBuffer)

ARMatteGeneratorについてはこちらに詳しく書いてありました。

得られたapha値をmix関数で処理して、人が常に前面になる画像を得ています。

(再掲)
half4 occluderResult = mix(sceneColor, cameraColor, alpha);

mixは線形補間を得る関数なので、仮想世界の色(sceneColor)、カメラの色(cameraColor)のうち、人の部分はcameraColorを、仮想オブジェクトの部分はsceneColorを使用するようになっています。つまり、人が仮想オブジェクトよりも常に前に存在する映像ができます。これはあとで合成するのに使います。

例えば、このaplhaを使って仮想オブジェクトの画像の代わりに赤い画像とカメラ画像をmix関数にかけるとこんな感じになります。

half4 occluderResult = mix(half4(float4(1.0, 0.0, 0.0, 1.0)), cameraColor, alpha);
return occluderResult;

画像6

背景が赤くなりました。何かに使えそうな予感がしますね。

このようにして得られた、人が前に来ている映像(occluderResult)と、仮想オブジェクトの映像(sceneColor)をさらに深度情報(showOccluder)でmix関数にかけます。

half4 mattingResult = mix(sceneColor, occluderResult, showOccluder);
return mattingResult;

これにより、仮想オブジェクトと人の前後関係を加味した映像が作り出されます。

画像7

今回は、仮想オブジェクトは配置しないので、alphaの情報だけを使用します。

光学迷彩のフィルターをMetalに移植する

Appleのサンプルに、光学迷彩のフィルターを移植すれば処理が実現できます。

オリジナルのソースコードを見てみましょう。

ARFoundationMeisaiDemo/arfoundation-samples/Assets/MEISAI/shader/MEISAI.shader

fixed4 frag (v2f i) : SV_Target
{

   fixed4 camCol = tex2D(_MainTex, i.uv);

   float2 displacedUV = float2(
       0.5 * snoise(float3(i.uv.x+camCol.r*2.0,i.uv.y+camCol.g*2.0, _Time.y*0.3)),
       0.5 * snoise(float3(i.uv.x+camCol.g*2.0,i.uv.y+camCol.b*2.0, _Time.y*0.4))
   );
   displacedUV = frac( i.uv + displacedUV );

   fixed4 displacedCol = tex2D(_MainTex, displacedUV);

   fixed4 stencil = tex2D(_StencilTex, GetStencilUV(i.uv));

   return lerp( camCol, displacedCol, stencil.r);

}

snoiseという関数を呼び出して、displacedColというテクスチャを得ています。詳しいパラメータはわかりませんが、これが光学迷彩の正体のようです。

試しに画面全体にエフェクトをかけてみます。

画像8

かっこいいですね。アルミホイルで移しだしたような映像が生まれます。

このsnoiseですが、高橋 啓治郎さんという有名な方が作ったフィルターのようです。

これをMetalシェーダーに移植します。

オリジナルでは次の場所に配置されています。

RFoundationMeisaiDemo/arfoundation-samples/Assets/MEISAI/shader/noise/SimplexNoise3D.hlsl

hlslというシェーダー言語で書かれていましたが、なんとそのままMetalシェーダーで動作しました。

変数もfloat3とかfloat4などMSLと同じなんですね。

ということで、snoiseは移植できました。

最後に、フラグメントシェーダ(オリジナルソースのfrag関数)を移植します。

AppleのサンプルのcompositeImageFragmentShaderの最後を次のようします。

float2 _Time = float2(0,0);
float2 displacedUV = float2(
   0.5 * snoise(float3(sceneTexCoord.x+cameraColor.r*2.0,sceneTexCoord.y+cameraColor.g*2.0, _Time.y*0.3)),
   0.5 * snoise(float3(sceneTexCoord.x+cameraColor.g*2.0,sceneTexCoord.y+cameraColor.b*2.0, _Time.y*0.4))
);
displacedUV = fract( sceneTexCoord + displacedUV );

half4 displacedCol = half4(sceneColorTexture.sample(s, displacedUV));
half4 occluderResult = mix(sceneColor, displacedCol, alpha);
half4 mattingResult = mix(sceneColor, occluderResult, showOccluder);
return mattingResult;

オリジナルの変数iは画面のテクスチャー座標、camColはカメラ画像のテクスチャーなのでそれぞれ対応するものを入れています。

オリジナルでは、stencilとして、マスクテクスチャーを得ていましたが、これはaplhaとして既に得られているのでこれを使用しています。

_Timeは経過時間を現しますが、今回は何も指定していません。
下の記事で経過時間を渡す方法を書いたので、これを参考に設定してみても良いかもしれません。

仕上がり

なんとなく魅力が落ちた気がしますが(家のせい?)、雰囲気は出ていますね。


作ったものはこちらのリポジトリに置きました。

なお、若干宣伝ぽくて恐縮ですが、私はフリーランスエンジニアをしております。
このような効果をアプリで実現したいという方がいらっしゃいましたら、
効果の部分の作成だけでも受託できますのでお気軽にご相談下さい。

連絡先名:TokyoYoshida
連絡先: yoshidaforpublic@gmail.com


この記事が気に入ったらサポートをしてみませんか?