アバターカメラの仕組み(Texture渡し編

先日、Mirrativさんとの合同勉強会があり、そこで「アバターカメラの仕組み」というタイトルで発表がありました。

本エントリでは、その勉強会でお話しした内容を再構成して公開します。
前半ではアバターカメラ機能の紹介、後半ではアバターカメラ機能を支えるTexture渡しについて実装に触れながらメリットを紹介します。

アバターカメラとは?

撮影した写真にアバターを合成してシェアできる機能です。
編集画面では、テキストや自分のQRコードを追加したり、アバターや背景、文字、QRコードの大きさや位置を自由に操作できます。

スクリーンショット 2021-08-05 15.51.19

ユーザーの反応

Texture渡しとは?

このアバターカメラ機能を支えている技術の1つにTexture渡しがあります。

Texture渡しとは、UnityからネイティブにTextureを渡し、ネイティブでいい感じの処理をするREALITY内の仕組みです。※1
アバターカメラでは、背景とアバターの撮影画面はUnity、編集画面はネイティブで実装しているので、撮影画面から編集画面に遷移する際に、Texture渡しが実行されます。

※1:REALITYアプリは、Unity as a Library(のようなもの)により、ネイティブアプリをベースに、Unity部はライブラリとして埋め込んでいます。この記事ではC#でUnityの環境で実装された領域をUnity、SwiftでiOSネイティブの環境で実装された領域をネイティブと表現します。

スクリーンショット 2021-08-05 16.07.19

Texture渡しの実装

Texture渡しは、UnityのTexture.GetNativeTexturePtrを利用して、テクスチャリソースへのネイティブポインタを取得し、ネイティブに渡して処理します。

取得できるポインタはプラットフォームごとに異なり、iOSではid<MTLTexture>が取得できます。

取得したポインタはUnityのNative Pluginの仕組みを使って、ネイティブに渡します。
Unity側で、[DllImport("__Internal")] をつけたstatic externなTextureのポインタを送信する関数を宣言します。

[DllImport("__Internal")]
private static extern void _ui_sendTexture(IntPtr texture);
// ~~中略~~
public static void SendTexture(Texture texture)
{
   _ui_sendTexture(texture.GetNativeTexturePtr());
}

Unityで定義した関数をObjective-Cで実装します。
受け取ったポインタをid<MTLTexture>にキャストし、Swiftに送信します。

void _ui_sendTexture(void *texture) {
   id<MTLTexture> tex = (__bridge id<MTLTexture>)texture;
   [[RealityAvatar shared] sendTexture:tex];
}

送信されたTextureを受け取り、処理します。

class RealityAvatar {
   static let shared = RealityAvatar()
   func send(texture: MTLTexture) {
      // UnityのTextureを取得
      // このTextureを活用する
   }
}

ネイティブでのTextureの活用(png編)

受け取ったMTLTextureをCoreImage Frameworkを利用してpngに変換することが可能です。
例えば、REALITYアプリでは、アバターカメラ機能の他に配信のスクショ機能で活用されています。

let texture: MTLTexture = /*取得したtexture*/
let context = CIContext()
let ci = CIImage(mtlTexture: texture, options: [:])!
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
let data = context.pngRepresentation(of: ci,
                                     format: .RGBA8,
                                     colorSpace: colorSpace,
                                     options: [:])!

たしかに、UnityのTexture2D.EncodeToPNGを使っても、pngに変換することが可能です。

RenderTexture renderTexture
var current = RenderTexture.active;
RenderTexture.active = renderTexture;
var texture2D = new Texture2D(renderTexture.width, renderTexture.height);
texture2D.ReadPixels(new Rect(0, 0, texture2D.width, texture2D.height), 0, 0, false);
texture2D.Apply();
RenderTexture.active = current;
var data = texture2D.EncodeToPNG();

しかし、ネイティブで処理する事で、高速にpngに変換することが可能です。

先ほど紹介したTexture2D.EncodeToPNGを使う手法では、RenderTextureからpngに変換するまでに、0.2sec ~ 0.3sec必要です。EncodeToPNGの処理が8割以上を占めます。
これに加えて、アルバムに保存する場合は取得したデータをNativeに渡す時間も必要です。

一方で、CoreImage Frameworkを使う手法では、Textureを渡す時間込みで、0.07sec ~ 0.09secで実行可能です。
Texture渡しをしてネイティブで処理することで、Unityで処理する場合に比べて、最大で約3倍高速にpngに変換することが可能です。

ネイティブでのTextureの活用(UI編)

REALITYでは、受け取ったMTLTextureをUIImageに変換することで、ネイティブのUIとして表示しています。
iOSのアバターカメラ機能では、UIImageViewを使って、Unityで撮影したアバターや背景を拡大縮小させています。
Unityでは実装が難しい複雑なUIも、ネイティブで実装することで実現する事ができました。

スクリーンショット 2021-08-05 17.58.30

MTLTextureからUIImageの変換は、CIImageとCGImageを経由させます。

let texture: MTLTexture = /*取得したtexture*/
let context = CIContext()
let ci = CIImage(mtlTexture: texture, options: [:])!
let cg = context.createCGImage(ci, from: ci.extent)!
let ui = UIImage(cgImage: cg)

この変換は、0.01sec ~ 0.02secで実行可能なので、フレームレートの低下など気にせず使う事が可能です。

まとめ

前半ではアバターカメラ機能の紹介、後半ではアバターカメラ機能を支えるTexture渡しについて実装に触れながらメリットを紹介しました。
REALITYアプリでは、Unityとネイティブを組み合わせることで、高速なメディア処理や、Unityで撮影した画像を高度なUIで利用する事を実現しています。

もし一緒にUnityとネイティブを組み合わせて快適なユーザ体験をつくってくれる、REALITYに興味のあるエンジニアの方がいらっしゃいましたら、下記のリンクからお気軽に話を聞きにきてくれると嬉しいです。

→ REALITYエンジニアカジュアル面談お申し込みフォームはこちら